diff --git a/package-lock.json b/package-lock.json
index 86920549..6888777e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@node-core/doc-kit",
- "version": "1.3.8",
+ "version": "1.3.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@node-core/doc-kit",
- "version": "1.3.8",
+ "version": "1.3.9",
"dependencies": {
"@actions/core": "^3.0.0",
"@heroicons/react": "^2.2.0",
diff --git a/package.json b/package.json
index d6e7fc22..0964696b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@node-core/doc-kit",
"type": "module",
- "version": "1.3.8",
+ "version": "1.3.9",
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/doc-kit.git"
diff --git a/src/generators/web/README.md b/src/generators/web/README.md
index 428c4f4e..ff0ce1e6 100644
--- a/src/generators/web/README.md
+++ b/src/generators/web/README.md
@@ -6,19 +6,20 @@ The `web` generator transforms JSX AST entries into complete web bundles, produc
The `web` generator accepts the following configuration options:
-| Name | Type | Default | Description |
-| ----------------- | --------- | --------------------------------------------- | --------------------------------------------------------------------- |
-| `output` | `string` | - | The directory where HTML, JavaScript, and CSS files will be written |
-| `templatePath` | `string` | `'template.html'` | Path to the HTML template file |
-| `project` | `string` | `'Node.js'` | Project name used in page titles and the version selector |
-| `title` | `string` | `'{project} v{version} Documentation'` | Title template for HTML pages (supports `{project}`, `{version}`) |
-| `useAbsoluteURLs` | `boolean` | `false` | When `true`, all internal links use absolute URLs based on `baseURL` |
-| `editURL` | `string` | `'${GITHUB_EDIT_URL}/doc/api{path}.md'` | URL template for "edit this page" links |
-| `pageURL` | `string` | `'{baseURL}/latest-{version}/api{path}.html'` | URL template for documentation page links |
-| `head` | `object` | See below | Configurable ``, ``, and raw markup for the document head |
-| `lightningcss` | `object` | `{}` | Options spread into LightningCSS while bundling CSS (see below) |
-| `imports` | `object` | See below | Object mapping `#theme/` aliases to component paths for customization |
-| `virtualImports` | `object` | `{}` | Additional virtual module mappings merged into the build |
+| Name | Type | Default | Description |
+| ----------------- | --------- | --------------------------------------------- | ------------------------------------------------------------------------ |
+| `output` | `string` | - | The directory where HTML, JavaScript, and CSS files will be written |
+| `templatePath` | `string` | `'template.html'` | Path to the HTML template file |
+| `project` | `string` | `'Node.js'` | Project name used in page titles and the version selector |
+| `title` | `string` | `'{project} v{version} Documentation'` | Title template for HTML pages (supports `{project}`, `{version}`) |
+| `useAbsoluteURLs` | `boolean` | `false` | When `true`, all internal links use absolute URLs based on `baseURL` |
+| `editURL` | `string` | `'${GITHUB_EDIT_URL}/doc/api{path}.md'` | URL template for "edit this page" links |
+| `pageURL` | `string` | `'{baseURL}/latest-{version}/api{path}.html'` | URL template for documentation page links |
+| `head` | `object` | See below | Configurable ``, ``, and raw markup for the document head |
+| `lightningcss` | `object` | `{}` | Options spread into LightningCSS while bundling CSS (see below) |
+| `imports` | `object` | See below | Object mapping `#theme/` aliases to component paths for customization |
+| `virtualImports` | `object` | `{}` | Additional virtual module mappings merged into the build |
+| `rolldown` | `object` | `{}` | Options merged into the Rolldown build — extra plugins, etc. (see below) |
#### `head`
@@ -95,6 +96,63 @@ To apply more than one visitor, compose them with LightningCSS's
[lightningcss]: https://lightningcss.dev/transforms.html
+#### Custom Rolldown options
+
+The `rolldown` object is merged into the [Rolldown][rolldown] `build` call used
+for **both** the client and server bundles, so you can register extra plugins,
+inject compile-time constants, add module aliases, or set any other Rolldown
+option. The same config is applied to both builds — branch inside a plugin (or
+read the `SERVER`/`CLIENT` defines) if you need per-target behavior.
+
+The merge follows these rules:
+
+- **`plugins`** are registered after the built-in virtual-module plugin (so
+ they see the in-memory entry modules) and before the CSS loader.
+- **`transform.define`** and **`resolve.alias`** are merged key-by-key, so you
+ can add entries without dropping the generator's. The built-in `SERVER`/
+ `CLIENT` defines and the `react`/`react-dom` → `preact/compat` aliases
+ (plus anything from `imports`) always win.
+- **All other options** (e.g. `output.minify`, `treeshake`, `external`,
+ `platform`, `logLevel`) override the generator's defaults.
+- **`input`** and the built-in plugins are managed by the generator and cannot
+ be overridden.
+
+> Functions (plugins, hooks, alias resolvers, etc.) are allowed here because the
+> `web` generator runs entirely on the main thread. Unlike the chunked
+> generators, its config is never serialized to a worker, so values that can't
+> be structured-cloned are safe. Keep this in mind if `web` ever gains parallel
+> processing: function-valued options would then need a serializable form.
+
+```js
+// doc-kit.config.mjs
+import myRolldownPlugin from './my-rolldown-plugin.mjs';
+
+export default {
+ web: {
+ rolldown: {
+ // Register additional plugins (run after the built-in ones).
+ plugins: [myRolldownPlugin()],
+
+ // Inject extra compile-time constants (merged with SERVER/CLIENT).
+ transform: {
+ define: {
+ 'process.env.ANALYTICS_ID': JSON.stringify('UA-XXXXX'),
+ },
+ },
+
+ // Extend module resolution with custom aliases.
+ resolve: {
+ alias: {
+ '@components': './src/components',
+ },
+ },
+ },
+ },
+};
+```
+
+[rolldown]: https://rolldown.rs/
+
#### Default `imports`
| Alias | Default | Description |
diff --git a/src/generators/web/index.mjs b/src/generators/web/index.mjs
index 0d53c592..3695c864 100644
--- a/src/generators/web/index.mjs
+++ b/src/generators/web/index.mjs
@@ -86,5 +86,9 @@ export default createLazyGenerator({
'#theme/Layout': join(import.meta.dirname, './ui/components/Layout'),
},
virtualImports: {},
+
+ // Options merged into the Rolldown build (client and server), e.g. extra
+ // `plugins`. See the README for the merge semantics.
+ rolldown: {},
},
});
diff --git a/src/generators/web/types.d.ts b/src/generators/web/types.d.ts
index ec8684cb..2fb239e9 100644
--- a/src/generators/web/types.d.ts
+++ b/src/generators/web/types.d.ts
@@ -1,4 +1,5 @@
import type { BundleAsyncOptions, CustomAtRules } from 'lightningcss-wasm';
+import type { BuildOptions } from 'rolldown';
import type { JSXContent } from '../jsx-ast/utils/buildContent.mjs';
@@ -33,6 +34,9 @@ export type Configuration = {
>;
imports: Record;
virtualImports: Record;
+ // Options merged into the Rolldown build for the client and server bundles.
+ // See the web generator README for the merge semantics.
+ rolldown: Partial;
};
export type Generator = GeneratorMetadata<
diff --git a/src/generators/web/utils/bundle.mjs b/src/generators/web/utils/bundle.mjs
index f084474e..482d176d 100644
--- a/src/generators/web/utils/bundle.mjs
+++ b/src/generators/web/utils/bundle.mjs
@@ -39,18 +39,24 @@ export default async function bundleCode(
) {
const config = getConfig('web');
+ const { rolldown = {} } = config;
+
const result = await build({
+ ...rolldown,
+
// Entry points: array of virtual module names that the virtual plugin provides
input: Array.from(codeMap.keys()),
// Experimental features: import maps for client, none for server
experimental: {
chunkImportMap: !server,
+ ...rolldown.experimental,
},
checks: {
// Disable plugin timing logs for cleaner output. This can be re-enabled for debugging performance issues.
pluginTimings: false,
+ ...rolldown.checks,
},
// Output configuration
@@ -63,25 +69,33 @@ export default async function bundleCode(
// Minify output only for browser builds to optimize file size.
// Server builds are usually not minified to preserve stack traces and debuggability.
minify: !server,
+
+ ...rolldown.output,
},
// Platform informs Rolldown of the environment-specific code behavior:
// - 'node' enables things like `require`, and skips polyfills.
// - 'browser' enables inlining of polyfills and uses native browser features.
- platform: server ? 'node' : 'browser',
+ platform: rolldown.platform ?? (server ? 'node' : 'browser'),
// External dependencies to exclude from bundling.
// These are expected to be available at runtime in the server environment.
// This reduces bundle size and avoids bundling shared server libs.
- external: server
- ? ['preact', 'preact-render-to-string', '@node-core/ui-components']
- : [],
+ external:
+ rolldown.external ??
+ (server
+ ? ['preact', 'preact-render-to-string', '@node-core/ui-components']
+ : []),
transform: {
+ ...rolldown.transform,
+
// Inject global compile-time constants that will be replaced in code.
// These are useful for tree-shaking and conditional branching.
// Be sure to update type declarations (`types.d.ts`) if these change.
define: {
+ ...rolldown.transform?.define,
+
// Boolean flags used for conditional logic in source code:
// Example: `if (SERVER) {...}` or `if (CLIENT) {...}`
// These flags help split logic for server/client environments.
@@ -93,7 +107,7 @@ export default async function bundleCode(
// JSX transformation configuration.
// `'react-jsx'` enables the automatic JSX runtime, which doesn't require `import React`.
// Since we're using Preact via aliasing, this setting works well with `preact/compat`.
- jsx: 'react-jsx',
+ jsx: rolldown.transform?.jsx ?? 'react-jsx',
},
// Module resolution configuration.
@@ -101,17 +115,20 @@ export default async function bundleCode(
// exports condition to use
conditionNames: ['rolldown'],
+ // Tell the bundler where to find node_modules.
+ // We use our custom `NODE_MODULES`, and then the cwd's `node_modules`.
+ modules: [await getNodeModules(), 'node_modules'],
+
+ ...rolldown.resolve,
+
// Alias react imports to preact/compat for smaller bundle sizes.
// Explicit jsx-runtime aliases are required for the automatic JSX transform.
alias: {
react: 'preact/compat',
'react-dom': 'preact/compat',
+ ...rolldown.resolve?.alias,
...config.imports,
},
-
- // Tell the bundler where to find node_modules.
- // We use our custom `NODE_MODULES`, and then the cwd's `node_modules`.
- modules: [await getNodeModules(), 'node_modules'],
},
// Array of plugins to apply during the build.
@@ -123,13 +140,15 @@ export default async function bundleCode(
...virtualImports,
}),
+ ...(rolldown.plugins ?? []),
+
// Load CSS imports via the custom plugin.
// This plugin will collect imported CSS files and return them as `source` chunks.
cssLoader(),
],
// Enable tree-shaking to remove unused code
- treeshake: true,
+ treeshake: rolldown.treeshake ?? true,
// Return chunks in memory instead of writing to disk
write: false,
diff --git a/src/generators/web/utils/css.mjs b/src/generators/web/utils/css.mjs
index 2f2ca86b..ad57a04c 100644
--- a/src/generators/web/utils/css.mjs
+++ b/src/generators/web/utils/css.mjs
@@ -59,7 +59,10 @@ export default () => {
}
// Read the raw CSS file from disk
- const source = await readFile(id, 'utf8');
+ const source = await (lightningcss.resolver?.read ?? readFile)(
+ id,
+ 'utf8'
+ );
// Use Lightning CSS to compile the file with CSS Modules enabled
const { code, exports } = await bundleAsync({