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
245 lines
7.1 KiB
JavaScript
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));
|