From 52930445fc14840fa73b291a0b5c866065b745d8 Mon Sep 17 00:00:00 2001 From: Jonathan Hornung Date: Sat, 16 Aug 2025 08:29:59 +0200 Subject: [PATCH] using webpack #122 --- .github/workflows/test.yml | 3 +- README.md | 6 +- jest-puppeteer.config.js | 2 +- package.json | 9 +- build-libs.js => webpack-libs-plugin.js | 150 +++++++++++------------- webpack.config.js | 57 ++++----- webpack.libs.config.js | 21 ++++ 7 files changed, 128 insertions(+), 120 deletions(-) rename build-libs.js => webpack-libs-plugin.js (72%) mode change 100755 => 100644 create mode 100644 webpack.libs.config.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ceea13..45eb46b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,8 +25,7 @@ jobs: with: node-version: ${{ matrix.node.version }} - run: npm install - - run: make public - - run: npm run build + - run: npm run build:all - name: Archive production artifacts uses: actions/upload-artifact@v4 with: diff --git a/README.md b/README.md index 4c1831c..6b1768d 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ Licenses: see [LICENSES](./LICENSE). ## Building +The project uses a **webpack-based build system** that reads library metadata from `libs-config.json` to automatically download, clone, and package OpenSCAD libraries and dependencies. This replaces the previous Makefile approach with a more standard, maintainable solution. + Prerequisites: * wget or curl * Node.js (>=18.12.0) @@ -90,7 +92,7 @@ rm -fR ../ochafik.github.io/openscad2 && cp -R dist ../ochafik.github.io/opensca ## Build your own WASM binary -[Makefile](./Makefile) fetches a prebuilt OpenSCAD web WASM binary, but you can build your own in a couple of minutes: +The build system fetches a prebuilt OpenSCAD web WASM binary, but you can build your own in a couple of minutes: - **Optional**: use your own openscad fork / branch: @@ -117,7 +119,7 @@ rm -fR ../ochafik.github.io/openscad2 && cp -R dist ../ochafik.github.io/opensca ## Adding OpenSCAD libraries -You'll need to update 3 files (search for BOSL2 for an example): +The build system uses a webpack plugin that reads from `libs-config.json` to manage all library dependencies. You'll need to update 3 files (search for BOSL2 for an example): - [libs-config.json](./libs-config.json): to add the library's metadata including repository URL, branch, and files to include/exclude in the zip archive diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js index 4de2291..c171789 100644 --- a/jest-puppeteer.config.js +++ b/jest-puppeteer.config.js @@ -8,7 +8,7 @@ const config = { ], }, server: { - command: `npm run start:${process.env.NODE_ENV}`, + command: `npm run start:${process.env.NODE_ENV || 'test'}`, port: process.env.NODE_ENV === 'production' ? 3000 : 4000, launchTimeout: 180000, }, diff --git a/package.json b/package.json index 6f026e2..6a9b3bd 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,13 @@ "test:e2e": "jest", "start:development": "npx webpack serve --mode=development", "start:production": "NODE_ENV=production PUBLIC_URL=http://localhost:3000/dist/ npm run build && npx serve", + "start:test": "npm run start:development", "start": "npm run start:development", "build": "NODE_ENV=production webpack --mode=production", - "build:libs": "node build-libs.js build", - "build:libs:clean": "node build-libs.js clean", - "build:libs:wasm": "node build-libs.js wasm", - "build:libs:fonts": "node build-libs.js fonts", + "build:libs": "LIBS_BUILD_MODE=all webpack --config webpack.libs.config.js", + "build:libs:clean": "LIBS_BUILD_MODE=clean webpack --config webpack.libs.config.js", + "build:libs:wasm": "LIBS_BUILD_MODE=wasm webpack --config webpack.libs.config.js", + "build:libs:fonts": "LIBS_BUILD_MODE=fonts webpack --config webpack.libs.config.js", "build:all": "npm run build:libs && npm run build" }, "eslintConfig": { diff --git a/build-libs.js b/webpack-libs-plugin.js old mode 100755 new mode 100644 similarity index 72% rename from build-libs.js rename to webpack-libs-plugin.js index 3981ba8..273d6b6 --- a/build-libs.js +++ b/webpack-libs-plugin.js @@ -1,32 +1,63 @@ #!/usr/bin/env node -import fs from 'fs/promises'; -import { createWriteStream, existsSync } from 'fs'; -import path from 'path'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import https from 'https'; -import { pipeline } from 'stream/promises'; +import { exec } from 'node:child_process'; +import { createWriteStream, existsSync } from 'node:fs'; +import fs from 'node:fs/promises'; +import https from 'node:https'; +import path from 'node:path'; +import { pipeline } from 'node:stream/promises'; +import { promisify } from 'node:util'; const execAsync = promisify(exec); -const CONFIG_FILE = 'libs-config.json'; -const LIBS_DIR = 'libs'; -const PUBLIC_LIBS_DIR = 'public/libraries'; -const SRC_WASM_DIR = 'src/wasm'; - -class LibsBuilder { - constructor() { +class OpenSCADLibrariesPlugin { + constructor(options = {}) { + this.configFile = options.configFile || 'libs-config.json'; + this.libsDir = options.libsDir || 'libs'; + this.publicLibsDir = options.publicLibsDir || 'public/libraries'; + this.srcWasmDir = options.srcWasmDir || 'src/wasm'; + this.buildMode = options.buildMode || 'all'; // 'all', 'wasm', 'fonts', 'libs' this.config = null; } + apply(compiler) { + const pluginName = 'OpenSCADLibrariesPlugin'; + + compiler.hooks.beforeRun.tapAsync(pluginName, async (_, callback) => { + try { + await this.loadConfig(); + + switch (this.buildMode) { + case 'all': + await this.buildAll(); + break; + case 'wasm': + await this.buildWasm(); + break; + case 'fonts': + await this.buildFonts(); + break; + case 'libs': + await this.buildAllLibraries(); + break; + case 'clean': + await this.clean(); + break; + } + + callback(); + } catch (error) { + callback(error); + } + }); + } + async loadConfig() { try { - const configContent = await fs.readFile(CONFIG_FILE, 'utf-8'); + const configContent = await fs.readFile(this.configFile, 'utf-8'); this.config = JSON.parse(configContent); } catch (error) { - console.error(`Failed to load config from ${CONFIG_FILE}:`, error.message); - process.exit(1); + throw new Error(`Failed to load config from ${this.configFile}: ${error.message}`); } } @@ -46,7 +77,6 @@ class LibsBuilder { return new Promise((resolve, reject) => { https.get(url, (response) => { if (response.statusCode === 302 || response.statusCode === 301) { - // Handle redirects return this.downloadFile(response.headers.location, outputPath) .then(resolve) .catch(reject); @@ -95,23 +125,18 @@ class LibsBuilder { if (includes.length > 0) { const findPatterns = includes.map(pattern => { if (pattern.includes('**/*.')) { - // Pattern like "examples/**/*.scad" const parts = pattern.split('/'); const dir = parts[0]; const filePattern = parts[parts.length - 1]; return `-path "./${dir}/*" -name "${filePattern}"`; } else if (pattern.includes('**')) { - // Pattern like "**/*.scad" const filePattern = pattern.replace('**/', ''); return `-name "${filePattern}"`; } else if (pattern.includes('*')) { - // Pattern like "*.scad" return `-name "${pattern}"`; } else if (pattern.includes('/')) { - // Path pattern like "bitmap/*.scad" return `-path "./${pattern}"`; } else { - // Direct file/directory name return `-name "${pattern}" -o -path "./${pattern}/*"`; } }).join(' -o '); @@ -132,7 +157,6 @@ class LibsBuilder { const zipCmd = `cd ${fullSourceDir} && ${findCmd} | zip -r ${path.resolve(outputPath)} -@`; console.log(`Creating zip: ${outputPath}`); - console.log(`Zip command: ${zipCmd}`); try { await execAsync(zipCmd); } catch (error) { @@ -146,22 +170,16 @@ class LibsBuilder { const wasmDir = wasmBuild.target; const wasmZip = `${wasmDir}.zip`; - // Create libs directory - await this.ensureDir(LIBS_DIR); + await this.ensureDir(this.libsDir); - // Download WASM if not exists if (!existsSync(wasmDir)) { await this.ensureDir(wasmDir); - - // Download WASM zip await this.downloadFile(wasmBuild.url, wasmZip); - // Extract WASM zip console.log(`Extracting WASM to ${wasmDir}`); await execAsync(`cd ${wasmDir} && unzip ../${path.basename(wasmZip)}`); } - // Create symlinks for public files await this.ensureDir('public'); const jsTarget = 'public/openscad.js'; @@ -170,34 +188,28 @@ class LibsBuilder { // Remove existing symlinks/files try { await fs.unlink(jsTarget); - } catch { - // ignore - file doesn't exist - } + } catch { /* ignore */ } try { await fs.unlink(wasmTarget); - } catch { - // ignore - file doesn't exist - } + } catch { /* ignore */ } - // Create new symlinks - use relative paths for portability + // Create new symlinks await fs.symlink(path.relative('public', path.join(wasmDir, 'openscad.js')), jsTarget); await fs.symlink(path.relative('public', path.join(wasmDir, 'openscad.wasm')), wasmTarget); // Create src/wasm symlink try { - await fs.unlink(SRC_WASM_DIR); - } catch { - // ignore - file doesn't exist - } - await fs.symlink(path.relative('src', wasmDir), SRC_WASM_DIR); + await fs.unlink(this.srcWasmDir); + } catch { /* ignore */ } + await fs.symlink(path.relative('src', wasmDir), this.srcWasmDir); console.log('WASM setup completed'); } async buildFonts() { const { fonts } = this.config; - const notoDir = path.join(LIBS_DIR, 'noto'); - const liberationDir = path.join(LIBS_DIR, 'liberation'); + const notoDir = path.join(this.libsDir, 'noto'); + const liberationDir = path.join(this.libsDir, 'liberation'); await this.ensureDir(notoDir); @@ -216,8 +228,8 @@ class LibsBuilder { } // Create fonts zip - const fontsZip = path.join(PUBLIC_LIBS_DIR, 'fonts.zip'); - await this.ensureDir(PUBLIC_LIBS_DIR); + const fontsZip = path.join(this.publicLibsDir, 'fonts.zip'); + await this.ensureDir(this.publicLibsDir); console.log('Creating fonts.zip'); const fontsCmd = `zip -r ${fontsZip} -j fonts.conf libs/noto/*.ttf libs/liberation/*.ttf libs/liberation/LICENSE libs/liberation/AUTHORS`; @@ -227,8 +239,8 @@ class LibsBuilder { } async buildLibrary(library) { - const libDir = path.join(LIBS_DIR, library.name); - const zipPath = path.join(PUBLIC_LIBS_DIR, `${library.name}.zip`); + const libDir = path.join(this.libsDir, library.name); + const zipPath = path.join(this.publicLibsDir, `${library.name}.zip`); // Clone repository if not exists if (!existsSync(libDir)) { @@ -248,7 +260,7 @@ class LibsBuilder { } async buildAllLibraries() { - await this.ensureDir(PUBLIC_LIBS_DIR); + await this.ensureDir(this.publicLibsDir); for (const library of this.config.libraries) { await this.buildLibrary(library); @@ -259,12 +271,12 @@ class LibsBuilder { console.log('Cleaning build artifacts...'); const cleanPaths = [ - LIBS_DIR, + this.libsDir, 'build', 'public/openscad.js', 'public/openscad.wasm', - `${PUBLIC_LIBS_DIR}/*.zip`, - SRC_WASM_DIR + `${this.publicLibsDir}/*.zip`, + this.srcWasmDir ]; for (const cleanPath of cleanPaths) { @@ -282,7 +294,7 @@ class LibsBuilder { console.log('Clean completed'); } - async build() { + async buildAll() { console.log('Building all libraries...'); await this.buildWasm(); @@ -293,32 +305,4 @@ class LibsBuilder { } } -async function main() { - const builder = new LibsBuilder(); - await builder.loadConfig(); - - const command = process.argv[2] || 'build'; - - switch (command) { - case 'build': - await builder.build(); - break; - case 'clean': - await builder.clean(); - break; - case 'wasm': - await builder.buildWasm(); - break; - case 'fonts': - await builder.buildFonts(); - break; - default: - console.log('Usage: node build-libs.js [build|clean|wasm|fonts]'); - process.exit(1); - } -} - -main().catch(error => { - console.error('Build failed:', error); - process.exit(1); -}); +export default OpenSCADLibrariesPlugin; diff --git a/webpack.config.js b/webpack.config.js index 2596075..5d68f2d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,16 +1,13 @@ import CopyPlugin from 'copy-webpack-plugin'; -import WorkboxPlugin from 'workbox-webpack-plugin'; import webpack from 'webpack'; -import packageConfig from './package.json' with {type: 'json'}; +import WorkboxPlugin from 'workbox-webpack-plugin'; -import path, {dirname} from 'path'; -import {fileURLToPath} from 'url'; +import path, { dirname } from 'path'; +import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const LOCAL_URL = process.env.LOCAL_URL ?? 'http://localhost:4000/'; -const PUBLIC_URL = process.env.PUBLIC_URL ?? packageConfig.homepage; const isDev = process.env.NODE_ENV !== 'production'; @@ -48,7 +45,7 @@ const config = [ 'style-loader', { loader: 'css-loader', - options:{url: false}, + options: { url: false }, } ] }, @@ -76,28 +73,28 @@ const config = [ }), ...(process.env.NODE_ENV === 'production' ? [ new WorkboxPlugin.GenerateSW({ - exclude: [ - /(^|\/)\./, - /\.map$/, - /^manifest.*\.js$/, - ], - // these options encourage the ServiceWorkers to get in there fast - // and not allow any straggling 'old' SWs to hang around - swDest: path.join(__dirname, 'dist', 'sw.js'), - maximumFileSizeToCacheInBytes: 200 * 1024 * 1024, - clientsClaim: true, - skipWaiting: true, - runtimeCaching: [{ - urlPattern: ({request, url}) => true, - handler: 'StaleWhileRevalidate', - options: { - cacheName: 'all', - expiration: { - maxEntries: 1000, - purgeOnQuotaError: true, - }, + exclude: [ + /(^|\/)\./, + /\.map$/, + /^manifest.*\.js$/, + ], + // these options encourage the ServiceWorkers to get in there fast + // and not allow any straggling 'old' SWs to hang around + swDest: path.join(__dirname, 'dist', 'sw.js'), + maximumFileSizeToCacheInBytes: 200 * 1024 * 1024, + clientsClaim: true, + skipWaiting: true, + runtimeCaching: [{ + urlPattern: ({ request, url }) => true, + handler: 'StaleWhileRevalidate', + options: { + cacheName: 'all', + expiration: { + maxEntries: 1000, + purgeOnQuotaError: true, }, - }], + }, + }], }), ] : []), new CopyPlugin({ @@ -113,7 +110,11 @@ const config = [ }, { from: path.resolve(__dirname, 'src/wasm/openscad.js'), + to: path.resolve(__dirname, 'dist'), + }, + { from: path.resolve(__dirname, 'src/wasm/openscad.wasm'), + to: path.resolve(__dirname, 'dist'), }, ], }), diff --git a/webpack.libs.config.js b/webpack.libs.config.js new file mode 100644 index 0000000..9507b12 --- /dev/null +++ b/webpack.libs.config.js @@ -0,0 +1,21 @@ +import OpenSCADLibrariesPlugin from './webpack-libs-plugin.js'; + +const buildMode = process.env.LIBS_BUILD_MODE || 'all'; + +/** @type {import('webpack').Configuration} */ +const config = { + mode: 'none', // We're not actually building JS, just using webpack as a task runner + entry: './package.json', // Dummy entry point that exists + output: { + path: '/tmp', // Output to temp directory + filename: 'webpack-libs-temp.js', // This won't be used + }, + plugins: [ + new OpenSCADLibrariesPlugin({ + buildMode: buildMode + }), + ], + stats: 'minimal', +}; + +export default config;