UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

181 lines (166 loc) 8.67 kB
// 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))); } }