From 07b5c7617ce387456404f17926abb34754a35fad Mon Sep 17 00:00:00 2001 From: fraxle Date: Mon, 18 May 2026 21:04:17 +0100 Subject: [PATCH] build.js setup --- build.js | 172 ++++++++++++++++++ package-lock.json | 445 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 ++ 3 files changed, 630 insertions(+) create mode 100644 build.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/build.js b/build.js new file mode 100644 index 0000000..b843a5c --- /dev/null +++ b/build.js @@ -0,0 +1,172 @@ +#!/usr/bin/env node +// build.js -- SunScope production bundler +// +// Usage: node build.js OR npm run build +// +// What it does: +// 1. Bundles + minifies JS (entry: assets/js/main.js) -> dist/assets/bundle.min.js +// 2. Resolves CSS @imports + minifies -> dist/assets/bundle.min.css +// 3. Copies index.html / about.html to dist/, rewriting script+link tags +// 4. Copies static files (images, robots.txt, sitemap.xml) +// +// Dev files are NEVER touched. Deploy from dist/ for production. + +import * as esbuild from 'esbuild'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = __dirname; +const DIST = path.join(ROOT, 'dist'); +const ASSETS = path.join(DIST, 'assets'); + +// ---- Helpers --------------------------------------------------------------- + +function copyFile(src, dest) { + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(src, dest); +} + +// Delete specific output files without trying to rmdir the folders +// (rmdir on Windows-mounted paths can fail inside Linux sandboxes) +function cleanDist() { + const targets = [ + path.join(ASSETS, 'bundle.min.js'), + path.join(ASSETS, 'bundle.min.css'), + path.join(DIST, 'index.html'), + path.join(DIST, 'about.html'), + path.join(ASSETS, '_tmp_flat.css'), + ]; + for (const f of targets) { + try { fs.unlinkSync(f); } catch (_) { /* ok if missing */ } + } +} + +// Recursively resolve CSS @import statements into one flat string +function resolveCSSImports(filePath, visited = new Set()) { + const resolved = path.resolve(filePath); + if (visited.has(resolved)) return ''; + visited.add(resolved); + const dir = path.dirname(resolved); + const content = fs.readFileSync(resolved, 'utf8'); + return content.replace(/@import\s+["']([^"']+)["'];?/g, (_, importPath) => { + const importFile = path.join(dir, importPath); + if (!fs.existsSync(importFile)) { + console.warn(' WARNING: CSS @import not found: ' + importFile); + return ''; + } + return resolveCSSImports(importFile, visited); + }); +} + +// Short cache-buster string +function bumpVersion() { + return Math.floor(Date.now() / 1000).toString(36); +} + +// Swap dev asset tags in HTML for bundle tags +function rewriteHTML(html, version) { + html = html.replace(/]*href=["'][^"']*assets\/[^"']*\.css[^"']*["'][^>]*>/gi, ''); + html = html.replace(/]*src=["'][^"']*assets\/js\/[^"']*["'][^>]*><\/script>/gi, ''); + const css = ' '; + const js = ' '; + html = html.replace('', css + '\n' + js + '\n'); + return html; +} + +// esbuild plugin: strips JSX block comments {/* ... */} that may contain +// backticks or other characters that trip up esbuild's JS parser. +// Safe to remove -- these are only developer notes, never runtime code. +const stripJSXCommentsPlugin = { + name: 'strip-jsx-comments', + setup(build) { + build.onLoad({ filter: /\.js$/ }, async (args) => { + let source = await fs.promises.readFile(args.path, 'utf8'); + // Remove {/* ... */} comment blocks (non-greedy, dotAll) + source = source.replace(/\{\/\*[\s\S]*?\*\/\}/g, ''); + return { contents: source, loader: 'js' }; + }); + }, +}; + +// ---- Main ------------------------------------------------------------------ + +async function build() { + const t0 = Date.now(); + console.log('SunScope build starting...'); + + cleanDist(); + fs.mkdirSync(ASSETS, { recursive: true }); + console.log(' Output dir ready: dist/'); + + // 2. Bundle + minify JavaScript + console.log(' Bundling JS...'); + await esbuild.build({ + entryPoints: [path.join(ROOT, 'assets/js/main.js')], + bundle: true, + minify: true, + format: 'iife', + globalName: 'SunScope', + outfile: path.join(ASSETS, 'bundle.min.js'), + logLevel: 'warning', + plugins: [stripJSXCommentsPlugin], + }); + const jsSize = fs.statSync(path.join(ASSETS, 'bundle.min.js')).size; + console.log(' JS bundle: ' + (jsSize / 1024).toFixed(1) + ' KB'); + + // 3. Resolve CSS @imports then minify + console.log(' Bundling CSS...'); + const flatCSS = resolveCSSImports(path.join(ROOT, 'assets/sunscope.css')); + const tmpCSS = path.join(os.tmpdir(), '_sunscope_flat.css'); + fs.writeFileSync(tmpCSS, flatCSS, 'utf8'); + await esbuild.build({ + entryPoints: [tmpCSS], + bundle: false, + minify: true, + outfile: path.join(ASSETS, 'bundle.min.css'), + logLevel: 'warning', + }); + fs.unlinkSync(tmpCSS); + const cssSize = fs.statSync(path.join(ASSETS, 'bundle.min.css')).size; + console.log(' CSS bundle: ' + (cssSize / 1024).toFixed(1) + ' KB'); + + // 4. Process HTML files + console.log(' Processing HTML...'); + const version = bumpVersion(); + for (const htmlFile of ['index.html', 'about.html']) { + const src = path.join(ROOT, htmlFile); + if (!fs.existsSync(src)) continue; + const updated = rewriteHTML(fs.readFileSync(src, 'utf8'), version); + fs.writeFileSync(path.join(DIST, htmlFile), updated, 'utf8'); + console.log(' -> ' + htmlFile); + } + + // 5. Copy static files + console.log(' Copying static files...'); + for (const f of ['robots.txt', 'sitemap.xml', 'og-image.png']) { + const src = path.join(ROOT, f); + if (fs.existsSync(src)) { + copyFile(src, path.join(DIST, f)); + console.log(' -> ' + f); + } + } + + const elapsed = ((Date.now() - t0) / 1000).toFixed(2); + console.log(''); + console.log('========================================'); + console.log(' Build complete in ' + elapsed + 's'); + console.log(' Output: dist/'); + console.log(' JS: ' + (jsSize / 1024).toFixed(1) + ' KB'); + console.log(' CSS: ' + (cssSize / 1024).toFixed(1) + ' KB'); + console.log('========================================'); + console.log(' Deploy the dist/ folder to production.'); + console.log(' Dev files in assets/ are untouched.'); + console.log('========================================'); +} + +build().catch(err => { + console.error('Build failed: ' + err.message); + process.exit(1); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2623e04 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,445 @@ +{ + "name": "sunscope", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sunscope", + "version": "1.0.0", + "devDependencies": { + "esbuild": "^0.21.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..74c4a50 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "sunscope", + "version": "1.0.0", + "description": "SunScope – UTCI Thermal Comfort Forecast", + "type": "module", + "scripts": { + "build": "node build.js", + "build:watch": "node --watch build.js" + }, + "devDependencies": { + "esbuild": "^0.21.0" + } +}