# Plugin System URL: /docs/concepts/plugin-system Plugin System [#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 [#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 [#how-it-works] Three plugin types exist: [`KernelPlugin`](../api/kernels), [`MiddlewarePlugin`](../api/middleware), and [`BundlerPlugin`](../api/bundler). Each is a plain object returned by a factory function. The factory pattern allows options validation and lazy URL resolution. KernelPlugin [#kernelplugin] A [KernelPlugin](../api/kernels) 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 the `defineKernel` module (resolved via `import.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 detection * `options` -- Kernel-specific options passed to `initialize()` The factory accepts options and merges them into the plugin. For example, `replicad({ withBrepEdges: true })` produces a plugin with `options: { withBrepEdges: true }`. MiddlewarePlugin [#middlewareplugin] A [MiddlewarePlugin](../api/middleware) describes an interceptor. Factory functions like `geometryCache()` and `parameterCache()` return: * `id` -- Unique identifier * `moduleUrl` -- URL of the `defineMiddleware` module * `options` -- Middleware-specific options Middleware order matters: first registered is the outermost layer in the onion model. See [Middleware Model](./middleware-model) for details. BundlerPlugin [#bundlerplugin] A [BundlerPlugin](../api/bundler) describes a bundler for specific file extensions. The built-in `esbuild()` handles `['ts', 'js', 'tsx', 'jsx']`: * `id` -- Unique identifier * `moduleUrl` -- URL of the `defineBundler` module * `extensions` -- File extensions this bundler handles * `options` -- 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 [#presets-for-zero-config-setup] The `presets.all()` function returns a `PresetOptions` object containing all built-in kernels, middleware, and bundlers: ```typescript import { createRuntimeClient, presets } from '@taucad/runtime'; const client = createRuntimeClient(presets.all()); ``` `PresetOptions` has `{ kernels, middleware, bundlers }` which can be spread into [`createRuntimeClient`](../api/client) alongside other options like `fileSystem`. Composition in createRuntimeClient [#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: 1. Loads kernel modules from `moduleUrl` and registers them in [`KernelRuntimeWorker`](./worker-model) 2. Loads middleware modules and builds the onion chain 3. 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 [#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.url` is 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 [#why-zod-schemas-for-validation] Kernels, middleware, and bundlers can define `optionsSchema` (a Zod object schema). When provided: * **Type safety** -- `z.infer` 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 [#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 `moduleUrl` and instantiates them. Kernel definitions implement `KernelDefinition`; middleware implements wrap hooks via `defineMiddleware`; bundlers implement `BundlerDefinition`. * **Plugins and Selection**: Kernel plugins drive [kernel selection](./kernel-selection). Extension lists, `detectImport`, and `builtinModuleNames` determine which kernel runs for a given file. Implications [#implications] * **No inheritance** -- Kernel authors use `defineKernel`, not `extends 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 [#further-reading] * [Architecture](./architecture) -- How layers compose * [Middleware Model](./middleware-model) -- How middleware wraps kernel operations * [Kernel Selection](./kernel-selection) -- How plugins influence selection * [API: Kernels](../api/kernels) -- Kernel factory functions and `KernelPlugin` * [API: Middleware](../api/middleware) -- `defineMiddleware` and middleware types * [API: Bundler](../api/bundler) -- `defineBundler` and `BundlerDefinition` * [Create Custom Kernel](../guides/custom-kernel) -- Implement a kernel with `defineKernel` * [Create Custom Middleware](../guides/custom-middleware) -- Implement middleware with `defineMiddleware`