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' },
);
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[];
};
};
}
Only 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