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:
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:

View file

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

View file

@ -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,
},

View file

@ -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": {

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

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

View file

@ -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'),
},
],
}),

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;