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/Makefile b/Makefile deleted file mode 100644 index fcdc3ac..0000000 --- a/Makefile +++ /dev/null @@ -1,285 +0,0 @@ -# Pin WASM build to a version known to work -WASM_BUILD_URL=https://files.openscad.org/playground/OpenSCAD-2025.03.25.wasm24456-WebAssembly-web.zip -# WASM_SNAPSHOT_JS_URL=https://files.openscad.org/snapshots/.snapshot_wasm.js -# WASM_BUILD_URL=$(shell curl ${WASM_SNAPSHOT_JS_URL} 2>/dev/null | grep https | sed -E "s/.*(https:[^']+)'.*/\1/" ) - -SINGLE_BRANCH_MAIN=--branch main --single-branch -SINGLE_BRANCH=--branch master --single-branch -SHALLOW=--depth 1 - -SHELL:=/usr/bin/env bash -WASM_BUILD=Release - -all: public - -.PHONY: public wasm -public: \ - src/wasm \ - public/openscad.js \ - public/openscad.wasm \ - public/libraries/fonts.zip \ - public/libraries/openscad.zip \ - public/libraries/NopSCADlib.zip \ - public/libraries/BOSL.zip \ - public/libraries/BOSL2.zip \ - public/libraries/boltsparts.zip \ - public/libraries/OpenSCAD-Snippet.zip \ - public/libraries/funcutils.zip \ - public/libraries/FunctionalOpenSCAD.zip \ - public/libraries/YAPP_Box.zip \ - public/libraries/MCAD.zip \ - public/libraries/smooth-prim.zip \ - public/libraries/plot-function.zip \ - public/libraries/openscad-tray.zip \ - public/libraries/closepoints.zip \ - public/libraries/Stemfie_OpenSCAD.zip \ - public/libraries/pathbuilder.zip \ - public/libraries/openscad_attachable_text3d.zip \ - public/libraries/brailleSCAD.zip \ - public/libraries/UB.scad.zip \ - public/libraries/lasercut.zip - -clean: - rm -fR libs build - rm -fR public/openscad.{js,wasm} - rm -fR public/libraries/*.zip - rm -fR src/wasm - -dist/index.js: public - npm run build - -dist/openscad-worker.js: src/openscad-worker.ts src/wasm/openscad.js - npx rollup -c - -src/wasm: libs/openscad-wasm - rm -f src/wasm - ln -sf "$(shell pwd)/libs/openscad-wasm" src/wasm - -wasm: libs/openscad - ( cd libs/openscad && ./scripts/wasm-base-docker-run.sh emcmake cmake -B build -DCMAKE_BUILD_TYPE=$(WASM_BUILD) -DEXPERIMENTAL=1 ) - ( cd libs/openscad && ./scripts/wasm-base-docker-run.sh /bin/bash -c "cmake --build build -j || cmake --build build -j2 || cmake --build build" ) - mkdir -p libs/openscad-wasm - cp libs/openscad/build/openscad.* libs/openscad-wasm/ - -libs/openscad-wasm: - mkdir -p libs/openscad-wasm - wget ${WASM_BUILD_URL} -O libs/openscad-wasm.zip - ( cd libs/openscad-wasm && unzip ../openscad-wasm.zip ) - -public/openscad.js: libs/openscad-wasm libs/openscad-wasm/openscad.js - ln -sf libs/openscad-wasm/openscad.js public/openscad.js - -public/openscad.wasm: libs/openscad-wasm libs/openscad-wasm/openscad.wasm - ln -sf libs/openscad-wasm/openscad.wasm public/openscad.wasm - -# Var w/ noto fonts -NOTO_FONTS=\ - libs/noto/NotoNaskhArabic-Bold.ttf \ - libs/noto/NotoNaskhArabic-Regular.ttf \ - libs/noto/NotoSans-Bold.ttf \ - libs/noto/NotoSans-Italic.ttf \ - libs/noto/NotoSans-Regular.ttf \ - libs/noto/NotoSansArmenian-Bold.ttf \ - libs/noto/NotoSansArmenian-Regular.ttf \ - libs/noto/NotoSansBalinese-Regular.ttf \ - libs/noto/NotoSansBengali-Bold.ttf \ - libs/noto/NotoSansBengali-Regular.ttf \ - libs/noto/NotoSansDevanagari-Bold.ttf \ - libs/noto/NotoSansDevanagari-Regular.ttf \ - libs/noto/NotoSansEthiopic-Bold.ttf \ - libs/noto/NotoSansEthiopic-Regular.ttf \ - libs/noto/NotoSansGeorgian-Bold.ttf \ - libs/noto/NotoSansGeorgian-Regular.ttf \ - libs/noto/NotoSansGujarati-Bold.ttf \ - libs/noto/NotoSansGujarati-Regular.ttf \ - libs/noto/NotoSansGurmukhi-Bold.ttf \ - libs/noto/NotoSansGurmukhi-Regular.ttf \ - libs/noto/NotoSansHebrew-Bold.ttf \ - libs/noto/NotoSansHebrew-Regular.ttf \ - libs/noto/NotoSansJavanese-Regular.ttf \ - libs/noto/NotoSansKannada-Bold.ttf \ - libs/noto/NotoSansKannada-Regular.ttf \ - libs/noto/NotoSansKhmer-Bold.ttf \ - libs/noto/NotoSansKhmer-Regular.ttf \ - libs/noto/NotoSansLao-Bold.ttf \ - libs/noto/NotoSansLao-Regular.ttf \ - libs/noto/NotoSansMongolian-Regular.ttf \ - libs/noto/NotoSansMyanmar-Bold.ttf \ - libs/noto/NotoSansMyanmar-Regular.ttf \ - libs/noto/NotoSansOriya-Bold.ttf \ - libs/noto/NotoSansOriya-Regular.ttf \ - libs/noto/NotoSansSinhala-Bold.ttf \ - libs/noto/NotoSansSinhala-Regular.ttf \ - libs/noto/NotoSansTamil-Bold.ttf \ - libs/noto/NotoSansTamil-Regular.ttf \ - libs/noto/NotoSansThai-Bold.ttf \ - libs/noto/NotoSansThai-Regular.ttf \ - libs/noto/NotoSansTibetan-Bold.ttf \ - libs/noto/NotoSansTibetan-Regular.ttf \ - libs/noto/NotoSansTifinagh-Regular.ttf \ - -# Way too big for now, also can't make them work yet: -# libs/noto/NotoSansCJKtc-Bold.otf -# libs/noto/NotoSansCJKtc-Regular.otf - -public/libraries/fonts.zip: $(NOTO_FONTS) libs/liberation - mkdir -p public/libraries - zip -r $@ -j fonts.conf libs/noto/*.ttf libs/liberation/{*.ttf,LICENSE,AUTHORS} - -libs/noto/%.ttf: - mkdir -p libs/noto - wget https://github.com/openmaptiles/fonts/raw/master/noto-sans/$(notdir $@) -O $@ - -libs/noto/%.otf: - mkdir -p libs/noto - wget https://github.com/openmaptiles/fonts/raw/master/noto-sans/$(notdir $@) -O $@ - -libs/liberation: - git clone --recurse https://github.com/shantigilbert/liberation-fonts-ttf.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -libs/openscad: - git clone --recurse https://github.com/openscad/openscad.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/openscad.zip: libs/openscad - mkdir -p public/libraries - ( cd libs/openscad ; zip -r - `find examples -name '*.scad' | grep -v tests` ) > public/libraries/openscad.zip - -libs/BOSL2: - git clone --recurse https://github.com/BelfrySCAD/BOSL2.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/BOSL2.zip: libs/BOSL2 - mkdir -p public/libraries - ( cd libs/BOSL2 ; zip -r ../../public/libraries/BOSL2.zip *.scad LICENSE examples ) - -libs/BOSL: - git clone --recurse https://github.com/revarbat/BOSL.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/BOSL.zip: libs/BOSL - mkdir -p public/libraries - ( cd libs/BOSL ; zip -r ../../public/libraries/BOSL.zip *.scad LICENSE ) - -libs/NopSCADlib: - git clone --recurse https://github.com/nophead/NopSCADlib.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/NopSCADlib.zip: libs/NopSCADlib - mkdir -p public/libraries - ( cd libs/NopSCADlib ; zip -r ../../public/libraries/NopSCADlib.zip `find . -name '*.scad'` COPYING ) - -libs/funcutils: - git clone --recurse https://github.com/thehans/funcutils.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/funcutils.zip: libs/funcutils - mkdir -p public/libraries - ( cd libs/funcutils ; zip -r ../../public/libraries/funcutils.zip *.scad LICENSE ) - -libs/FunctionalOpenSCAD: - git clone --recurse https://github.com/thehans/FunctionalOpenSCAD.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/FunctionalOpenSCAD.zip: libs/FunctionalOpenSCAD - mkdir -p public/libraries - ( cd libs/FunctionalOpenSCAD ; zip -r ../../public/libraries/FunctionalOpenSCAD.zip *.scad LICENSE ) - -libs/YAPP_Box: - git clone --recurse https://github.com/mrWheel/YAPP_Box.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ - -public/libraries/YAPP_Box.zip: libs/YAPP_Box - mkdir -p public/libraries - ( cd libs/YAPP_Box ; zip -r ../../public/libraries/YAPP_Box.zip `find . -name '*.scad'` LICENSE ) - -libs/MCAD: - git clone --recurse https://github.com/openscad/MCAD.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/MCAD.zip: libs/MCAD - mkdir -p public/libraries - ( cd libs/MCAD ; zip -r ../../public/libraries/MCAD.zip *.scad bitmap/*.scad LICENSE ) - -libs/boltsparts: - git clone --recurse https://github.com/boltsparts/boltsparts.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ - -public/libraries/boltsparts.zip: libs/boltsparts - mkdir -p public/libraries - ( cd libs/boltsparts/openscad ; zip -r ../../../public/libraries/boltsparts.zip `find . -name '*.scad' | grep -v tests` ../LICENSE ) - -libs/OpenSCAD-Snippet: - git clone --recurse https://github.com/AngeloNicoli/OpenSCAD-Snippet.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ - -public/libraries/OpenSCAD-Snippet.zip: libs/OpenSCAD-Snippet - mkdir -p public/libraries - ( cd libs/OpenSCAD-Snippet ; zip -r ../../public/libraries/OpenSCAD-Snippet.zip `find . -name '*.scad'` LICENSE ) - -libs/Stemfie_OpenSCAD: - git clone --recurse https://github.com/Cantareus/Stemfie_OpenSCAD.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ - -public/libraries/Stemfie_OpenSCAD.zip: libs/Stemfie_OpenSCAD - mkdir -p public/libraries - ( cd libs/Stemfie_OpenSCAD ; zip -r ../../public/libraries/Stemfie_OpenSCAD.zip *.scad LICENSE ) - -libs/pathbuilder: - git clone --recurse https://github.com/dinther/pathbuilder.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ - -public/libraries/pathbuilder.zip: libs/pathbuilder - mkdir -p public/libraries - ( cd libs/pathbuilder ; zip -r ../../public/libraries/pathbuilder.zip *.scad demo/*.scad LICENSE ) - -libs/openscad_attachable_text3d: - git clone --recurse https://github.com/jon-gilbert/openscad_attachable_text3d.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ - -public/libraries/openscad_attachable_text3d.zip: libs/openscad_attachable_text3d - mkdir -p public/libraries - ( cd libs/openscad_attachable_text3d ; zip -r ../../public/libraries/openscad_attachable_text3d.zip *.scad LICENSE ) - -libs/brailleSCAD: - git clone --recurse https://github.com/BelfrySCAD/brailleSCAD.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ - -public/libraries/brailleSCAD.zip: libs/brailleSCAD - mkdir -p public/libraries - ( cd libs/brailleSCAD ; zip -r ../../public/libraries/brailleSCAD.zip *.scad LICENSE ) - -# libs/threads: -# git clone --recurse https://github.com/rcolyer/threads.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -# public/libraries/threads.zip: libs/threads -# mkdir -p public/libraries -# ( cd libs/threads ; zip -r ../../public/libraries/threads.zip *.scad LICENSE.txt ) - -libs/smooth-prim: - git clone --recurse https://github.com/rcolyer/smooth-prim.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/smooth-prim.zip: libs/smooth-prim - mkdir -p public/libraries - ( cd libs/smooth-prim ; zip -r ../../public/libraries/smooth-prim.zip *.scad LICENSE.txt ) - -libs/plot-function: - git clone --recurse https://github.com/rcolyer/plot-function.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/plot-function.zip: libs/plot-function - mkdir -p public/libraries - ( cd libs/plot-function ; zip -r ../../public/libraries/plot-function.zip *.scad LICENSE.txt ) - -libs/closepoints: - git clone --recurse https://github.com/rcolyer/closepoints.git ${SHALLOW} ${SINGLE_BRANCH} $@ - -public/libraries/closepoints.zip: libs/closepoints - mkdir -p public/libraries - ( cd libs/closepoints ; zip -r ../../public/libraries/closepoints.zip *.scad LICENSE.txt ) - -libs/UB.scad: - git clone --recurse https://github.com/UBaer21/UB.scad.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ - -public/libraries/UB.scad.zip: libs/UB.scad - mkdir -p public/libraries - ( cd libs/UB.scad ; zip -r ../../public/libraries/UB.scad.zip libraries/*.scad LICENSE examples/UBexamples ) - -libs/openscad-tray: - git clone --recurse https://github.com/sofian/openscad-tray.git ${SHALLOW} ${SINGLE_BRANCH_MAIN} $@ - -public/libraries/openscad-tray.zip: libs/openscad-tray - mkdir -p public/libraries - ( cd libs/openscad-tray ; zip -r ../../public/libraries/openscad-tray.zip *.scad LICENSE ) - -libs/lasercut: - git clone --recurse https://github.com/bmsleight/lasercut.git ${SHALLOW} ${SINGLE_BRANCH} $@ -public/libraries/lasercut.zip: libs/lasercut - mkdir -p public/libraries - ( cd libs/lasercut ; zip -r ../../public/libraries/lasercut.zip *.scad LICENSE ) - diff --git a/README.md b/README.md index bbc30e0..6b1768d 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Licenses: see [LICENSES](./LICENSE). - Animation rendering (And other formats than STL) - [x] Compress URL fragment - [x] Mobile (iOS) editing support: switch to https://www.npmjs.com/package/react-codemirror ? -- [ ] Replace Makefile w/ something that reads the libs metadata +- [x] Replace Makefile w/ something that reads the libs metadata - [ ] Merge modifiers rendering code to openscad - Model /home fs in shared state. have two clear paths: /libraries for builtins, and /home for user data. State pointing to /libraries paths needs not store the data except if there's overrides (flagged as modifications in the file picker) - Drag and drop of files (SCAD, STL, etc) and Zip archives. For assets, auto insert the corresponding import. @@ -48,11 +48,15 @@ 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 -* GNU make +* wget or curl +* Node.js (>=18.12.0) * npm -* Docker able to run amd64 containers. If running on a different platform (including Silicon Mac), you can add support for amd64 images through QEMU with: +* git +* zip +* Docker able to run amd64 containers (only needed if building WASM from source). If running on a different platform (including Silicon Mac), you can add support for amd64 images through QEMU with: ```bash docker run --privileged --rm tonistiigi/binfmt --install all @@ -61,27 +65,26 @@ Prerequisites: Local dev: ```bash -make public +npm run build:libs # Download WASM and build all OpenSCAD libraries npm install -npm start +npm run start # http://localhost:4000/ ``` Local prod (test both the different inlining and serving under a prefix): ```bash -make public +npm run build:libs # Download WASM and build all OpenSCAD libraries npm install -npm run start:prod +npm run start:production # http://localhost:3000/dist/ ``` Deployment (edit "homepage" in `package.json` to match your deployment root!): ```bash -make public +npm run build:all # Build libraries and compile the application npm install -NODE_ENV=production npm run build rm -fR ../ochafik.github.io/openscad2 && cp -R dist ../ochafik.github.io/openscad2 # Now commit and push changes, wait for site update and enjoy! @@ -89,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: @@ -104,24 +107,45 @@ rm -fR ../ochafik.github.io/openscad2 && cp -R dist ../ochafik.github.io/opensca - Build WASM binary (add `WASM_BUILD=Debug` argument if you'd like to debug any cryptic crashes): ```bash - make wasm + npm run build:libs:wasm ``` - Then continue the build: ```bash - make public - npm start + npm run build:libs + npm run start ``` ## 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): -- [Makefile](./Makefile): to pull the library's code (optionally alias some files for easier imports) and package it as a `.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 - [src/fs/zip-archives.ts](./src/fs/zip-archives.ts): to use the `.zip` archive in the UI (both for file explorer and automatic imports mounting) - [LICENSE.md](./LICENSE.md): most libraries require proper disclosure of their usage and of their license. If a license is unique, paste it in full, otherwise, link to one of the standard ones already there. +### Library Configuration Format + +In `libs-config.json`, add an entry like this: + +```json +{ + "name": "LibraryName", + "repo": "https://github.com/user/repo.git", + "branch": "main", + "zipIncludes": ["*.scad", "LICENSE", "examples"], + "zipExcludes": ["**/tests/**"], + "workingDir": "." +} +``` + +Available build commands: +- `npm run build:libs` - Build all libraries +- `npm run build:libs:clean` - Clean all build artifacts +- `npm run build:libs:wasm` - Download/build just the WASM binary +- `npm run build:libs:fonts` - Download/build just the fonts + Send us a PR, then once it's merged request an update to the hosted https://ochafik.com/openscad2 demo. 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/libs-config.json b/libs-config.json new file mode 100644 index 0000000..6b44cd3 --- /dev/null +++ b/libs-config.json @@ -0,0 +1,248 @@ +{ + "wasmBuild": { + "url": "https://files.openscad.org/playground/OpenSCAD-2025.03.25.wasm24456-WebAssembly-web.zip", + "target": "libs/openscad-wasm" + }, + "libraries": [ + { + "name": "openscad", + "repo": "https://github.com/openscad/openscad.git", + "branch": "master", + "zipIncludes": [ + "examples/**/*.scad" + ], + "zipExcludes": [ + "**/tests/**" + ] + }, + { + "name": "BOSL2", + "repo": "https://github.com/BelfrySCAD/BOSL2.git", + "branch": "master", + "zipIncludes": [ + "*.scad", + "LICENSE", + "examples" + ] + }, + { + "name": "BOSL", + "repo": "https://github.com/revarbat/BOSL.git", + "branch": "master", + "zipIncludes": [ + "*.scad", + "LICENSE" + ] + }, + { + "name": "NopSCADlib", + "repo": "https://github.com/nophead/NopSCADlib.git", + "branch": "master", + "zipIncludes": [ + "**/*.scad", + "COPYING" + ] + }, + { + "name": "funcutils", + "repo": "https://github.com/thehans/funcutils.git", + "branch": "master", + "zipIncludes": [ + "*.scad", + "LICENSE" + ] + }, + { + "name": "FunctionalOpenSCAD", + "repo": "https://github.com/thehans/FunctionalOpenSCAD.git", + "branch": "master", + "zipIncludes": [ + "*.scad", + "LICENSE" + ] + }, + { + "name": "YAPP_Box", + "repo": "https://github.com/mrWheel/YAPP_Box.git", + "branch": "main", + "zipIncludes": [ + "**/*.scad", + "LICENSE" + ] + }, + { + "name": "MCAD", + "repo": "https://github.com/openscad/MCAD.git", + "branch": "master", + "zipIncludes": [ + "*.scad", + "bitmap/*.scad", + "LICENSE" + ] + }, + { + "name": "boltsparts", + "repo": "https://github.com/boltsparts/boltsparts.git", + "branch": "main", + "zipIncludes": [ + "**/*.scad", + "../LICENSE" + ], + "zipExcludes": [ + "**/tests/**" + ], + "workingDir": "openscad" + }, + { + "name": "OpenSCAD-Snippet", + "repo": "https://github.com/AngeloNicoli/OpenSCAD-Snippet.git", + "branch": "main", + "zipIncludes": [ + "**/*.scad", + "LICENSE" + ] + }, + { + "name": "Stemfie_OpenSCAD", + "repo": "https://github.com/Cantareus/Stemfie_OpenSCAD.git", + "branch": "main", + "zipIncludes": [ + "*.scad", + "LICENSE" + ] + }, + { + "name": "pathbuilder", + "repo": "https://github.com/dinther/pathbuilder.git", + "branch": "main", + "zipIncludes": [ + "*.scad", + "demo/*.scad", + "LICENSE" + ] + }, + { + "name": "openscad_attachable_text3d", + "repo": "https://github.com/jon-gilbert/openscad_attachable_text3d.git", + "branch": "main", + "zipIncludes": [ + "*.scad", + "LICENSE" + ] + }, + { + "name": "brailleSCAD", + "repo": "https://github.com/BelfrySCAD/brailleSCAD.git", + "branch": "main", + "zipIncludes": [ + "*.scad", + "LICENSE" + ] + }, + { + "name": "smooth-prim", + "repo": "https://github.com/rcolyer/smooth-prim.git", + "branch": "master", + "zipIncludes": [ + "*.scad", + "LICENSE.txt" + ] + }, + { + "name": "plot-function", + "repo": "https://github.com/rcolyer/plot-function.git", + "branch": "master", + "zipIncludes": [ + "*.scad", + "LICENSE.txt" + ] + }, + { + "name": "closepoints", + "repo": "https://github.com/rcolyer/closepoints.git", + "branch": "master", + "zipIncludes": [ + "*.scad", + "LICENSE.txt" + ] + }, + { + "name": "UB.scad", + "repo": "https://github.com/UBaer21/UB.scad.git", + "branch": "main", + "zipIncludes": [ + "libraries/*.scad", + "LICENSE", + "examples/UBexamples" + ] + }, + { + "name": "openscad-tray", + "repo": "https://github.com/sofian/openscad-tray.git", + "branch": "main", + "zipIncludes": [ + "*.scad", + "LICENSE" + ] + }, + { + "name": "lasercut", + "repo": "https://github.com/bmsleight/lasercut.git", + "branch": "master", + "zipIncludes": [ + "*.scad", + "LICENSE" + ] + } + ], + "fonts": { + "notoFonts": [ + "NotoNaskhArabic-Bold.ttf", + "NotoNaskhArabic-Regular.ttf", + "NotoSans-Bold.ttf", + "NotoSans-Italic.ttf", + "NotoSans-Regular.ttf", + "NotoSansArmenian-Bold.ttf", + "NotoSansArmenian-Regular.ttf", + "NotoSansBalinese-Regular.ttf", + "NotoSansBengali-Bold.ttf", + "NotoSansBengali-Regular.ttf", + "NotoSansDevanagari-Bold.ttf", + "NotoSansDevanagari-Regular.ttf", + "NotoSansEthiopic-Bold.ttf", + "NotoSansEthiopic-Regular.ttf", + "NotoSansGeorgian-Bold.ttf", + "NotoSansGeorgian-Regular.ttf", + "NotoSansGujarati-Bold.ttf", + "NotoSansGujarati-Regular.ttf", + "NotoSansGurmukhi-Bold.ttf", + "NotoSansGurmukhi-Regular.ttf", + "NotoSansHebrew-Bold.ttf", + "NotoSansHebrew-Regular.ttf", + "NotoSansJavanese-Regular.ttf", + "NotoSansKannada-Bold.ttf", + "NotoSansKannada-Regular.ttf", + "NotoSansKhmer-Bold.ttf", + "NotoSansKhmer-Regular.ttf", + "NotoSansLao-Bold.ttf", + "NotoSansLao-Regular.ttf", + "NotoSansMongolian-Regular.ttf", + "NotoSansMyanmar-Bold.ttf", + "NotoSansMyanmar-Regular.ttf", + "NotoSansOriya-Bold.ttf", + "NotoSansOriya-Regular.ttf", + "NotoSansSinhala-Bold.ttf", + "NotoSansSinhala-Regular.ttf", + "NotoSansTamil-Bold.ttf", + "NotoSansTamil-Regular.ttf", + "NotoSansThai-Bold.ttf", + "NotoSansThai-Regular.ttf", + "NotoSansTibetan-Bold.ttf", + "NotoSansTibetan-Regular.ttf", + "NotoSansTifinagh-Regular.ttf" + ], + "notoBaseUrl": "https://github.com/openmaptiles/fonts/raw/master/noto-sans/", + "liberationRepo": "https://github.com/shantigilbert/liberation-fonts-ttf.git", + "liberationBranch": "master" + } +} diff --git a/package.json b/package.json index 3f04aba..6a9b3bd 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,14 @@ "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": "NODE_ENV=production webpack --mode=production", + "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": { "extends": [ @@ -74,9 +80,10 @@ "style-loader": "^4.0.0", "ts-loader": "^9.5.1", "tslib": "^2.8.1", + "typescript": "^5.9.2", "webpack": "^5.97.1", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.0", "workbox-webpack-plugin": "^7.3.0" } -} +} \ No newline at end of file diff --git a/tests/e2e.test.js b/tests/e2e.test.js index 6ea7f50..bc883fb 100644 --- a/tests/e2e.test.js +++ b/tests/e2e.test.js @@ -22,24 +22,24 @@ beforeEach(async () => { afterEach(async () => { // console.log('Messages:', JSON.stringify(messages, null, 2)); const testName = expect.getState().currentTestName; - console.log(`[${testName}] Messages:`, JSON.stringify(messages.map(({text}) => text), null, 2)); - + console.log(`[${testName}] Messages:`, JSON.stringify(messages.map(({ text }) => text), null, 2)); + const errors = messages.filter(msg => - msg.type === 'error' && - !(msg.text.includes('404') - && msg.stack.some(s => - s.url.indexOf('fonts/InterVariable.woff') >= 0))); + msg.type === 'error' && + !(msg.text.includes('404') + && msg.stack.some(s => + s.url.indexOf('fonts/InterVariable.woff') >= 0))); expect(errors).toHaveLength(0); }); function loadSrc(src) { - return page.goto(baseUrl + '#src=' + encodeURIComponent(src)); + return page.goto(`${baseUrl}#src=${encodeURIComponent(src)}`); } function loadPath(path) { - return page.goto(baseUrl + '#path=' + encodeURIComponent(path)); + return page.goto(`${baseUrl}#path=${encodeURIComponent(path)}`); } function loadUrl(url) { - return page.goto(baseUrl + '#url=' + encodeURIComponent(url)); + return page.goto(`${baseUrl}#url=${encodeURIComponent(url)}`); } async function waitForViewer() { await page.waitForSelector('model-viewer'); @@ -63,19 +63,34 @@ function expect3DManifold() { } function waitForCustomizeButton() { return page.waitForFunction(() => { - const buttons = document.querySelectorAll('input[role=switch]'); - for (const button of buttons) { - if (button.parentElement.innerText === 'Customize') { - return button; + // Try multiple selectors for PrimeReact components + // ToggleButton might render as button or input elements + const selectors = [ + 'input[role=switch]', + 'button', + '[role=tab]', + '.p-togglebutton', + '.p-tabmenu-nav a' + ]; + + for (const selector of selectors) { + const elements = document.querySelectorAll(selector); + for (const element of elements) { + const text = element.textContent || element.innerText || ''; + const parentText = element.parentElement?.textContent || element.parentElement?.innerText || ''; + if (text.includes('Customize') || parentText.includes('Customize')) { + return element; + } } } - }); + return null; + }, { timeout: 45000 }); // Increase timeout to 45 seconds } function waitForLabel(text) { - return page.waitForFunction((text) => { + return page.waitForFunction(() => { return Array.from(document.querySelectorAll('label')).find(el => el.textContent === 'myVar'); // return Array.from(document.querySelectorAll('label')).find(el => el.textContent === text); - }, {}, text); + }); } describe('e2e', () => { @@ -90,7 +105,7 @@ describe('e2e', () => { await waitForViewer(); expect3DPolySet(); }, longTimeout); - + test('use BOSL2', async () => { await loadSrc(` include ; @@ -128,8 +143,17 @@ describe('e2e', () => { ].join('\r\n')); await waitForViewer(); expect3DPolySet(); + + // Wait for syntax checking to complete and parameters to be detected + await page.waitForFunction(() => { + // Look for any indication that parameters have been processed + const messages = Array.from(document.querySelectorAll('*')).map(el => el.textContent || '').join(' '); + return messages.includes('myVar') || messages.includes('Customize'); + }, { timeout: 30000 }); + await (await waitForCustomizeButton()).click(); await page.waitForSelector('fieldset'); await waitForLabel('myVar'); }, longTimeout); }); + diff --git a/webpack-libs-plugin.js b/webpack-libs-plugin.js new file mode 100644 index 0000000..273d6b6 --- /dev/null +++ b/webpack-libs-plugin.js @@ -0,0 +1,308 @@ +#!/usr/bin/env node + +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); + +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(this.configFile, 'utf-8'); + this.config = JSON.parse(configContent); + } catch (error) { + throw new Error(`Failed to load config from ${this.configFile}: ${error.message}`); + } + } + + async ensureDir(dirPath) { + try { + await fs.mkdir(dirPath, { recursive: true }); + } catch (error) { + if (error.code !== 'EEXIST') { + throw error; + } + } + } + + async downloadFile(url, outputPath) { + console.log(`Downloading ${url} to ${outputPath}`); + + return new Promise((resolve, reject) => { + https.get(url, (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + return this.downloadFile(response.headers.location, outputPath) + .then(resolve) + .catch(reject); + } + + if (response.statusCode !== 200) { + reject(new Error(`Failed to download: ${response.statusCode}`)); + return; + } + + const fileStream = createWriteStream(outputPath); + pipeline(response, fileStream) + .then(resolve) + .catch(reject); + }).on('error', reject); + }); + } + + async cloneRepo(repo, targetDir, branch = 'master', shallow = true) { + const cloneArgs = [ + 'clone', + '--recurse', + shallow ? '--depth 1' : '', + `--branch ${branch}`, + '--single-branch', + repo, + targetDir + ].filter(Boolean); + + console.log(`Cloning ${repo} to ${targetDir}`); + try { + await execAsync(`git ${cloneArgs.join(' ')}`); + } catch (error) { + console.error(`Failed to clone ${repo}:`, error.message); + throw error; + } + } + + async createZip(sourceDir, outputPath, includes = [], excludes = [], workingDir = '.') { + await this.ensureDir(path.dirname(outputPath)); + + const fullSourceDir = path.join(sourceDir, workingDir); + + // Build find command for includes + let findCmd = ''; + if (includes.length > 0) { + const findPatterns = includes.map(pattern => { + if (pattern.includes('**/*.')) { + const parts = pattern.split('/'); + const dir = parts[0]; + const filePattern = parts[parts.length - 1]; + return `-path "./${dir}/*" -name "${filePattern}"`; + } else if (pattern.includes('**')) { + const filePattern = pattern.replace('**/', ''); + return `-name "${filePattern}"`; + } else if (pattern.includes('*')) { + return `-name "${pattern}"`; + } else if (pattern.includes('/')) { + return `-path "./${pattern}"`; + } else { + return `-name "${pattern}" -o -path "./${pattern}/*"`; + } + }).join(' -o '); + findCmd = `find . \\( ${findPatterns} \\)`; + } else { + findCmd = 'find . -name "*.scad"'; + } + + // Add excludes + if (excludes.length > 0) { + const excludePatterns = excludes.map(pattern => { + const cleanPattern = pattern.replace('**/', '').replace('/**', ''); + return `-not -path "*/${cleanPattern}*"`; + }).join(' '); + findCmd += ` ${excludePatterns}`; + } + + const zipCmd = `cd ${fullSourceDir} && ${findCmd} | zip -r ${path.resolve(outputPath)} -@`; + + console.log(`Creating zip: ${outputPath}`); + try { + await execAsync(zipCmd); + } catch (error) { + console.error(`Failed to create zip ${outputPath}:`, error.message); + throw error; + } + } + + async buildWasm() { + const { wasmBuild } = this.config; + const wasmDir = wasmBuild.target; + const wasmZip = `${wasmDir}.zip`; + + await this.ensureDir(this.libsDir); + + if (!existsSync(wasmDir)) { + await this.ensureDir(wasmDir); + await this.downloadFile(wasmBuild.url, wasmZip); + + console.log(`Extracting WASM to ${wasmDir}`); + await execAsync(`cd ${wasmDir} && unzip ../${path.basename(wasmZip)}`); + } + + await this.ensureDir('public'); + + const jsTarget = 'public/openscad.js'; + const wasmTarget = 'public/openscad.wasm'; + + // Remove existing symlinks/files + try { + await fs.unlink(jsTarget); + } catch { /* ignore */ } + try { + await fs.unlink(wasmTarget); + } catch { /* ignore */ } + + // 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(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(this.libsDir, 'noto'); + const liberationDir = path.join(this.libsDir, 'liberation'); + + await this.ensureDir(notoDir); + + // Download Noto fonts + for (const font of fonts.notoFonts) { + const fontPath = path.join(notoDir, font); + if (!existsSync(fontPath)) { + const url = fonts.notoBaseUrl + font; + await this.downloadFile(url, fontPath); + } + } + + // Clone liberation fonts if not exists + if (!existsSync(liberationDir)) { + await this.cloneRepo(fonts.liberationRepo, liberationDir, fonts.liberationBranch); + } + + // Create fonts zip + 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`; + await execAsync(fontsCmd); + + console.log('Fonts setup completed'); + } + + async buildLibrary(library) { + 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)) { + await this.cloneRepo(library.repo, libDir, library.branch); + } + + // Create zip + await this.createZip( + libDir, + zipPath, + library.zipIncludes || ['*.scad'], + library.zipExcludes || [], + library.workingDir || '.' + ); + + console.log(`Built ${library.name}`); + } + + async buildAllLibraries() { + await this.ensureDir(this.publicLibsDir); + + for (const library of this.config.libraries) { + await this.buildLibrary(library); + } + } + + async clean() { + console.log('Cleaning build artifacts...'); + + const cleanPaths = [ + this.libsDir, + 'build', + 'public/openscad.js', + 'public/openscad.wasm', + `${this.publicLibsDir}/*.zip`, + this.srcWasmDir + ]; + + for (const cleanPath of cleanPaths) { + try { + if (cleanPath.includes('*')) { + await execAsync(`rm -f ${cleanPath}`); + } else { + await fs.rm(cleanPath, { recursive: true, force: true }); + } + } catch { + // Ignore errors for files that don't exist + } + } + + console.log('Clean completed'); + } + + async buildAll() { + console.log('Building all libraries...'); + + await this.buildWasm(); + await this.buildFonts(); + await this.buildAllLibraries(); + + console.log('Build completed successfully!'); + } +} + +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;