Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
The precomputed code client loader is a Webpack/Turbopack loader that enables build-time optimization of client-side demo components by processing demo client files and precomputing external dependencies for live editing environments.
The loader processes demo client files that use the createDemoClient factory pattern, automatically resolving and injecting all external dependencies at build time. This enables efficient live editing by having all required imports available without runtime resolution.
Tip
For the overall factory concept (including server vs client separation) see the Built Factories Pattern.
Note
The loader works with any
create*Clientfunction, not justcreateDemoClient. You could have custom demo client factories that follow the same pattern.
createDemoClient() or any create*Client() calls in client.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/*/client.ts'
// Add custom patterns if needed
additionalDemoPatterns: {
client: ['./app/**/snippets/*/client.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/*/client.ts': {
loaders: ['@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient'],
},
},
},
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
config.module.rules.push({
test: /[/\\]demos[/\\][^/\\]+[/\\]client\.ts$/,
use: [
defaultLoaders.babel,
'@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient',
],
});
return config;
},
};
The loader expects client files with create*Client factory functions following this pattern:
Important
Client files must include the
'use client';directive at the top since they are designed for client-side live editing environments.
Note
The client loader reads the corresponding
index.tsfile in the same directory to discover variants, demo name, and slug information. Theclient.tsandindex.tsfiles work as a pair.
app/
├── components/
│ └── my-component/
│ └── demos/
│ ├── basic-demo/
│ │ ├── index.ts # ← Main demo with createDemo()
│ │ ├── client.ts # ← createDemoClient() processed here
│ │ └── Example.tsx
│ └── advanced-demo/
│ ├── index.ts # ← Main demo
│ ├── client.ts # ← Client bundle processed here
│ ├── css-modules/
│ │ └── Example.tsx
│ └── tailwind/
│ └── Example.tsx
Create a client.ts file with the factory pattern:
'use client';
import { createDemoClient } from '../createDemoClient';
export const BasicDemoClient = createDemoClient(import.meta.url);
You can pass options to the client factory function:
'use client';
import { createDemoClient } from '../createDemoClient';
export const BasicDemoClient = createDemoClient(import.meta.url, {
name: 'Basic Demo Client',
});
The client file doesn't declare variants itself — the loader discovers them from the paired index.ts (e.g. a createDemoWithVariants call) and collects and merges externals across all of them:
'use client';
import { createDemoClient } from '../createDemoClient';
// Externals are collected from every variant defined in ./index.ts
export const MultiVariantDemoClient = createDemoClient(import.meta.url);
The loader works with any create*Client function. You can create custom demo client factories as needed:
'use client';
import { createCustomDemoClient } from '../createCustomDemoClient';
export const CustomDemoClient = createCustomDemoClient(import.meta.url, {
theme: 'dark',
interactive: true,
});
The loader follows these steps to precompute externals for your client components:
Finds your create*Client function call and extracts any options.
Discovers related variant files by reading the corresponding main demo file (index.ts in the same directory) to extract variant information. The client loader uses the variants defined in the main demo file since client files typically don't define their own variants.
Adds all external imports to the top of the client file and passes them as precompute.externals.
// Your source before processing
'use client';
export const DemoClient = createDemoClient(import.meta.url);
// After processing (simplified)
'use client';
import React from 'react';
import { Button } from '@mui/material';
import { styled } from '@mui/system';
export const DemoClient = createDemoClient(import.meta.url, {
precompute: {
externals: {
react: React,
'@mui/material': { Button },
'@mui/system': { styled },
},
},
});
The loader replaces the client factory function call with externals injected as imports and a data structure containing:
// Externals are injected as imports at the top
'use client';
import React from 'react';
import { Button, TextField } from '@mui/material';
import { styled } from '@mui/system';
// Function call gets externals in precompute.externals
export const DemoClient = createDemoClient(import.meta.url, {
precompute: {
externals: {
react: React,
'@mui/material': { Button, TextField },
'@mui/system': { styled },
},
},
});
Each entry maps a module path to the actual imported values resolved at build time:
const externals = {
react: React, // Default import
'@mui/material': { Button, TextField }, // Named imports as object
'@mui/system': { styled }, // Single named import as object
lodash: lodash, // Namespace import
};
The externals are the actual resolved import values, not descriptors. This allows the live editing environment to directly access the imported modules without additional resolution.
Only one create*Client function call is allowed per file:
// × Multiple calls will cause a build error
'use client';
export const DemoClient1 = createDemoClient(/* ... */);
export const DemoClient2 = createDemoClient(/* ... */); // Error!
// ✓ demo-1/client.ts — use separate files instead
'use client';
export const DemoClient1 = createDemoClient(/* ... */);
// ✓ demo-2/client.ts
'use client';
export const DemoClient2 = createDemoClient(/* ... */);
If the paired index.ts cannot be read, or contains no create* call or variants, the loader logs a warning and leaves the client file unprocessed (no externals are injected). If a variant file itself fails to load, the loader fails the build with an error naming the variant.
If any variant imports a server-only module (fs, node:*, or the server-only package), the loader leaves the client file untouched rather than inlining server code into the client bundle. The downstream factory then sees no precompute.externals and skips wrapping with a provider.
Your create*Client function must follow this pattern:
createDemoClient(
import.meta.url, // Required: file URL
{ options }, // Optional: options object
);
You can skip processing by adding skipPrecompute: true:
'use client';
export const DemoClient = createDemoClient(import.meta.url, {
skipPrecompute: true, // Will skip loader processing
});
client.ts vs index.ts)client.ts not *.ts)| Parameter | Type | Description |
|---|---|---|
| source | |
Promise<void>