From 58edd0c059991fbebdf597c878d06d53ab79edb0 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Sun, 30 Aug 2026 23:56:00 -0400 Subject: [PATCH] fix(editor): keep manifold-3d out of consumer bundler graphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit manifold-3d's emscripten glue awaits import('node:module') behind a Node check; the branch never executes in a browser, but webpack refuses to build any graph that can reach it. export-manager.tsx statically imports the manifold worker wrapper and ExportManager renders unconditionally from the editor root, so every external webpack consumer of @pascal-app/editor failed at build time (#715). The worker chunk is still built by the consumer's bundler, but it no longer contains a traceable manifold-3d specifier. The glue is loaded at runtime through an import() no bundler follows: bare specifier first (bun tests, dev servers, bundlers that inlined it anyway), then a version-pinned jsDelivr copy for bundled browser builds — emscripten locates manifold.wasm relative to the glue's own URL, so the CDN path self-resolves. configureManifoldRuntime(options) lets offline or CSP-restricted hosts point both URLs at self-hosted assets. A failed load no longer poisons later attempts: the cached module promise resets on rejection. Fixes #715 Co-Authored-By: Claude Fable 5 --- packages/editor/src/index.tsx | 2 + .../lib/print-shell-compiler-manifold-core.ts | 69 ++++++++++++++++--- .../print-shell-compiler-manifold-worker.ts | 15 +++- .../print-shell-compiler-manifold.worker.ts | 2 +- .../src/lib/print-shell-compiler-protocol.ts | 8 +++ 5 files changed, 84 insertions(+), 12 deletions(-) diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx index 0d105fdc1..c2cb09461 100644 --- a/packages/editor/src/index.tsx +++ b/packages/editor/src/index.tsx @@ -515,6 +515,8 @@ export { editorHostPanelRegistry, registerEditorHostPanel, } from './lib/plugin-panels' +export { configureManifoldRuntime } from './lib/print-shell-compiler-manifold-worker' +export type { ManifoldRuntimeOptions } from './lib/print-shell-compiler-protocol' export { createQuickMeasurementPointerScheduler, quickMeasurementContext, diff --git a/packages/editor/src/lib/print-shell-compiler-manifold-core.ts b/packages/editor/src/lib/print-shell-compiler-manifold-core.ts index 082dcb835..013cd2e26 100644 --- a/packages/editor/src/lib/print-shell-compiler-manifold-core.ts +++ b/packages/editor/src/lib/print-shell-compiler-manifold-core.ts @@ -1,6 +1,10 @@ -import ManifoldModule, { type Manifold as ManifoldSolid, type ManifoldToplevel } from 'manifold-3d' +import type { Manifold as ManifoldSolid, ManifoldToplevel } from 'manifold-3d' import type { PrintShellCompileDiagnostic } from './print-shell-compiler-baseline' -import type { ManifoldCompileOutput, ManifoldMeshData } from './print-shell-compiler-protocol' +import type { + ManifoldCompileOutput, + ManifoldMeshData, + ManifoldRuntimeOptions, +} from './print-shell-compiler-protocol' let modulePromise: Promise | null = null const MANIFOLD_OUTPUT_WELD_EPSILON_METERS = 2e-5 @@ -8,14 +12,59 @@ const COLLINEAR_SEAM_CROSS_LENGTH_SQ = 1e-20 type Triangle = [number, number, number] -async function getManifoldModule(wasmUrl?: string): Promise { - modulePromise ??= ManifoldModule(wasmUrl ? { locateFile: () => wasmUrl } : undefined).then( - (module) => { +type ManifoldFactory = (config?: { + locateFile?: (path: string) => string +}) => Promise + +// manifold-3d's emscripten glue awaits import('node:module') behind a Node +// check. The branch never executes in a browser, but webpack refuses to +// *build* a graph that can reach it, so a static specifier here poisons every +// external bundler that compiles this package's source (#715). The factory is +// therefore loaded through an import() no bundler can trace: the bare +// specifier resolves wherever node-style resolution exists at runtime (bun +// tests, dev servers, bundlers that inline it anyway), and the version-pinned +// CDN copy covers bundled browser builds that left the specifier unresolved — +// emscripten then locates manifold.wasm relative to the glue's own URL. Hosts +// that can't reach the CDN (offline, CSP) pass their own URLs through +// configureManifoldRuntime. +const MANIFOLD_VERSION = '3.5.1' +const FALLBACK_MODULE_URL = `https://cdn.jsdelivr.net/npm/manifold-3d@${MANIFOLD_VERSION}/manifold.js` + +function importUntraced(specifier: string): Promise<{ default: ManifoldFactory }> { + return import(/* webpackIgnore: true */ /* @vite-ignore */ specifier) +} + +async function loadManifoldFactory(moduleUrl?: string): Promise { + const specifiers = moduleUrl ? [moduleUrl] : ['manifold-3d', FALLBACK_MODULE_URL] + let lastError: unknown + for (const specifier of specifiers) { + try { + return (await importUntraced(specifier)).default + } catch (error) { + lastError = error + } + } + throw lastError instanceof Error ? lastError : new Error('Failed to load manifold-3d.') +} + +async function getManifoldModule(runtime?: ManifoldRuntimeOptions): Promise { + modulePromise ??= loadManifoldFactory(runtime?.moduleUrl) + .then((factory) => { + const wasmUrl = runtime?.wasmUrl + return factory(wasmUrl ? { locateFile: () => wasmUrl } : undefined) + }) + .then((module) => { module.setup() return module - }, - ) - return modulePromise + }) + try { + return await modulePromise + } catch (error) { + // A transient load failure (offline, blocked CDN) must not poison every + // later compile attempt with the cached rejection. + modulePromise = null + throw error + } } function manifoldMesh( @@ -211,7 +260,7 @@ function elapsed(startedAt: number): number { export async function compileManifoldMeshData( meshes: ManifoldMeshData[], - wasmUrl?: string, + runtime?: ManifoldRuntimeOptions, ): Promise { const startedAt = performance.now() const sourceNodeIds = Array.from(new Set(meshes.map((mesh) => mesh.nodeId))).sort() @@ -238,7 +287,7 @@ export async function compileManifoldMeshData( } try { - const module = await getManifoldModule(wasmUrl) + const module = await getManifoldModule(runtime) for (const mesh of meshes) { try { solids.push(new module.Manifold(manifoldMesh(module, mesh))) diff --git a/packages/editor/src/lib/print-shell-compiler-manifold-worker.ts b/packages/editor/src/lib/print-shell-compiler-manifold-worker.ts index f4ddb4c85..d274f3a60 100644 --- a/packages/editor/src/lib/print-shell-compiler-manifold-worker.ts +++ b/packages/editor/src/lib/print-shell-compiler-manifold-worker.ts @@ -16,12 +16,25 @@ import { import type { ManifoldCompileOutput, ManifoldMeshData, + ManifoldRuntimeOptions, ManifoldWorkerRequest, ManifoldWorkerResponse, } from './print-shell-compiler-protocol' const WORKER_TIMEOUT_MS = 60_000 +let manifoldRuntime: ManifoldRuntimeOptions | undefined + +/** + * Overrides where the print-export worker loads the manifold-3d module and + * wasm from. See the loader in print-shell-compiler-manifold-core.ts for the + * default resolution order; hosts with restrictive networks should call this + * with self-hosted asset URLs before the first print export. + */ +export function configureManifoldRuntime(options: ManifoldRuntimeOptions | undefined): void { + manifoldRuntime = options +} + export type ManifoldCompileRunner = (meshes: ManifoldMeshData[]) => Promise export type SemanticManifoldCompileOptions = SemanticPrintCompileOptions & { @@ -76,7 +89,7 @@ export const runManifoldWorker: ManifoldCompileRunner = (meshes) => { const activeWorker = getWorker() const id = nextRequestId nextRequestId += 1 - const request: ManifoldWorkerRequest = { id, meshes } + const request: ManifoldWorkerRequest = { id, meshes, runtime: manifoldRuntime } const transfer = meshes.flatMap((mesh) => [ mesh.positions.buffer as ArrayBuffer, mesh.indices.buffer as ArrayBuffer, diff --git a/packages/editor/src/lib/print-shell-compiler-manifold.worker.ts b/packages/editor/src/lib/print-shell-compiler-manifold.worker.ts index 49b273969..20f0118cc 100644 --- a/packages/editor/src/lib/print-shell-compiler-manifold.worker.ts +++ b/packages/editor/src/lib/print-shell-compiler-manifold.worker.ts @@ -10,7 +10,7 @@ const workerScope = self as unknown as { } workerScope.addEventListener('message', async (event) => { - const output = await compileManifoldMeshData(event.data.meshes) + const output = await compileManifoldMeshData(event.data.meshes, event.data.runtime) const response: ManifoldWorkerResponse = { id: event.data.id, ...output } const transfer: Transferable[] = [] if (response.status === 'compiled') { diff --git a/packages/editor/src/lib/print-shell-compiler-protocol.ts b/packages/editor/src/lib/print-shell-compiler-protocol.ts index 8554f1e2e..6907ba62b 100644 --- a/packages/editor/src/lib/print-shell-compiler-protocol.ts +++ b/packages/editor/src/lib/print-shell-compiler-protocol.ts @@ -22,9 +22,17 @@ export type ManifoldCompileOutput = durationMs: number } +export type ManifoldRuntimeOptions = { + /** URL of the manifold-3d emscripten glue module. Defaults to the pinned CDN copy. */ + moduleUrl?: string + /** URL of manifold.wasm. Defaults to resolving relative to the glue module. */ + wasmUrl?: string +} + export type ManifoldWorkerRequest = { id: number meshes: ManifoldMeshData[] + runtime?: ManifoldRuntimeOptions } export type ManifoldWorkerResponse = ManifoldCompileOutput & { id: number }