From aa9ebc59ce7f940d18fa5795fb97259b76075b7e Mon Sep 17 00:00:00 2001 From: Devin C Date: Tue, 3 Mar 2026 13:18:03 -0500 Subject: [PATCH 1/5] convert inline worker generation to a post-processing step --- .gitignore | 3 -- package.json | 9 ++-- scripts/README.md | 46 +++++++++++++++++++ scripts/build-workers.js | 27 ----------- scripts/post-process-workers.js | 79 +++++++++++++++++++++++++++++++++ src/lib/loaders/pcd/index.ts | 5 +-- 6 files changed, 130 insertions(+), 39 deletions(-) create mode 100644 scripts/README.md delete mode 100644 scripts/build-workers.js create mode 100644 scripts/post-process-workers.js diff --git a/.gitignore b/.gitignore index 0989c9b6d..5a53f74d8 100644 --- a/.gitignore +++ b/.gitignore @@ -34,9 +34,6 @@ vite.config.ts.timestamp-* # Sentry Config File .env.sentry-build-plugin -# Generated -*.inline.ts - # Protobuf src/lib/buf protos/vendor diff --git a/package.json b/package.json index 3e935e29b..53b523109 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,11 @@ "scripts": { "dev": "tsx server/check-bun && bun run server/server.ts", "build": "vite build && npm run prepack", - "build:workers": "node scripts/build-workers.js", - "prepack": "svelte-kit sync && pnpm run build:workers && svelte-package && publint", + "prepack": "svelte-kit sync && svelte-package && node scripts/post-process-workers.js && publint", "preview": "vite preview", - "prepare": "svelte-kit sync && pnpm run build:workers || echo ''", - "check": "svelte-kit sync && pnpm run build:workers && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && pnpm run build:workers && svelte-check --tsconfig ./tsconfig.json --watch", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", "lint": "prettier --check . && eslint .", "knip": "npx knip --include files,exports,types", diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..0a2313c53 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,46 @@ +# scripts/ + +Build and code generation scripts that run as part of the development workflow or publish pipeline. + +--- + +## `post-process-workers.js` + +**Run automatically as part of `pnpm run prepack`** (`svelte-package && node scripts/post-process-workers.js && publint`). + +### What it does + +After `svelte-package` copies `src/lib` into `dist/`, this script rewrites web worker references so the published package works correctly in consumer projects. + +It scans `dist/` for files containing `new Worker(new URL(..., import.meta.url))`, bundles each referenced worker file into a self-contained IIFE using esbuild (inlining all dependencies like Three.js PCDLoader), replaces the `new Worker(new URL(...))` call with a Blob URL equivalent, and deletes the now-unnecessary worker `.js` files from `dist/`. + +### Why we need it + +This is a workaround for a [known open Vite bug](https://github.com/vitejs/vite/issues/21422). When a library uses `new Worker(new URL('./worker.js', import.meta.url))`, a consumer project's Vite dep optimizer moves the library to `.vite/deps/` but the worker file doesn't follow -- causing a runtime error. + +`svelte-package` copies `.ts` files as-is (transpiled to JS), so there is no point in the library packaging pipeline where Vite's bundler runs and can resolve worker imports. The fix is to post-process `dist/` and inline the worker code so that consumers load it from a Blob URL with no file path dependency. + +In dev mode within this project, Vite handles `new Worker(new URL(...))` natively with full HMR support -- no post-processing is needed. + +--- + +## `model-pipeline.js` + +**Run manually via `pnpm run model-pipeline:run`** when new 3D models need to be added to the project. + +### What it does + +Converts `.glb` and `.gltf` 3D model files into typed Threlte/Svelte components using [`@threlte/gltf`](https://threlte.xyz/docs/reference/gltf/getting-started). + +Place model files in `static/models/`, then run the script. It: + +1. Finds all `.glb`/`.gltf` files in `static/models/` (skipping already-transformed files) +2. Runs `@threlte/gltf` on each file to generate a typed Svelte component +3. Moves the generated `.svelte` files to `src/lib/components/models/` +4. Cleans up the intermediate files from `static/models/` + +Configuration at the top of the file controls output options (TypeScript types, Draco compression, mesh simplification, etc.). By default `overwrite: false` -- existing components are not replaced. + +### Why we need it + +Hand-writing Three.js scene graphs for complex GLTF models is tedious and error-prone. `@threlte/gltf` generates Svelte components that exactly mirror the scene hierarchy of a model, including typed props for materials and geometry. This script automates the full pipeline from raw model file to usable Svelte component. diff --git a/scripts/build-workers.js b/scripts/build-workers.js deleted file mode 100644 index c37f7b351..000000000 --- a/scripts/build-workers.js +++ /dev/null @@ -1,27 +0,0 @@ -import { build } from 'esbuild' -import { writeFileSync } from 'fs' - -const workerPaths = ['src/lib/loaders/pcd/worker.ts'] - -for (const workerPath of workerPaths) { - const result = await build({ - entryPoints: [workerPath], - bundle: true, - format: 'iife', - write: false, - minify: true, - }) - - const code = result.outputFiles[0].text - const escaped = code.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$') - const outFile = `${workerPath.replace('.ts', '.inline.ts')}` - - writeFileSync( - outFile, - `// AUTO-GENERATED by scripts/build-workers.js - do not edit\nexport const workerCode = \`${escaped}\`\n` - ) - - console.log(`Built ${workerPath} into ${outFile}`) -} - -console.log('Built all workers') diff --git a/scripts/post-process-workers.js b/scripts/post-process-workers.js new file mode 100644 index 000000000..ba738d332 --- /dev/null +++ b/scripts/post-process-workers.js @@ -0,0 +1,79 @@ +/** + * Post-processes dist/ after svelte-package to inline web workers. + * + * svelte-package copies worker.ts -> worker.js into dist/ alongside the files + * that reference them. But dist/index.js files still contain + * `new Worker(new URL('./worker.js', import.meta.url))` which breaks when + * consumed by another Vite project's dep optimizer. + * + * This script: + * 1. Scans dist/ for JS files containing the `new Worker(new URL(...))` pattern + * 2. Bundles the referenced worker file with esbuild into a self-contained IIFE + * 3. Rewrites the worker instantiation to use a Blob URL instead + * 4. Deletes the now-unnecessary worker.js file from dist/ + */ + +import { build } from 'esbuild' +import { readFileSync, writeFileSync, unlinkSync, globSync } from 'fs' +import { resolve, dirname } from 'path' + +const WORKER_PATTERN = + /new Worker\(new URL\(['"]([^'"]+)['"]\s*,\s*import\.meta\.url\)\s*(?:,\s*\{[^}]*\})?\)/g + +const distFiles = globSync('dist/**/*.js') +const deleted = new Set() + +for (const file of distFiles) { + if (deleted.has(resolve(file))) continue + + const code = readFileSync(file, 'utf8') + const matches = [...code.matchAll(WORKER_PATTERN)] + + if (matches.length === 0) continue + + let result = code + + for (const match of matches) { + const [fullMatch, workerRelPath] = match + const workerAbsPath = resolve(dirname(file), workerRelPath) + + console.log(`Inlining worker ${workerRelPath} referenced in ${file}`) + + const bundle = await build({ + entryPoints: [workerAbsPath], + bundle: true, + format: 'iife', + write: false, + minify: true, + }) + + const bundledCode = bundle.outputFiles[0].text + const escaped = bundledCode + .replace(/\\/g, '\\\\') + .replace(/`/g, '\\`') + .replace(/\$/g, '\\$') + + const replacement = [ + `(function() {`, + ` const __workerCode = \`${escaped}\``, + ` const __blob = new Blob([__workerCode], { type: 'text/javascript' })`, + ` return new Worker(URL.createObjectURL(__blob))`, + `})()`, + ].join('\n') + + result = result.replace(fullMatch, replacement) + + try { + unlinkSync(workerAbsPath) + deleted.add(workerAbsPath) + console.log(`Deleted ${workerAbsPath}`) + } catch { + console.warn(`Could not delete ${workerAbsPath} -- may already be removed`) + } + } + + writeFileSync(file, result) + console.log(`Rewrote ${file}`) +} + +console.log('Post-processed all workers in dist/') diff --git a/src/lib/loaders/pcd/index.ts b/src/lib/loaders/pcd/index.ts index 36aaa02e8..6ac4c616b 100644 --- a/src/lib/loaders/pcd/index.ts +++ b/src/lib/loaders/pcd/index.ts @@ -1,9 +1,6 @@ -import { workerCode } from './worker.inline' import type { Message, SuccessMessage } from './messages' -const blob = new Blob([workerCode], { type: 'text/javascript' }) -const url = URL.createObjectURL(blob) -const worker = new Worker(url) +const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }) let requestId = 0 const pending = new Map< From 9767af37e2090a01e8538cfcbdde89d0752bc86b Mon Sep 17 00:00:00 2001 From: Devin C Date: Tue, 3 Mar 2026 14:15:23 -0500 Subject: [PATCH 2/5] lint --- scripts/post-process-workers.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/post-process-workers.js b/scripts/post-process-workers.js index ba738d332..44c4a825c 100644 --- a/scripts/post-process-workers.js +++ b/scripts/post-process-workers.js @@ -48,10 +48,7 @@ for (const file of distFiles) { }) const bundledCode = bundle.outputFiles[0].text - const escaped = bundledCode - .replace(/\\/g, '\\\\') - .replace(/`/g, '\\`') - .replace(/\$/g, '\\$') + const escaped = bundledCode.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$') const replacement = [ `(function() {`, From 37dd415fa2cbdc4609b3afa2382cabc58ebd81ec Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:50:27 +0000 Subject: [PATCH 3/5] fix wireit ordering, replaceAll, and ./worker.js path - package command now runs svelte-package && node scripts/post-process-workers.js && publint so post-processing happens after dist/ is created, not before - remove build:workers from package, vite-build, and check wireit dependencies - fix build:workers wireit metadata: correct files reference and empty output array - remove pnpm build:workers from prepare and check:watch scripts - use replaceAll in post-process-workers.js to handle duplicate worker patterns - use ./worker.js (relative path) in pcd/index.ts Co-authored-by: Devin T. Currie --- package.json | 19 +++++++------------ scripts/post-process-workers.js | 2 +- src/lib/loaders/pcd/index.ts | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 23bab8636..1bf83e8ec 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,11 @@ "go-build": "wireit", "package": "wireit", "svelte-sync": "wireit", - "prepare": "pnpm svelte-sync && pnpm build:workers || echo ''", + "prepare": "pnpm svelte-sync || echo ''", "prepack": "wireit", "preview": "vite preview", "check": "wireit", - "check:watch": "svelte-kit sync && pnpm build:workers && svelte-check --tsconfig ./tsconfig.json --watch", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write . && ./scripts/eslint-progress.sh . --fix", "lint": "wireit", "lint:prettier": "wireit", @@ -84,8 +84,7 @@ ], "dependencies": [ "proto:gen-ts", - "svelte-sync", - "build:workers" + "svelte-sync" ], "packageLocks": [ "pnpm-lock.yaml" @@ -105,12 +104,10 @@ "build:workers": { "command": "node scripts/post-process-workers.js", "files": [ - "scripts/build-workers.js", + "scripts/post-process-workers.js", "src/lib/loaders/pcd/worker.ts" ], - "output": [ - "src/lib/loaders/pcd/worker.inline.ts" - ], + "output": [], "packageLocks": [ "pnpm-lock.yaml" ] @@ -129,7 +126,7 @@ ] }, "package": { - "command": "svelte-package && publint", + "command": "svelte-package && node scripts/post-process-workers.js && publint", "files": [ "src/lib/**", "svelte.config.js", @@ -141,8 +138,7 @@ ], "dependencies": [ "proto:gen-ts", - "svelte-sync", - "build:workers" + "svelte-sync" ], "packageLocks": [ "pnpm-lock.yaml" @@ -184,7 +180,6 @@ "dependencies": [ "proto:gen-ts", "svelte-sync", - "build:workers", "vet" ], "packageLocks": [ diff --git a/scripts/post-process-workers.js b/scripts/post-process-workers.js index 8a2daebbf..c6176b3f9 100644 --- a/scripts/post-process-workers.js +++ b/scripts/post-process-workers.js @@ -61,7 +61,7 @@ for (const file of distFiles) { `})()`, ].join('\n') - result = result.replace(fullMatch, replacement) + result = result.replaceAll(fullMatch, replacement) try { unlinkSync(workerAbsPath) diff --git a/src/lib/loaders/pcd/index.ts b/src/lib/loaders/pcd/index.ts index d71cec6fc..fa26e480d 100644 --- a/src/lib/loaders/pcd/index.ts +++ b/src/lib/loaders/pcd/index.ts @@ -1,6 +1,6 @@ import type { Message, SuccessMessage } from './messages' -const worker = new Worker(new URL('worker.js', import.meta.url), { type: 'module' }) +const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }) let requestId = 0 const pending = new Map< From 6f127d53ca9cbaed2cd013cec6474b16bcd0110b Mon Sep 17 00:00:00 2001 From: Devin C Date: Wed, 8 Jul 2026 12:22:21 -0400 Subject: [PATCH 4/5] lint --- src/lib/loaders/pcd/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/loaders/pcd/index.ts b/src/lib/loaders/pcd/index.ts index fa26e480d..d71cec6fc 100644 --- a/src/lib/loaders/pcd/index.ts +++ b/src/lib/loaders/pcd/index.ts @@ -1,6 +1,6 @@ import type { Message, SuccessMessage } from './messages' -const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }) +const worker = new Worker(new URL('worker.js', import.meta.url), { type: 'module' }) let requestId = 0 const pending = new Map< From 8852e4b7ec5584e139741442a594f45737fb7ba1 Mon Sep 17 00:00:00 2001 From: Devin C Date: Wed, 8 Jul 2026 16:52:58 -0400 Subject: [PATCH 5/5] fix vite config --- vite.config.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 904611fa1..5e5492ddc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -42,7 +42,14 @@ export default defineConfig({ // @testing-library/svelte is excluded so its Svelte components aren't // pre-bundled, but its CJS grandchild aria-query (via @testing-library/dom) // must still be optimized or Rolldown won't expose its named exports. - include: ['@testing-library/svelte > @testing-library/dom'], + // + // PCDLoader is imported by the pcd web worker (loaders/pcd/worker.ts). Vite + // discovers worker deps lazily, so without this the dep optimizer re-bundles + // mid-run and reloads the page, flaking out whatever tests are importing then. + include: [ + '@testing-library/svelte > @testing-library/dom', + 'three/examples/jsm/loaders/PCDLoader.js', + ], exclude: ['@testing-library/svelte'], }, build: {