using webpack #122

This commit is contained in:
Jonathan Hornung 2025-08-16 08:29:59 +02:00
commit 52930445fc
7 changed files with 128 additions and 120 deletions

View file

@ -25,8 +25,7 @@ jobs:
with: with:
node-version: ${{ matrix.node.version }} node-version: ${{ matrix.node.version }}
- run: npm install - run: npm install
- run: make public - run: npm run build:all
- run: npm run build
- name: Archive production artifacts - name: Archive production artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:

View file

@ -48,6 +48,8 @@ Licenses: see [LICENSES](./LICENSE).
## Building ## 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: Prerequisites:
* wget or curl * wget or curl
* Node.js (>=18.12.0) * 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 ## 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: - **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 ## 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 - [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

View file

@ -8,7 +8,7 @@ const config = {
], ],
}, },
server: { 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, port: process.env.NODE_ENV === 'production' ? 3000 : 4000,
launchTimeout: 180000, launchTimeout: 180000,
}, },

View file

@ -30,12 +30,13 @@
"test:e2e": "jest", "test:e2e": "jest",
"start:development": "npx webpack serve --mode=development", "start:development": "npx webpack serve --mode=development",
"start:production": "NODE_ENV=production PUBLIC_URL=http://localhost:3000/dist/ npm run build && npx serve", "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", "start": "npm run start:development",
"build": "NODE_ENV=production webpack --mode=production", "build": "NODE_ENV=production webpack --mode=production",
"build:libs": "node build-libs.js build", "build:libs": "LIBS_BUILD_MODE=all webpack --config webpack.libs.config.js",
"build:libs:clean": "node build-libs.js clean", "build:libs:clean": "LIBS_BUILD_MODE=clean webpack --config webpack.libs.config.js",
"build:libs:wasm": "node build-libs.js wasm", "build:libs:wasm": "LIBS_BUILD_MODE=wasm webpack --config webpack.libs.config.js",
"build:libs:fonts": "node build-libs.js fonts", "build:libs:fonts": "LIBS_BUILD_MODE=fonts webpack --config webpack.libs.config.js",
"build:all": "npm run build:libs && npm run build" "build:all": "npm run build:libs && npm run build"
}, },
"eslintConfig": { "eslintConfig": {

150
build-libs.js → webpack-libs-plugin.js Executable file → Normal file
View file

@ -1,32 +1,63 @@
#!/usr/bin/env node #!/usr/bin/env node
import fs from 'fs/promises'; import { exec } from 'node:child_process';
import { createWriteStream, existsSync } from 'fs'; import { createWriteStream, existsSync } from 'node:fs';
import path from 'path'; import fs from 'node:fs/promises';
import { exec } from 'child_process'; import https from 'node:https';
import { promisify } from 'util'; import path from 'node:path';
import https from 'https'; import { pipeline } from 'node:stream/promises';
import { pipeline } from 'stream/promises'; import { promisify } from 'node:util';
const execAsync = promisify(exec); const execAsync = promisify(exec);
const CONFIG_FILE = 'libs-config.json'; class OpenSCADLibrariesPlugin {
const LIBS_DIR = 'libs'; constructor(options = {}) {
const PUBLIC_LIBS_DIR = 'public/libraries'; this.configFile = options.configFile || 'libs-config.json';
const SRC_WASM_DIR = 'src/wasm'; this.libsDir = options.libsDir || 'libs';
this.publicLibsDir = options.publicLibsDir || 'public/libraries';
class LibsBuilder { this.srcWasmDir = options.srcWasmDir || 'src/wasm';
constructor() { this.buildMode = options.buildMode || 'all'; // 'all', 'wasm', 'fonts', 'libs'
this.config = null; 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() { async loadConfig() {
try { try {
const configContent = await fs.readFile(CONFIG_FILE, 'utf-8'); const configContent = await fs.readFile(this.configFile, 'utf-8');
this.config = JSON.parse(configContent); this.config = JSON.parse(configContent);
} catch (error) { } catch (error) {
console.error(`Failed to load config from ${CONFIG_FILE}:`, error.message); throw new Error(`Failed to load config from ${this.configFile}: ${error.message}`);
process.exit(1);
} }
} }
@ -46,7 +77,6 @@ class LibsBuilder {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
https.get(url, (response) => { https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) { if (response.statusCode === 302 || response.statusCode === 301) {
// Handle redirects
return this.downloadFile(response.headers.location, outputPath) return this.downloadFile(response.headers.location, outputPath)
.then(resolve) .then(resolve)
.catch(reject); .catch(reject);
@ -95,23 +125,18 @@ class LibsBuilder {
if (includes.length > 0) { if (includes.length > 0) {
const findPatterns = includes.map(pattern => { const findPatterns = includes.map(pattern => {
if (pattern.includes('**/*.')) { if (pattern.includes('**/*.')) {
// Pattern like "examples/**/*.scad"
const parts = pattern.split('/'); const parts = pattern.split('/');
const dir = parts[0]; const dir = parts[0];
const filePattern = parts[parts.length - 1]; const filePattern = parts[parts.length - 1];
return `-path "./${dir}/*" -name "${filePattern}"`; return `-path "./${dir}/*" -name "${filePattern}"`;
} else if (pattern.includes('**')) { } else if (pattern.includes('**')) {
// Pattern like "**/*.scad"
const filePattern = pattern.replace('**/', ''); const filePattern = pattern.replace('**/', '');
return `-name "${filePattern}"`; return `-name "${filePattern}"`;
} else if (pattern.includes('*')) { } else if (pattern.includes('*')) {
// Pattern like "*.scad"
return `-name "${pattern}"`; return `-name "${pattern}"`;
} else if (pattern.includes('/')) { } else if (pattern.includes('/')) {
// Path pattern like "bitmap/*.scad"
return `-path "./${pattern}"`; return `-path "./${pattern}"`;
} else { } else {
// Direct file/directory name
return `-name "${pattern}" -o -path "./${pattern}/*"`; return `-name "${pattern}" -o -path "./${pattern}/*"`;
} }
}).join(' -o '); }).join(' -o ');
@ -132,7 +157,6 @@ class LibsBuilder {
const zipCmd = `cd ${fullSourceDir} && ${findCmd} | zip -r ${path.resolve(outputPath)} -@`; const zipCmd = `cd ${fullSourceDir} && ${findCmd} | zip -r ${path.resolve(outputPath)} -@`;
console.log(`Creating zip: ${outputPath}`); console.log(`Creating zip: ${outputPath}`);
console.log(`Zip command: ${zipCmd}`);
try { try {
await execAsync(zipCmd); await execAsync(zipCmd);
} catch (error) { } catch (error) {
@ -146,22 +170,16 @@ class LibsBuilder {
const wasmDir = wasmBuild.target; const wasmDir = wasmBuild.target;
const wasmZip = `${wasmDir}.zip`; const wasmZip = `${wasmDir}.zip`;
// Create libs directory await this.ensureDir(this.libsDir);
await this.ensureDir(LIBS_DIR);
// Download WASM if not exists
if (!existsSync(wasmDir)) { if (!existsSync(wasmDir)) {
await this.ensureDir(wasmDir); await this.ensureDir(wasmDir);
// Download WASM zip
await this.downloadFile(wasmBuild.url, wasmZip); await this.downloadFile(wasmBuild.url, wasmZip);
// Extract WASM zip
console.log(`Extracting WASM to ${wasmDir}`); console.log(`Extracting WASM to ${wasmDir}`);
await execAsync(`cd ${wasmDir} && unzip ../${path.basename(wasmZip)}`); await execAsync(`cd ${wasmDir} && unzip ../${path.basename(wasmZip)}`);
} }
// Create symlinks for public files
await this.ensureDir('public'); await this.ensureDir('public');
const jsTarget = 'public/openscad.js'; const jsTarget = 'public/openscad.js';
@ -170,34 +188,28 @@ class LibsBuilder {
// Remove existing symlinks/files // Remove existing symlinks/files
try { try {
await fs.unlink(jsTarget); await fs.unlink(jsTarget);
} catch { } catch { /* ignore */ }
// ignore - file doesn't exist
}
try { try {
await fs.unlink(wasmTarget); await fs.unlink(wasmTarget);
} catch { } catch { /* ignore */ }
// ignore - file doesn't exist
}
// 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.js')), jsTarget);
await fs.symlink(path.relative('public', path.join(wasmDir, 'openscad.wasm')), wasmTarget); await fs.symlink(path.relative('public', path.join(wasmDir, 'openscad.wasm')), wasmTarget);
// Create src/wasm symlink // Create src/wasm symlink
try { try {
await fs.unlink(SRC_WASM_DIR); await fs.unlink(this.srcWasmDir);
} catch { } catch { /* ignore */ }
// ignore - file doesn't exist await fs.symlink(path.relative('src', wasmDir), this.srcWasmDir);
}
await fs.symlink(path.relative('src', wasmDir), SRC_WASM_DIR);
console.log('WASM setup completed'); console.log('WASM setup completed');
} }
async buildFonts() { async buildFonts() {
const { fonts } = this.config; const { fonts } = this.config;
const notoDir = path.join(LIBS_DIR, 'noto'); const notoDir = path.join(this.libsDir, 'noto');
const liberationDir = path.join(LIBS_DIR, 'liberation'); const liberationDir = path.join(this.libsDir, 'liberation');
await this.ensureDir(notoDir); await this.ensureDir(notoDir);
@ -216,8 +228,8 @@ class LibsBuilder {
} }
// Create fonts zip // Create fonts zip
const fontsZip = path.join(PUBLIC_LIBS_DIR, 'fonts.zip'); const fontsZip = path.join(this.publicLibsDir, 'fonts.zip');
await this.ensureDir(PUBLIC_LIBS_DIR); await this.ensureDir(this.publicLibsDir);
console.log('Creating fonts.zip'); 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`; 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) { async buildLibrary(library) {
const libDir = path.join(LIBS_DIR, library.name); const libDir = path.join(this.libsDir, library.name);
const zipPath = path.join(PUBLIC_LIBS_DIR, `${library.name}.zip`); const zipPath = path.join(this.publicLibsDir, `${library.name}.zip`);
// Clone repository if not exists // Clone repository if not exists
if (!existsSync(libDir)) { if (!existsSync(libDir)) {
@ -248,7 +260,7 @@ class LibsBuilder {
} }
async buildAllLibraries() { async buildAllLibraries() {
await this.ensureDir(PUBLIC_LIBS_DIR); await this.ensureDir(this.publicLibsDir);
for (const library of this.config.libraries) { for (const library of this.config.libraries) {
await this.buildLibrary(library); await this.buildLibrary(library);
@ -259,12 +271,12 @@ class LibsBuilder {
console.log('Cleaning build artifacts...'); console.log('Cleaning build artifacts...');
const cleanPaths = [ const cleanPaths = [
LIBS_DIR, this.libsDir,
'build', 'build',
'public/openscad.js', 'public/openscad.js',
'public/openscad.wasm', 'public/openscad.wasm',
`${PUBLIC_LIBS_DIR}/*.zip`, `${this.publicLibsDir}/*.zip`,
SRC_WASM_DIR this.srcWasmDir
]; ];
for (const cleanPath of cleanPaths) { for (const cleanPath of cleanPaths) {
@ -282,7 +294,7 @@ class LibsBuilder {
console.log('Clean completed'); console.log('Clean completed');
} }
async build() { async buildAll() {
console.log('Building all libraries...'); console.log('Building all libraries...');
await this.buildWasm(); await this.buildWasm();
@ -293,32 +305,4 @@ class LibsBuilder {
} }
} }
async function main() { export default OpenSCADLibrariesPlugin;
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);
});

View file

@ -1,16 +1,13 @@
import CopyPlugin from 'copy-webpack-plugin'; import CopyPlugin from 'copy-webpack-plugin';
import WorkboxPlugin from 'workbox-webpack-plugin';
import webpack from 'webpack'; import webpack from 'webpack';
import packageConfig from './package.json' with {type: 'json'}; import WorkboxPlugin from 'workbox-webpack-plugin';
import path, {dirname} from 'path'; import path, { dirname } from 'path';
import {fileURLToPath} from 'url'; import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); 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'; const isDev = process.env.NODE_ENV !== 'production';
@ -48,7 +45,7 @@ const config = [
'style-loader', 'style-loader',
{ {
loader: 'css-loader', loader: 'css-loader',
options:{url: false}, options: { url: false },
} }
] ]
}, },
@ -88,7 +85,7 @@ const config = [
clientsClaim: true, clientsClaim: true,
skipWaiting: true, skipWaiting: true,
runtimeCaching: [{ runtimeCaching: [{
urlPattern: ({request, url}) => true, urlPattern: ({ request, url }) => true,
handler: 'StaleWhileRevalidate', handler: 'StaleWhileRevalidate',
options: { options: {
cacheName: 'all', cacheName: 'all',
@ -113,7 +110,11 @@ const config = [
}, },
{ {
from: path.resolve(__dirname, 'src/wasm/openscad.js'), from: path.resolve(__dirname, 'src/wasm/openscad.js'),
to: path.resolve(__dirname, 'dist'),
},
{
from: path.resolve(__dirname, 'src/wasm/openscad.wasm'), from: path.resolve(__dirname, 'src/wasm/openscad.wasm'),
to: path.resolve(__dirname, 'dist'),
}, },
], ],
}), }),

21
webpack.libs.config.js Normal file
View file

@ -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;