Bundler & locateFile
How to wire the OCCT wasm asset in Vite, Next.js, Bun, Deno, and plain Node.
Start with import oc from 'libcascade'. The package resolves its adjacent
WASM asset without configuration in Node and compatible bundlers. Reach for
createInstance from libcascade/init — the entry that accepts options — only
when a bundler or deployment copies the binary to another URL.
The package exposes the binary through the libcascade/wasm
subpath export. Every relocation recipe on this page resolves the wasm through that
specifier; resolving it any other way (deep node_modules paths, direct
dist/* imports) bypasses the package's exports map and breaks under
strict bundler resolution.
Vite 6+
Vite's ?url suffix turns any asset import into a content-hashed URL string.
Use it when you want Vite to emit a content-hashed asset URL:
import { createInstance } from 'libcascade/init';
import wasmUrl from 'libcascade/wasm?url';
const oc = await createInstance({ locateFile: () => wasmUrl });Add libcascade to optimizeDeps.exclude in vite.config.ts so
Vite skips its dep-optimizer for the binary module:
import { defineConfig } from 'vite';
export default defineConfig({
optimizeDeps: { exclude: ['libcascade'] },
});Next.js 15 (App Router)
Next's App Router lacks a first-class ?url wasm import. The reliable pattern
is to copy the wasm into public/ at install time via a postinstall script
that resolves the subpath:
import { copyFile, mkdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
const src = fileURLToPath(import.meta.resolve('libcascade/wasm'));
await mkdir('public', { recursive: true });
await copyFile(src, 'public/opencascade_single.wasm');{
"scripts": {
"postinstall": "node scripts/copy-wasm.mjs"
}
}Then reference the public path:
'use client';
import { createInstance } from 'libcascade/init';
let ocPromise: ReturnType<typeof createInstance> | undefined;
export const getOc = () =>
(ocPromise ??= createInstance({ locateFile: () => '/opencascade_single.wasm' }));Mark libcascade as a server external package if you only call it
client-side:
import type { NextConfig } from 'next';
const config: NextConfig = {
serverExternalPackages: ['libcascade'],
};
export default config;Bun
Bun resolves wasm imports natively. The ?url pattern works identically to
Vite. No extra config required.
Node (ESM)
Use import.meta.resolve to find the wasm sibling to the loader:
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { createInstance } from 'libcascade/init';
const WASM_DIR = dirname(
fileURLToPath(import.meta.resolve('libcascade/wasm')),
);
const oc = await createInstance({ locateFile: (file: string) => join(WASM_DIR, file) });Deno
Deno exposes the same import.meta.resolve API as Node 22+. The Node snippet
above works unchanged.
Webpack 5
Webpack 5 handles wasm via asset/resource (preferred) or the legacy
file-loader. Wire locateFile to the emitted URL:
import { createInstance } from 'libcascade/init';
import wasmUrl from 'libcascade/wasm';
const oc = await createInstance({
locateFile: (file) => (file.endsWith('.wasm') ? wasmUrl : file),
});module.exports = {
module: {
rules: [
{
test: /\.wasm$/,
type: 'asset/resource',
},
],
},
resolve: {
fallback: {
fs: false,
perf_hooks: false,
os: false,
path: false,
worker_threads: false,
crypto: false,
stream: false,
},
},
};Mark libcascade as an external or exclude it from aggressive
bundle inlining — the 12+ MB wasm must stay a separate fetch.
Legacy bundlers
Create-React-App, react-app-rewired, and Webpack 4 are not supported in
V3 docs. Use Vite, current Next.js, Bun, Node, Deno, or Webpack 5 instead.
Common pitfalls
- Prefer zero configuration. Add
locateFileonly when the deployed WASM URL differs from the package-adjacent default. - Don't bundle the wasm inline. Bundling the 12+ MB binary as base64 explodes your JS payload and prevents the browser's wasm streaming compiler.
- Cache the
createInstancePromise. Each call instantiates another wasm module with its own C++ heap — memoize behind a singleton. The eager root entry (import oc from 'libcascade') needs no memoisation: ES module evaluation already happens once per realm.
Multi-threaded variant
The pthread-enabled build is a variant of the same package: ask for it with
createInstance({ variant: 'multi' }). That entry also owns the worker
plumbing the raw libcascade/multi glue leaves to you. Its wasm is exported at
libcascade/multi/wasm.
Browser prerequisite: every page that loads the threaded wasm must send cross-origin isolation headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpWithout these headers, browsers refuse to expose SharedArrayBuffer and the wasm fails to instantiate. See the multi-threaded build guide for benchmarks and when not to ship threaded.
Vite prerequisite: Emscripten spawns its pthread workers as ES modules with
top-level await, which Vite's default iife worker format cannot emit:
import { defineConfig } from 'vite';
export default defineConfig({
worker: { format: 'es' },
optimizeDeps: { exclude: ['libcascade'] },
});Run the following once after await createInstance(...) in every recipe below — it matches the benchmark harness and is required for full speedup on mesh/boolean workloads:
oc.BOPAlgo_Options.SetParallelMode(true);
oc.BRepMesh_IncrementalMesh.SetParallelDefault(true);Vite 6+
import { createInstance } from 'libcascade/init';
import wasmUrl from 'libcascade/multi/wasm?url';
const oc = await createInstance({ variant: 'multi', locateFile: () => wasmUrl });
oc.BOPAlgo_Options.SetParallelMode(true);
oc.BRepMesh_IncrementalMesh.SetParallelDefault(true);Next.js 15 (App Router)
Copy the MT wasm into public/ at install time:
import { copyFile, mkdir } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
const src = fileURLToPath(import.meta.resolve('libcascade/multi/wasm'));
await mkdir('public', { recursive: true });
await copyFile(src, 'public/opencascade_multi.wasm');'use client';
import { createInstance } from 'libcascade/init';
let ocPromise: ReturnType<typeof createInstance> | undefined;
export const getOcMulti = () =>
(ocPromise ??= createInstance({
variant: 'multi',
locateFile: () => '/opencascade_multi.wasm',
}).then((oc) => {
oc.BOPAlgo_Options.SetParallelMode(true);
oc.BRepMesh_IncrementalMesh.SetParallelDefault(true);
return oc;
}));Node (ESM)
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { createInstance } from 'libcascade/init';
const WASM_DIR = dirname(
fileURLToPath(import.meta.resolve('libcascade/multi/wasm')),
);
const oc = await createInstance({
variant: 'multi',
locateFile: (file: string) => join(WASM_DIR, file),
});
oc.BOPAlgo_Options.SetParallelMode(true);
oc.BRepMesh_IncrementalMesh.SetParallelDefault(true);Per-call overrides (SetRunParallel(true), BRepMesh_IncrementalMesh(..., isInParallel=true)) remain available for granular opt-in. See Multi-threaded build — Per-call activation.