Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
The precompute loader is a Webpack/Turbopack loader that enables build-time optimization of code examples by processing demo files and precomputing syntax highlighting, TypeScript transformations, and dependency resolution.
The loader processes demo files that use the createDemo factory pattern, automatically resolving and processing all code variants at build time rather than runtime.
Tip
For the high-level rationale behind this pattern see the Built Factories Pattern.
Note
The loader works with any
create*function, not justcreateDemo. You could haveapp/components/my-component/snippets/example/index.tswithcreateSnippet(), or any other factory function that follows the same pattern.
createDemo(), createSnippet(), or any create*() calls in index.ts filesThe easiest way to configure this loader is with the withDocsInfra Next.js plugin:
// next.config.js
import { withDocsInfra } from '@mui/internal-docs-infra/withDocsInfra';
export default withDocsInfra({
// Automatically includes:
// - './app/**/demos/*/index.ts'
// - './src/demo-data/*/index.ts' (for globals)
// Add custom patterns if needed
additionalDemoPatterns: {
index: ['./app/**/snippets/*/index.ts'],
},
});
If you need manual control, add the loader directly to your next.config.mjs:
Note
The Turbopack loader requires Next.js version v15.5 or later (depends on this fix)
/** @type {import('next').NextConfig} */
const nextConfig = {
turbopack: {
rules: {
'./app/**/demos/*/index.ts': {
loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter'],
},
// Add pattern for global demo data
'./src/demo-data/*/index.ts': {
loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter'],
},
},
},
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
config.module.rules.push({
test: /[/\\]demos[/\\][^/\\]+[/\\]index\.ts$/,
use: [
defaultLoaders.babel,
'@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter',
],
});
// Add rule for global demo data
config.module.rules.push({
test: /[/\\]demo-data[/\\][^/\\]+[/\\]index\.ts$/,
use: [
defaultLoaders.babel,
'@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter',
],
});
return config;
},
};
The loader expects files with create* factory functions following this pattern:
app/
├── components/
│ └── my-component/
│ ├── demos/
│ │ ├── basic-demo/
│ │ │ ├── index.ts # ← createDemo() processed here
│ │ │ ├── React.tsx
│ │ │ └── Vue.vue
│ │ └── advanced-demo/
│ │ ├── index.ts # ← And here
│ │ ├── TypeScript.ts
│ │ └── JavaScript.js
│ └── snippets/
│ └── example/
│ ├── index.ts # ← createSnippet() processed here
│ └── Component.tsx
Create an index.ts file with the factory pattern:
import { createDemo } from '../createDemo';
import Default from './Default';
export const BasicDemo = createDemo(import.meta.url, Default);
The loader handles multiple code variants automatically:
import { createDemoWithVariants } from '../createDemo';
import CssModules from './Component.tsx';
import Tailwind from './Component.jsx';
export const MultiVariantDemo = createDemoWithVariants(import.meta.url, { CssModules, Tailwind });
You can pass options to the factory function, such as a name and slug override:
import { createDemoWithVariants } from '../createDemo';
import CssModules from './Component.tsx';
import Tailwind from './Component.jsx';
export const MultiVariantDemo = createDemoWithVariants(
import.meta.url,
{ CssModules, Tailwind },
{ name: 'Multi-variant Example', slug: 'multi' },
);
You can configure comment extraction options per-demo, which override the global loader options:
import { createDemo } from '../createDemo';
import Example from './Example';
export const HighlightDemo = createDemo(import.meta.url, Example, {
// Collect @focus and @section comments for enhancers
notableCommentsPrefix: ['@focus', '@section'],
// Remove @internal comments from displayed source
removeCommentsWithPrefix: ['@internal'],
});
This is useful when different demos need different comment handling—for example, one demo might use @focus comments while another uses @section comments for different visual treatments.
The loader works with any create* function. To use custom factory functions like createSnippet(), extend your configuration:
/** @type {import('next').NextConfig} */
const nextConfig = {
turbopack: {
rules: {
'./app/**/demos/*/index.ts': {
loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter'],
},
// Add pattern for snippets directory
'./app/**/snippets/*/index.ts': {
loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter'],
},
},
},
};
Then create your custom factory file:
import { createSnippet } from '../createSnippet';
import Example from './Example';
export const ComponentSnippet = createSnippet(import.meta.url, Example);
The loader follows these steps to precompute your code examples:
Finds your create* function call and extracts the variants and options.
Converts relative imports to absolute file paths for each variant.
Inserts precompute into your source with the processed data.
// Your source before processing
export const Demo = createDemo(import.meta.url, Component);
// After processing (simplified)
export const Demo = createDemo(
import.meta.url,
Component,
{
precompute: {
Default: {
fileName: "Component.tsx",
source: /* syntax highlighted HAST nodes */,
transforms: { /* JavaScript version */ }
}
}
}
);
The loader replaces the factory function call with a data structure containing:
interface PrecomputedData {
[variantName: string]: {
fileName: string; // Main file name
source: HastNode[]; // Syntax highlighted AST
extraFiles: {
// Additional dependencies
[path: string]: HastNode[];
};
transforms: {
// Language variants
[language: string]: HastNode[];
};
};
}
The loader accepts configuration options when used with webpack:
// next.config.mjs
config.module.rules.push({
test: /[/\\]demos[/\\][^/\\]+[/\\]index\.ts$/,
use: [
defaultLoaders.babel,
{
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter',
options: {
// Output format for HAST nodes
output: 'hastGzip', // 'hast' | 'hastJson' | 'hastGzip'
// Comment extraction for sourceEnhancers
notableCommentsPrefix: ['@highlight', '@focus'],
removeCommentsWithPrefix: ['@internal'],
// Performance logging
performance: {
logging: true,
notableMs: 100,
showWrapperMeasures: false,
},
},
},
],
});
These options control how comments are extracted from source files and made available to sourceEnhancers:
notableCommentsPrefix: Array of prefixes for comments that should be collected and included in the result. Comments starting with these prefixes will be available in the comments field passed to enhancers.
removeCommentsWithPrefix: Array of prefixes for comments that should be stripped from the displayed source. These comments can still be collected via notableCommentsPrefix.
Tip
These options can be overridden per-demo in the
createDemo()options object. See Per-Demo Comment Extraction.
Example use case - highlighting specific lines:
// In your demo file:
const example = 'highlighted';
const normal = 'not highlighted';
// The @highlight comment is extracted and passed to your enhancer,
// which can then add visual highlighting to that line
'hast': Raw HAST objects (largest, fastest to parse)'hastJson': JSON-stringified HAST (smaller, needs parsing)'hastGzip': Gzip-compressed JSON (smallest, needs decompression)logging: Enable performance logging during buildsnotableMs: Threshold in milliseconds for logging slow operationsshowWrapperMeasures: Include wrapper timing measurements in logsOnly one create* function call is allowed per file:
// × Multiple calls will cause a build error
export const Demo1 = createDemo(/* ... */);
export const Demo2 = createDemo(/* ... */); // Error!
// ✓ Use separate files instead
// demo-1/index.ts
export const Demo1 = createDemo(/* ... */);
// demo-2/index.ts
export const Demo2 = createDemo(/* ... */);
If a variant file cannot be found, the loader will log a warning and skip that variant, but continue processing other variants.
Your create* function must follow this pattern:
createDemo(
import.meta.url, // Required: file URL
component, // Required: component object
{ options }, // Optional: options object
);
createDemoWithVariants(
import.meta.url, // Required: file URL
{ variants }, // Required: variant object
{ options }, // Optional: options object
);
TypeScript, JavaScript, CSS)maxDepth limits for recursion