@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
359 lines (339 loc) • 13.5 kB
JavaScript
// Local type definition matching Next.js's internal JSONValue
// Used for Turbopack loader options which require serializable values
// Define webpack options interface based on Next.js webpack function signature
/**
* Get default MDX options for docs-infra
*/
export function getDocsInfraMdxOptions(customOptions = {}) {
const {
extractToIndex = true,
baseDir,
errorIfIndexOutOfDate = Boolean(process.env.CI),
defaultInlineCodeLanguage,
codeBlockEmphasisOptions
} = customOptions;
// Normalize extractToIndex to options object
let extractToIndexOptions;
if (extractToIndex === false) {
extractToIndexOptions = false;
} else if (extractToIndex === true) {
// Default filter: include all files under app/ and src/app/
// Index files (pattern/page.mdx) are automatically excluded by the matching logic
// Use process.cwd() as default baseDir (the directory where Next.js is running)
extractToIndexOptions = {
include: ['app', 'src/app'],
exclude: [],
baseDir: baseDir ?? process.cwd()
};
} else {
extractToIndexOptions = {
...extractToIndex,
baseDir: baseDir ?? process.cwd()
};
}
const defaultRemarkPlugins = [['remark-gfm'], ['@mui/internal-docs-infra/pipeline/transformMarkdownMetadata', {
extractToIndex: extractToIndexOptions,
markerPath: '.next/cache/docs-infra/index-updates',
errorIfIndexOutOfDate
}], ['@mui/internal-docs-infra/pipeline/transformMarkdownRelativePaths'], ['@mui/internal-docs-infra/pipeline/transformMarkdownBlockquoteCallouts'],
// Only pass options if explicitly set (undefined uses plugin default of 'tsx')
defaultInlineCodeLanguage !== undefined ? ['@mui/internal-docs-infra/pipeline/transformMarkdownCode', {
defaultInlineCodeLanguage
}] : ['@mui/internal-docs-infra/pipeline/transformMarkdownCode'], ['@mui/internal-docs-infra/pipeline/transformMarkdownMetaLinks']];
const defaultRehypePlugins = [codeBlockEmphasisOptions ? ['@mui/internal-docs-infra/pipeline/transformHtmlCodeBlock', codeBlockEmphasisOptions] : ['@mui/internal-docs-infra/pipeline/transformHtmlCodeBlock'], ['@mui/internal-docs-infra/pipeline/transformHtmlCodeInline'],
// enhancers
['@mui/internal-docs-infra/pipeline/enhanceCodeInline']];
// Build final plugin arrays
const remarkPlugins = customOptions.remarkPlugins ?? [...defaultRemarkPlugins, ...(customOptions.additionalRemarkPlugins ?? [])];
const rehypePlugins = customOptions.rehypePlugins ?? [...defaultRehypePlugins, ...(customOptions.additionalRehypePlugins ?? [])];
return {
remarkPlugins,
rehypePlugins
};
}
/**
* Next.js plugin for MUI docs infrastructure.
* Configures webpack loaders, turbopack rules for docs sites.
* Use getDocsInfraMdxOptions() with createMDX for MDX integration.
*/
export function withDocsInfra(options = {}) {
const {
additionalPageExtensions = [],
enableExportOutput = true,
demoPathPattern = './app/**/demos/*/index.ts',
demoDataPathPattern = './demo-data/*/index.ts',
clientDemoPathPattern = './app/**/demos/*/client.ts',
additionalDemoPatterns = {},
additionalTurbopackRules = {},
performance = {},
deferCodeParsing = 'gzip',
removeCommentsWithPrefix,
notableCommentsPrefix,
typesIndexFileName = 'page.mdx',
errorIfTypesIndexOutOfDate = Boolean(process.env.CI),
requireDemoClient,
requireDemoPage = false,
transformTypescriptToJavascript = false
} = options;
// Only include ordering in loader options if explicitly provided
const ordering = options.ordering;
const descriptionReplacements = options.descriptionReplacements;
const demoEmphasisOptions = options.demoEmphasisOptions;
const codeBlockEmphasisOptions = options.codeBlockEmphasisOptions;
// Compute updateParentIndex options similar to how transformMarkdownMetadata does
const updateParentIndex = {
baseDir: process.cwd(),
indexFileName: typesIndexFileName,
markerDir: '.next/cache/docs-infra/types-index-updates',
onlyUpdateIndexes: true,
errorIfOutOfDate: errorIfTypesIndexOutOfDate
};
let output = 'hastCompressed';
if (deferCodeParsing === 'json') {
output = 'hastJson';
} else if (deferCodeParsing === 'none') {
output = 'hast';
}
return (nextConfig = {}) => {
const basePageExtensions = ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'];
const pageExtensions = [...basePageExtensions, ...additionalPageExtensions];
// Build Turbopack rules
// Filter out undefined values to satisfy Turbopack's JSONValue type requirement
const codeHighlighterOptions = {
performance,
output,
...(removeCommentsWithPrefix && {
removeCommentsWithPrefix
}),
...(notableCommentsPrefix && {
notableCommentsPrefix
}),
...(demoEmphasisOptions && {
emphasisOptions: demoEmphasisOptions
}),
...(transformTypescriptToJavascript ? {
transformTypescriptToJavascript: true
} : {})
};
// The demo highlighter options carry `requireClient`/`requirePage` so the validate
// CLI can discover both the import specifier and the patterns it should ensure
// clients/pages for. The loader itself ignores these options.
const demoCodeHighlighterOptions = {
...codeHighlighterOptions,
...(requireDemoClient ? {
requireClient: requireDemoClient
} : {}),
...(requireDemoPage ? {
requirePage: true
} : {})
};
const turbopackRules = {
[demoPathPattern]: {
loaders: [{
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter',
options: demoCodeHighlighterOptions
}]
},
[demoDataPathPattern]: {
loaders: [{
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter',
options: codeHighlighterOptions
}]
},
[clientDemoPathPattern]: {
loaders: [{
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient',
options: {
performance
}
}]
},
'./app/**/types.ts': {
loaders: [{
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedTypes',
options: {
performance,
socketDir: '.next/docs-infra',
updateParentIndex,
...(codeBlockEmphasisOptions ? {
codeBlockEmphasisOptions: codeBlockEmphasisOptions
} : {}),
...(ordering ? {
ordering: ordering
} : {}),
...(descriptionReplacements ? {
descriptionReplacements: descriptionReplacements
} : {})
}
}]
},
'./app/sitemap/index.ts': {
loaders: [{
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedSitemap',
options: {
performance
}
}]
}
};
// Add additional demo patterns to Turbopack rules
if (additionalDemoPatterns.index) {
additionalDemoPatterns.index.forEach(pattern => {
turbopackRules[pattern] = {
loaders: [{
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter',
options: demoCodeHighlighterOptions
}]
};
});
}
if (additionalDemoPatterns.client) {
additionalDemoPatterns.client.forEach(pattern => {
turbopackRules[pattern] = {
loaders: [{
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient',
options: {
performance
}
}]
};
});
}
// Merge with additional turbopack rules
Object.assign(turbopackRules, additionalTurbopackRules);
const config = {
...nextConfig,
pageExtensions,
...(enableExportOutput && {
output: 'export'
}),
turbopack: {
...nextConfig.turbopack,
rules: {
...nextConfig.turbopack?.rules,
...turbopackRules
}
},
webpack: (webpackConfig, webpackOptions) => {
// Call existing webpack function if it exists
if (nextConfig.webpack) {
webpackConfig = nextConfig.webpack(webpackConfig, webpackOptions);
}
// Ensure module and rules exist
if (!webpackConfig.module) {
webpackConfig.module = {};
}
if (!webpackConfig.module.rules) {
webpackConfig.module.rules = [];
}
const {
defaultLoaders
} = webpackOptions;
// Add loader for demo index files
webpackConfig.module.rules.push({
test: new RegExp('[/\\\\]demos[/\\\\][^/\\\\]+[/\\\\]index\\.ts$'),
use: [defaultLoaders.babel, {
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter',
options: demoCodeHighlighterOptions
}]
});
// Add loader for demo data
webpackConfig.module.rules.push({
test: new RegExp('[/\\\\]demo-data[/\\\\][^/\\\\]+[/\\\\]index\\.ts$'),
use: [defaultLoaders.babel, {
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter',
options: codeHighlighterOptions
}]
});
// Client files for live demos - processes externals
webpackConfig.module.rules.push({
test: new RegExp('[/\\\\]demos[/\\\\][^/\\\\]+[/\\\\]client\\.ts$'),
use: [defaultLoaders.babel, {
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient',
options: {
performance
}
}]
});
// Sitemap loader
webpackConfig.module.rules.push({
test: new RegExp('[/\\\\]sitemap[/\\\\]index\\.ts$'),
use: [defaultLoaders.babel, {
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedSitemap',
options: {
performance
}
}]
});
// Types files for type metadata
webpackConfig.module.rules.push({
test: new RegExp('[/\\\\]app[/\\\\].*[/\\\\]types\\.ts$'),
use: [defaultLoaders.babel, {
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedTypes',
options: {
performance,
socketDir: '.next/docs-infra',
updateParentIndex,
...(codeBlockEmphasisOptions ? {
codeBlockEmphasisOptions
} : {}),
...(ordering ? {
ordering
} : {}),
...(descriptionReplacements ? {
descriptionReplacements
} : {})
}
}]
});
// Add webpack rules for additional demo patterns
if (additionalDemoPatterns.index) {
additionalDemoPatterns.index.forEach(pattern => {
// Convert Turbopack pattern to webpack regex
// Pattern like './app/**/demos/*/demo-*/index.ts'
// Should match paths like '/app/components/demos/Button/demo-variant/index.ts'
// Use placeholders to avoid corrupting character classes during replacement
const SEP = 'PATH_SEP_PLACEHOLDER';
const NOT_SEP = 'NOT_PATH_SEP_PLACEHOLDER';
const regexPattern = pattern.replace(/^\.\//, '') // Remove leading ./
.replace(/\*\*\//g, 'DOUBLE_STAR_PLACEHOLDER') // Replace **/ with placeholder
.replace(/\*/g, NOT_SEP) // Replace single * with placeholder
.replace(/\./g, '\\.') // Escape dots
.replace(/DOUBLE_STAR_PLACEHOLDER/g, `(?:${NOT_SEP}${SEP})*`) // Replace placeholder with zero or more directories
.replace(/\//g, SEP) // Convert all path separators to placeholder
.replace(new RegExp(NOT_SEP, 'g'), '[^/\\\\]+') // Replace NOT_SEP with actual pattern
.replace(new RegExp(SEP, 'g'), '[/\\\\]'); // Replace SEP with actual pattern
webpackConfig.module.rules.push({
test: new RegExp(`${regexPattern}$`),
use: [defaultLoaders.babel, {
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighter',
options: demoCodeHighlighterOptions
}]
});
});
}
if (additionalDemoPatterns.client) {
additionalDemoPatterns.client.forEach(pattern => {
// Convert Turbopack pattern to webpack regex
const regexPattern = pattern.replace(/^\.\//, '/') // Remove leading ./
.replace(/\*\*\//g, 'DOUBLE_STAR_PLACEHOLDER') // Replace **/ with placeholder
.replace(/\*/g, '[^/\\\\]+') // Replace single * with single dir pattern
.replace(/\./g, '\\.') // Escape dots
.replace(/DOUBLE_STAR_PLACEHOLDER/g, '(?:[^/\\\\]+/)*'); // Replace placeholder with zero or more directories
webpackConfig.module.rules.push({
test: new RegExp(`${regexPattern}$`),
use: [defaultLoaders.babel, {
loader: '@mui/internal-docs-infra/pipeline/loadPrecomputedCodeHighlighterClient',
options: {
performance
}
}]
});
});
}
return webpackConfig;
}
};
return config;
};
}