diff --git a/.gitignore b/.gitignore index 625627c5a..b0cfbbf58 100644 --- a/.gitignore +++ b/.gitignore @@ -48,9 +48,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 df4087ac7..ff9c2fa6d 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,12 @@ "dev:bun": "tsx server/check-bun && bun run server/server.ts", "dev:https": "vite dev -- --https", "build": "vite build && npm run prepack", - "build:workers": "node scripts/build-workers.js", - "prepack": "svelte-kit sync && pnpm build:workers && svelte-package && publint", + "build:workers": "node scripts/post-process-workers.js", + "prepack": "svelte-kit sync && svelte-package && pnpm build:workers && publint", "preview": "vite preview", - "prepare": "svelte-kit sync && pnpm build:workers || echo ''", - "check": "svelte-kit sync && pnpm build:workers && svelte-check --tsconfig ./tsconfig.json && pnpm vet", - "check:watch": "svelte-kit sync && pnpm build:workers && svelte-check --tsconfig ./tsconfig.json --watch", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && pnpm vet", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write . && eslint . --fix", "lint:draw": "golangci-lint run ./draw/...", "lint:client": "golangci-lint run ./client/...", 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 b217832a0..000000000 --- a/scripts/build-workers.js +++ /dev/null @@ -1,30 +0,0 @@ -import { build } from 'esbuild' -import { writeFileSync } from 'node: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 - .replaceAll('\\', '\\\\') - .replaceAll('`', '\\`') - .replaceAll('$', String.raw`\$`) - 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..8a2daebbf --- /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 { globSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs' +import path from 'node: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(path.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 = path.resolve(path.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 + .replaceAll('\\', '\\\\') + .replaceAll('`', '\\`') + .replaceAll('$', String.raw`\$`) + + 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 de628f62b..d71cec6fc 100644 --- a/src/lib/loaders/pcd/index.ts +++ b/src/lib/loaders/pcd/index.ts @@ -1,10 +1,6 @@ import type { Message, SuccessMessage } from './messages' -import { workerCode } from './worker.inline' - -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<