@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
181 lines (166 loc) • 8.67 kB
JavaScript
// webpack does not like node: imports
// eslint-disable-next-line n/prefer-node-protocol
import path from 'path';
import { createPerformanceLogger, logPerformance, nameMark, performanceMeasure } from "../loadPrecomputedCodeHighlighter/performanceLogger.mjs";
import { parseCreateFactoryCall } from "../parseCreateFactoryCall/parseCreateFactoryCall.mjs";
import { replacePrecomputeValue } from "../parseCreateFactoryCall/replacePrecomputeValue.mjs";
import { loadServerTypes } from "../loadServerTypes/index.mjs";
import { rewriteImportsToNull } from "../loaderUtils/rewriteImports.mjs";
const functionName = 'Load Precomputed Types';
/**
* Webpack loader that processes types and precomputes meta.
*
* Finds createTypesMeta calls, loads and processes all component types,
* then injects the precomputed type meta back into the source.
*
* Supports single component syntax: createTypesMeta(import.meta.url, Component)
* And object syntax: createTypesMeta(import.meta.url, { Component1, Component2 })
*
* Automatically skips processing if skipPrecompute: true is set.
*/
export async function loadPrecomputedTypes(source) {
const callback = this.async();
this.cacheable();
const options = this.getOptions();
const performanceNotableMs = options.performance?.notableMs ?? 100;
const performanceShowWrapperMeasures = options.performance?.showWrapperMeasures ?? false;
// Ensure rootContext always ends with / for correct URL resolution
const rootContext = this.rootContext || process.cwd();
const relativePath = path.relative(rootContext, this.resourcePath);
let observer = undefined;
if (options.performance?.logging) {
observer = new PerformanceObserver(createPerformanceLogger(performanceNotableMs, performanceShowWrapperMeasures, relativePath));
observer.observe({
entryTypes: ['measure']
});
}
let currentMark = nameMark(functionName, 'Start Loading', [relativePath]);
performance.mark(currentMark);
try {
// Parse the source to find a single createTypesMeta call
const typesMetaCall = await parseCreateFactoryCall(source, this.resourcePath, {
allowExternalVariants: true
});
currentMark = performanceMeasure(currentMark, {
mark: 'Parsed Factory',
measure: 'Factory Parsing'
}, [functionName, relativePath]);
// If no createTypesMeta call found, return the source unchanged
if (!typesMetaCall) {
callback(null, source);
return;
}
// If skipPrecompute is true, return the source unchanged
if (typesMetaCall.options.skipPrecompute) {
callback(null, source);
return;
}
// Resolve socket directory from loader options
const socketDir = options.socketDir ? path.resolve(rootContext, options.socketDir) : undefined;
// Convert types.ts path to types.md path
const typesMarkdownPath = this.resourcePath.replace(/\.tsx?$/, '.md');
// Check if this component should be excluded from the parent index
const excludeFromIndex = Boolean(typesMetaCall.structuredOptions?.excludeFromIndex);
// Resolve updateParentIndex.baseDir to an absolute path if provided
// Skip if excludeFromIndex is set in the factory call options
const updateParentIndex = options.updateParentIndex && !excludeFromIndex ? {
baseDir: options.updateParentIndex.baseDir ? path.resolve(rootContext, options.updateParentIndex.baseDir) : rootContext,
indexFileName: options.updateParentIndex.indexFileName
} : undefined;
// Call the core server-side logic
const result = await loadServerTypes({
typesMarkdownPath,
rootContext,
variants: typesMetaCall.variants,
watchSourceDirectly: Boolean(typesMetaCall.structuredOptions?.watchSourceDirectly),
formattingOptions: options.formatting,
socketDir,
performanceLogging: options.performance?.logging,
updateParentIndex,
externalTypesPattern: options.externalTypesPattern,
ordering: options.ordering,
descriptionReplacements: options.descriptionReplacements,
codeBlockEmphasisOptions: options.codeBlockEmphasisOptions,
sync: true,
output: 'hastCompressed'
});
currentMark = performanceMeasure(currentMark, {
mark: 'server types meta loaded',
measure: 'server types meta loading'
}, [functionName, relativePath], true);
// Determine if the factory was written with a single component or multiple components (object form)
// createTypes(import.meta.url, Checkbox) => 'Checkbox'
// createTypes(import.meta.url, { Checkbox, Button }) => undefined
const singleComponentName = typeof typesMetaCall.structuredVariants === 'string' ? typesMetaCall.structuredVariants : undefined;
const precompute = {
exports: result.exports,
additionalTypes: result.additionalTypes,
variantOnlyAdditionalTypes: result.variantOnlyAdditionalTypes,
variantTypeNames: result.variantTypeNames,
singleComponentName,
anchorMap: result.anchorMap
};
// Replace the component reference with an empty object to avoid importing actual component code.
// The createMultipleTypes factory will use precompute.exports keys instead of typeDef keys.
const modifiedCallInfo = {
...typesMetaCall,
structuredVariants: {}
};
// Replace the factory function call with the actual precomputed data.
// This modifies the factory call at the BOTTOM of the file, so import positions remain valid.
let modifiedSource = replacePrecomputeValue(source, precompute, modifiedCallInfo);
// Remove the component import(s) to prevent loading React components on the server.
// Find which external import contains the component(s) we're replacing.
// NOTE: We do this AFTER replacePrecomputeValue because:
// 1. replacePrecomputeValue modifies the factory call at the bottom of the file
// 2. Import positions (at the top) remain valid after that modification
// 3. rewriteImportsToNull uses those positions to null out the imports
if (typesMetaCall.importsAndComments?.externals) {
// Collect component names to remove from imports
const componentNames = new Set();
if (singleComponentName) {
// Single component form: createTypes(import.meta.url, Component)
componentNames.add(singleComponentName);
} else if (typesMetaCall.structuredVariants && typeof typesMetaCall.structuredVariants === 'object') {
// Object form: createTypes(import.meta.url, { Component1, Component2 })
Object.keys(typesMetaCall.structuredVariants).forEach(name => componentNames.add(name));
}
if (componentNames.size > 0) {
const componentImportPaths = new Set();
for (const [importPath, importData] of Object.entries(typesMetaCall.importsAndComments.externals)) {
const hasComponent = importData.names.some(n => componentNames.has(n.name) || componentNames.has(n.alias ?? ''));
if (hasComponent) {
componentImportPaths.add(importPath);
}
}
if (componentImportPaths.size > 0) {
modifiedSource = rewriteImportsToNull(modifiedSource, componentImportPaths, typesMetaCall.importsAndComments.externals);
}
}
}
performanceMeasure(currentMark, {
mark: 'replaced precompute',
measure: 'precompute replacement'
}, [functionName, relativePath]);
// Add all dependencies to webpack's watch list
// Dependencies are already paths from TypeScript's program.getSourceFiles()
result.allDependencies.forEach(dep => {
this.addDependency(dep);
});
if (options.performance?.logging) {
if (options.performance?.significantDependencyCountThreshold && result.allDependencies.length > options.performance.significantDependencyCountThreshold) {
// eslint-disable-next-line no-console
console.log(`[${functionName}] ${relativePath} - added ${result.allDependencies.length} dependencies to watch:\n\n${result.allDependencies.map(dep => `- ${path.relative(rootContext, dep)}`).join('\n')}\n`);
}
}
// log any pending performance entries before completing
observer?.takeRecords()?.forEach(entry => logPerformance(entry, performanceNotableMs, performanceShowWrapperMeasures, relativePath));
observer?.disconnect();
callback(null, modifiedSource);
} catch (error) {
// log any pending performance entries before completing
observer?.takeRecords()?.forEach(entry => logPerformance(entry, performanceNotableMs, performanceShowWrapperMeasures, relativePath));
observer?.disconnect();
callback(error instanceof Error ? error : new Error(String(error)));
}
}