Plugin System
How defineKernel, defineMiddleware, and defineBundler create an extensible runtime.
Plugin System
The @taucad/runtime is extensible through a plugin system. Instead of inheritance hierarchies or configuration files, three plugin types compose via factory functions. This design keeps the core small, enables tree-shaking, and makes the extension surface explicit.
Context and Motivation
CAD runtimes must support multiple engines (Replicad, OpenCASCADE, Manifold, OpenSCAD, JSCAD, Zoo), multiple preprocessing steps (caching, coordinate transforms), and multiple bundling strategies (esbuild for JS/TS, none for OpenSCAD). Inheritance would force a rigid class hierarchy; configuration files would push logic into strings. Plugins offer a middle path: typed, composable extensions that the framework discovers at runtime through explicit registration.
How It Works
Three plugin types exist: KernelPlugin, MiddlewarePlugin, and BundlerPlugin. Each is a plain object returned by a factory function. The factory pattern allows options validation and lazy URL resolution.
KernelPlugin
A KernelPlugin describes a CAD engine. Factory functions like replicad(), opencascade(), manifold(), jscad(), zoo(), and tau() from @taucad/runtime/kernels -- plus openscad() from the separately-published @taucad/openscad package -- return a registration object:
id-- Unique identifier (e.g.,'replicad')moduleUrl-- URL of thedefineKernelmodule (resolved viaimport.meta.url)extensions-- File extensions this kernel handles (['ts', 'js'],['scad'], or['*']for catch-all)detectImport-- Optional regex for content-based selection (e.g.,import.*from\s+['"]replicad['"])builtinModuleNames-- Module names for bundler-assisted transitive import detectionoptions-- Kernel-specific options passed toinitialize()
The factory accepts options and merges them into the plugin. For example, replicad({ withBrepEdges: true }) produces a plugin with options: { withBrepEdges: true }.
MiddlewarePlugin
A MiddlewarePlugin describes an interceptor. Factory functions like geometryCache() and parameterCache() return:
id-- Unique identifiermoduleUrl-- URL of thedefineMiddlewaremoduleoptions-- Middleware-specific options
Middleware order matters: first registered is the outermost layer in the onion model. See Middleware Model for details.
BundlerPlugin
A BundlerPlugin describes a bundler for specific file extensions. The built-in esbuild() handles ['ts', 'js', 'tsx', 'jsx']:
id-- Unique identifiermoduleUrl-- URL of thedefineBundlermoduleextensions-- File extensions this bundler handlesoptions-- Bundler-specific options (e.g., esbuild target)
Multiple bundlers can be registered; the framework routes by extension. A kernel that does not use the bundler (e.g., OpenSCAD) never loads it.
Presets for Zero-Config Setup
The presets.all() function returns a PresetOptions object containing all built-in kernels, middleware, and bundlers:
import { createRuntimeClient, presets } from '@taucad/runtime';
const client = createRuntimeClient(presets.all());PresetOptions has { kernels, middleware, bundlers } which can be spread into createRuntimeClient alongside other options like fileSystem.
Composition in createRuntimeClient
When you call createRuntimeClient({ kernels, middleware, bundlers }), the client does not instantiate plugins immediately. It stores the plugin objects and passes them to the worker during initialize(). The worker then:
- Loads kernel modules from
moduleUrland registers them inKernelRuntimeWorker - Loads middleware modules and builds the onion chain
- Loads bundler modules and registers them by extension
Plugins are plain data; no class instances cross the main-thread/worker boundary. Only URLs and options are serialized.
The Factory Function Pattern
Each plugin type uses a factory rather than a constructor or static method. Benefits:
- Options validation -- Factories can validate and default options before returning the plugin. Zod schemas (when used) run at factory call time.
- URL resolution --
import.meta.urlis resolved in the factory's module context, so each plugin knows its own script location. - Immutability -- The returned object is a snapshot. Callers cannot mutate shared state.
- Tree-shaking -- Unused plugins are never imported; the factory is the single entry point.
Under the hood, factories are built with createKernelPlugin, createMiddlewarePlugin, and createBundlerPlugin helpers that handle URL resolution and options merging.
Why Zod Schemas for Validation
Kernels, middleware, and bundlers can define optionsSchema (a Zod object schema). When provided:
- Type safety --
z.infer<typeof optionsSchema>gives TypeScript the options type. - Runtime validation -- Invalid options throw at initialization, not during a render.
- Defaults --
.default()on schema fields supplies missing values.
The framework calls optionsSchema.parse(rawOptions) before passing options to initialize(). This keeps validation in one place and avoids scattered type checks.
Key Relationships
- Plugins and Client: The client aggregates plugins and forwards them to the worker. The client does not interpret plugin contents.
- Plugins and Worker: The worker dynamically imports modules from
moduleUrland instantiates them. Kernel definitions implementKernelDefinition; middleware implements wrap hooks viadefineMiddleware; bundlers implementBundlerDefinition. - Plugins and Selection: Kernel plugins drive kernel selection. Extension lists,
detectImport, andbuiltinModuleNamesdetermine which kernel runs for a given file.
Implications
- No inheritance -- Kernel authors use
defineKernel, notextends KernelWorker. This reduces coupling and simplifies testing. - Lazy loading -- Modules load on first use. A client with Replicad and OpenSCAD only loads the kernel for the file being rendered.
- Explicit dependencies -- Plugins declare what they need. The framework wires them together; there is no magic injection.
Further Reading
- Architecture -- How layers compose
- Middleware Model -- How middleware wraps kernel operations
- Kernel Selection -- How plugins influence selection
- API: Kernels -- Kernel factory functions and
KernelPlugin - API: Middleware --
defineMiddlewareand middleware types - API: Bundler --
defineBundlerandBundlerDefinition - Create Custom Kernel -- Implement a kernel with
defineKernel - Create Custom Middleware -- Implement middleware with
defineMiddleware