@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
99 lines (96 loc) • 3.84 kB
JavaScript
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { parseAllCreateFactoryCalls } from "../pipeline/parseCreateFactoryCall/parseCreateFactoryCall.mjs";
import { findDemoIndexFiles } from "./findDemoIndexFiles.mjs";
import { fileExists, formatWithPrettier } from "./fileUtils/index.mjs";
const PAGE_TS_FILE_NAME = 'page.ts';
const PAGE_TSX_FILE_NAME = 'page.tsx';
const UNKNOWN_EXPORT_NAME = 'unknown';
/**
* Generates the contents for an auto-created demo `page.tsx`. The page renders
* the demo's named export from the sibling `index.ts` inside a `Page` component,
* so the demo renders as its own route.
*
* Exported for tests and reuse.
*/
export function generatePageFileContent(exportName) {
return [`import * as React from 'react';`, `import { ${exportName} } from '.';`, ``, `export default function Page() {`, ` return <${exportName} />;`, `}`, ``].join('\n');
}
/**
* Reads the demo's export name from a demo `index.ts` by reusing the same
* `create*` factory parser the precomputed code highlighter loader uses to load
* variants. Returns `null` when no named `export const X = create*(...)` is
* found (e.g. an anonymous default export), since a re-export page needs a name
* to import.
*
* Exported for tests.
*/
export async function findDemoExportName(source, filePath) {
const factories = await parseAllCreateFactoryCalls(source, filePath, {
allowExternalVariants: true
});
const exportName = Object.keys(factories).find(name => name !== UNKNOWN_EXPORT_NAME);
return exportName ?? null;
}
/**
* Ensures every demo `index.ts` matched by the configured demo patterns has a
* sibling `page.tsx` that renders the demo as the route's default export.
*
* Existing `page.tsx`/`page.ts` files are left untouched so developers can
* customise the page (e.g. wrap the demo with additional layout). Returns the
* list of files that were created, plus any errors encountered.
*/
export async function ensureDemoPages(options) {
const {
baseDir,
requirements
} = options;
if (requirements.length === 0) {
return {
demoCount: 0,
updatedFiles: [],
errors: []
};
}
const patterns = requirements.map(entry => entry.pattern);
const indexFiles = await findDemoIndexFiles(baseDir, patterns);
const updatedFiles = [];
const errors = [];
await Promise.all(Array.from(indexFiles.keys()).map(async indexPath => {
try {
const dir = path.dirname(indexPath);
const pageTsPath = path.join(dir, PAGE_TS_FILE_NAME);
const pageTsxPath = path.join(dir, PAGE_TSX_FILE_NAME);
// Only generate the page when neither a .ts nor .tsx page exists. Existing
// pages are left alone so developers can wrap the demo with extra layout.
const [pageTsExists, pageTsxExists] = await Promise.all([fileExists(pageTsPath), fileExists(pageTsxPath)]);
if (pageTsExists || pageTsxExists) {
return;
}
const indexSource = await readFile(indexPath, 'utf-8');
const exportName = await findDemoExportName(indexSource, indexPath);
if (!exportName) {
errors.push({
filePath: path.relative(baseDir, indexPath),
message: 'Could not determine the demo export name from a create* factory call.'
});
return;
}
const generated = generatePageFileContent(exportName);
const formatted = await formatWithPrettier(generated, pageTsxPath);
await writeFile(pageTsxPath, formatted, 'utf-8');
updatedFiles.push(path.relative(baseDir, pageTsxPath));
} catch (error) {
errors.push({
filePath: path.relative(baseDir, indexPath),
message: error?.message ?? String(error)
});
}
}));
updatedFiles.sort();
return {
demoCount: indexFiles.size,
updatedFiles,
errors
};
}