Files
react-test/docs/scripts/formattedTSDemos.js
how2ice 005cf56baf
Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
init project
2025-12-12 14:26:25 +09:00

245 lines
7.1 KiB
JavaScript

/* eslint-disable no-console */
/**
* Transpiles TypeScript demos to formatted JavaScript.
* Can be used to verify that JS and TS demos are equivalent. No introduced change
* would indicate equivalence.
*/
/**
* List of demos or folders to ignore when transpiling.
* Only ignore files that aren't used in the UI.
*/
const ignoreList = ['/pages.ts', 'docs/data/joy/getting-started/templates'];
const path = require('path');
const fs = require('node:fs');
const babel = require('@babel/core');
const prettier = require('prettier');
const {
getPropTypesFromFile,
injectPropTypesInFile,
} = require('@mui/internal-scripts/typescript-to-proptypes');
const {
createTypeScriptProjectBuilder,
} = require('@mui-internal/api-docs-builder/utils/createTypeScriptProject');
const { default: yargs } = require('yargs');
const { hideBin } = require('yargs/helpers');
const { fixBabelGeneratorIssues, fixLineEndings } = require('@mui/internal-docs-utils');
const { default: CORE_TYPESCRIPT_PROJECTS } = require('../../scripts/coreTypeScriptProjects');
const babelConfig = {
presets: ['@babel/preset-typescript'],
plugins: [],
generatorOpts: { retainLines: true },
babelrc: false,
configFile: false,
shouldPrintComment: (comment) => !comment.startsWith(' @babel-ignore-comment-in-output'),
};
const workspaceRoot = path.join(__dirname, '../../');
async function getFiles(root) {
const files = [];
try {
await Promise.all(
(await fs.promises.readdir(root)).map(async (name) => {
const filePath = path.join(root, name);
const stat = await fs.promises.stat(filePath);
if (
stat.isDirectory() &&
!ignoreList.some((ignorePath) =>
filePath.startsWith(path.normalize(`${workspaceRoot}/${ignorePath}`)),
)
) {
files.push(...(await getFiles(filePath)));
} else if (
stat.isFile() &&
/\.tsx?$/.test(filePath) &&
!filePath.endsWith('types.ts') &&
!filePath.endsWith('.d.ts') &&
!ignoreList.some((ignorePath) => filePath.endsWith(path.normalize(ignorePath)))
) {
files.push(filePath);
}
}),
);
} catch (error) {
if (error.message?.includes('no such file or directory')) {
return [];
}
throw error;
}
return files;
}
const TranspileResult = {
Success: 0,
Failed: 1,
};
async function transpileFile(tsxPath, project) {
const jsPath = tsxPath.replace(/\.tsx?$/, '.js');
try {
const source = await fs.promises.readFile(tsxPath, 'utf8');
const transformOptions = { ...babelConfig, filename: tsxPath };
const enableJSXPreview =
!tsxPath.includes(path.join('pages', 'premium-themes')) &&
!tsxPath.includes(path.join('getting-started', 'templates'));
if (enableJSXPreview) {
transformOptions.plugins = transformOptions.plugins.concat([
[
require.resolve('docs/src/modules/utils/babel-plugin-jsx-preview'),
{ maxLines: 16, outputFilename: `${tsxPath}.preview` },
],
]);
}
const { code } = await babel.transformAsync(source, transformOptions);
if (/import \w* from 'prop-types'/.test(code)) {
throw new Error('TypeScript demo contains prop-types, please remove them');
}
console.log(tsxPath);
const propTypesAST = getPropTypesFromFile({
project,
filePath: tsxPath,
shouldResolveObject: ({ name }) => {
if (name === 'classes' || name === 'ownerState' || name === 'popper') {
return false;
}
return undefined;
},
});
const codeWithPropTypes = injectPropTypesInFile({ components: propTypesAST, target: code });
const prettierConfig = await prettier.resolveConfig(jsPath, {
config: path.join(workspaceRoot, 'prettier.config.mjs'),
});
const prettierFormat = async (jsSource) =>
prettier.format(jsSource, { ...prettierConfig, filepath: jsPath });
const codeWithoutTsIgnoreComments = codeWithPropTypes.replace(/^\s*\/\/ @ts-ignore.*$/gm, '');
const prettified = await prettierFormat(codeWithoutTsIgnoreComments);
const formatted = fixBabelGeneratorIssues(prettified);
const correctedLineEndings = fixLineEndings(source, formatted);
// removed blank lines change potential formatting
await fs.promises.writeFile(jsPath, await prettierFormat(correctedLineEndings));
return TranspileResult.Success;
} catch (err) {
console.error('Something went wrong transpiling %s\n%s\n', tsxPath, err);
return TranspileResult.Failed;
}
}
async function main(argv) {
const { watch: watchMode, disableCache, pattern } = argv;
// TODO: Remove at some point.
// Though not too soon so that it isn't disruptive.
// It's a no-op anyway.
if (disableCache !== undefined) {
console.warn(
'--disable-cache does not have any effect since it is the default. In the future passing this flag will throw.',
);
}
const filePattern = new RegExp(pattern);
if (pattern.length > 0) {
console.log(`Only considering demos matching ${filePattern}`);
}
const tsxFiles = [
...(await getFiles(path.join(workspaceRoot, 'docs/src/pages'))), // old structure
...(await getFiles(path.join(workspaceRoot, 'docs/data'))), // new structure
].filter((fileName) => filePattern.test(fileName));
const buildProject = createTypeScriptProjectBuilder(CORE_TYPESCRIPT_PROJECTS);
const project = buildProject('docs', { files: tsxFiles });
let successful = 0;
let failed = 0;
(
await Promise.all(
tsxFiles.map((file) => {
return transpileFile(file, project);
}),
)
).forEach((result) => {
switch (result) {
case TranspileResult.Success: {
successful += 1;
break;
}
case TranspileResult.Failed: {
failed += 1;
break;
}
default: {
throw new Error(`No handler for ${result}`);
}
}
});
console.log(
[
'------ Summary ------',
'%i demo(s) were successfully transpiled',
'%i demo(s) were unsuccessful',
].join('\n'),
successful,
failed,
);
if (!watchMode) {
if (failed > 0) {
process.exit(1);
}
return;
}
tsxFiles.forEach((filePath) => {
fs.watchFile(filePath, { interval: 500 }, async () => {
if ((await transpileFile(filePath, project, true)) === 0) {
console.log('Success - %s', filePath);
}
});
});
console.log('\nWatching for file changes...');
}
yargs()
.command({
command: '$0',
description: 'transpile TypeScript demos',
builder: (command) => {
return command
.option('watch', {
default: false,
description: 'transpiles demos as soon as they changed',
type: 'boolean',
})
.option('disable-cache', {
description: 'No longer supported. The cache is disabled by default.',
type: 'boolean',
})
.option('pattern', {
default: '',
description:
'Transpiles only the TypeScript demos whose filename matches the given pattern.',
type: 'string',
});
},
handler: main,
})
.help()
.strict(true)
.version(false)
.parse(hideBin(process.argv));