UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

121 lines (118 loc) 4.09 kB
import { readdir } from 'node:fs/promises'; import path from 'node:path'; const INDEX_FILE_NAME = 'index.ts'; /** * Converts a Turbopack-style glob (e.g. `./app/**\/demos/*\/index.ts`) to a * RegExp that matches absolute filesystem paths. Mirrors the logic used by * `withDocsInfra` for webpack rule generation. Pass-through when the input is * already a RegExp (webpack-rule `test` regexes). */ export function patternToRegExp(pattern) { if (pattern instanceof RegExp) { return pattern; } // Sentinels are wrapped in NUL bytes, which cannot occur in a file path or // glob. That makes them un-forgeable: the two-pass token substitution below // can never synthesize a sentinel by concatenating literal input with inserted // replacement output (e.g. an input segment ending in "NOT_" fusing with a // freshly-inserted separator). Built via fromCharCode so the source carries no // literal NUL bytes. const NUL = String.fromCharCode(0); const SEP = `${NUL}SEP${NUL}`; const NOT_SEP = `${NUL}NOT_SEP${NUL}`; const DOUBLE_STAR = `${NUL}DOUBLE_STAR${NUL}`; const body = pattern.replace(/^\.\//, '') // drop leading ./ .replace(/\*\*\//g, DOUBLE_STAR).replace(/\*/g, NOT_SEP).replace(/\./g, '\\.').replace(new RegExp(DOUBLE_STAR, 'g'), `(?:${NOT_SEP}${SEP})*`).replace(/\//g, SEP).replace(new RegExp(NOT_SEP, 'g'), '[^/\\\\]+').replace(new RegExp(SEP, 'g'), '[/\\\\]'); return new RegExp(`(?:^|[/\\\\])${body}$`); } /** * Finds the longest fixed-prefix directory in a glob pattern so we can avoid * walking the entire workspace. Webpack `test` regexes have no extractable * prefix, so we fall back to walking from `baseDir`. */ function patternBaseDir(pattern, baseDir) { if (pattern instanceof RegExp) { return baseDir; } const stripped = pattern.replace(/^\.\//, ''); const segments = stripped.split('/'); const fixed = []; for (const segment of segments) { if (segment.includes('*')) { break; } fixed.push(segment); } return path.join(baseDir, ...fixed); } async function collectIndexFiles(dir) { const out = []; let entries; try { entries = await readdir(dir, { withFileTypes: true }); } catch (error) { if (error.code === 'ENOENT') { return out; } throw error; } await Promise.all(entries.map(async entry => { if (entry.name === 'node_modules' || entry.name.startsWith('.')) { return; } const full = path.join(dir, entry.name); if (entry.isDirectory()) { const sub = await collectIndexFiles(full); out.push(...sub); } else if (entry.isFile() && entry.name === INDEX_FILE_NAME) { out.push(full); } })); return out; } /** * Walks the workspace once per fixed glob prefix and returns a map from each * matched demo `index.ts` absolute path to the first pattern that matched it. * * Shared by `ensureDemoClients` and `ensureDemoPages` so demo discovery stays * consistent between the `requireClient` and `requirePage` validate passes. */ export async function findDemoIndexFiles(baseDir, patterns) { // Map from absolute index.ts path → matching pattern (first wins). const results = new Map(); // Group patterns by their fixed prefix to share filesystem walks. const prefixes = new Map(); for (const pattern of patterns) { const prefix = patternBaseDir(pattern, baseDir); const existing = prefixes.get(prefix); if (existing) { existing.push(pattern); } else { prefixes.set(prefix, [pattern]); } } const compiledPatterns = patterns.map(pattern => ({ pattern, regex: patternToRegExp(pattern) })); await Promise.all(Array.from(prefixes.keys()).map(async prefix => { const indexFiles = await collectIndexFiles(prefix); for (const filePath of indexFiles) { if (results.has(filePath)) { continue; } for (const { pattern, regex } of compiledPatterns) { if (regex.test(filePath)) { results.set(filePath, pattern); break; } } } })); return results; }