Skip to content

[code-infra] Add --watch option to build:types #45547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 110 additions & 48 deletions scripts/buildTypes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,70 @@ import path from 'path';
import yargs from 'yargs';
import { $ } from 'execa';
import * as babel from '@babel/core';
import throttle from 'lodash/throttle';

interface Options {
watch: boolean;
}

const $$ = $({ stdio: 'inherit' });

async function emitDeclarations(tsconfig: string, outDir: string) {
async function emitDeclarations(tsconfig: string, outDir: string, { watch = false }: Options) {
// eslint-disable-next-line no-console
console.log(`Building types for ${path.resolve(tsconfig)}`);
await $$`tsc -p ${tsconfig} --outDir ${outDir} --declaration --emitDeclarationOnly`;

const tsconfigExists = await fs.access(tsconfig).then(
() => true,
() => false,
);

if (!tsconfigExists) {
throw new Error(
'Unable to find a tsconfig to build this project. ' +
`The package root needs to contain a 'tsconfig.build.json'. ` +
`The package root is '${path.basename(tsconfig)}'`,
);
}

const watchOptions = watch ? ['--watch', '--preserveWatchOutput'] : [];
await $$`tsc -p ${tsconfig} --outDir ${outDir} --declaration --emitDeclarationOnly ${watchOptions}`;
}

async function addImportExtensions(folder: string) {
console.log(`Adding import extensions`);
const dtsFiles = await glob('**/*.d.ts', { absolute: true, cwd: folder });
if (dtsFiles.length === 0) {
throw new Error(`Unable to find declaration files in '${folder}'`);
async function addImportExtensions(dtsFile: string) {
const result = await babel.transformFileAsync(dtsFile, {
configFile: false,
plugins: [
['@babel/plugin-syntax-typescript', { dts: true }],
['@mui/internal-babel-plugin-resolve-imports'],
],
});

if (typeof result?.code === 'string') {
const existing = await fs.readFile(dtsFile, 'utf8');
if (existing === result.code) {
return;
}
await fs.writeFile(dtsFile, result.code);
} else {
console.error('failed to transform', dtsFile);
}
}

await Promise.all(
dtsFiles.map(async (dtsFile) => {
const result = await babel.transformFileAsync(dtsFile, {
configFile: false,
plugins: [
['@babel/plugin-syntax-typescript', { dts: true }],
['@mui/internal-babel-plugin-resolve-imports'],
],
});

if (typeof result?.code === 'string') {
await fs.writeFile(dtsFile, result.code);
} else {
console.error('failed to transform', dtsFile);
}
}),
);
async function watchDeclarations(sourceDirectory: string, onChange: (file: string) => void) {
const handleChange = throttle(onChange, 300);
const watcher = fs.watch(sourceDirectory, { recursive: true });
for await (const event of watcher) {
if (event.filename?.endsWith('.d.ts')) {
handleChange(event.filename);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider normalizing path.resolve(sourceDirectory, event.filename), windows uses \, unix /, path.resolve() ensures you're using the correct format for the current OS

}
}
}

async function copyDeclarations(sourceDirectory: string, destinationDirectory: string) {
const fullSourceDirectory = path.resolve(sourceDirectory);
const fullDestinationDirectory = path.resolve(destinationDirectory);

// eslint-disable-next-line no-console
console.log(`Copying declarations from ${fullSourceDirectory} to ${fullDestinationDirectory}`);

await fs.cp(fullSourceDirectory, fullDestinationDirectory, {
Expand All @@ -60,12 +86,44 @@ async function copyDeclarations(sourceDirectory: string, destinationDirectory: s
});
}

async function postProcessDts(
dtsFile: string,
{
esmFolder,
buildFolder,
modernFolder,
}: { esmFolder: string; buildFolder: string; modernFolder: string },
) {
const relative = path.relative(esmFolder, dtsFile);
const buildFile = path.join(buildFolder, relative);
const modernFile = path.join(modernFolder, relative);
await addImportExtensions(dtsFile);
await Promise.all([fs.cp(dtsFile, buildFile), fs.cp(dtsFile, modernFile)]);
}

async function postProcessTypes(esmFolder: string, buildFolder: string, modernFolder: string) {
// eslint-disable-next-line no-console
console.log(`Adding import extensions`);
const dtsFiles = await glob('**/*.d.ts', { absolute: true, cwd: esmFolder });
if (dtsFiles.length === 0) {
throw new Error(`Unable to find declaration files in '${esmFolder}'`);
}

await Promise.all(
dtsFiles.map(async (dtsFile) => {
await postProcessDts(dtsFile, { esmFolder, buildFolder, modernFolder });
}),
);
}

interface HandlerArgv {
skipTsc: boolean;
watch: boolean;
}

async function main(argv: HandlerArgv) {
const packageRoot = process.cwd();
const tsconfigPath = path.join(packageRoot, 'tsconfig.build.json');

const srcPath = path.join(packageRoot, 'src');
const buildFolder = path.join(packageRoot, 'build');
Expand All @@ -75,27 +133,25 @@ async function main(argv: HandlerArgv) {
await copyDeclarations(srcPath, esmFolder);

if (!argv.skipTsc) {
const tsconfigPath = path.join(packageRoot, 'tsconfig.build.json');
const tsconfigExists = await fs.access(tsconfigPath).then(
() => true,
() => false,
);

if (!tsconfigExists) {
throw new Error(
'Unable to find a tsconfig to build this project. ' +
`The package root needs to contain a 'tsconfig.build.json'. ` +
`The package root is '${packageRoot}'`,
);
}

await emitDeclarations(tsconfigPath, esmFolder);
await emitDeclarations(tsconfigPath, esmFolder, { watch: false });
}

await addImportExtensions(esmFolder);

await copyDeclarations(esmFolder, buildFolder);
await copyDeclarations(esmFolder, modernFolder);
await postProcessTypes(esmFolder, buildFolder, modernFolder);

if (argv.watch) {
// eslint-disable-next-line no-console
console.log('Watching for changes...');
await Promise.all([
watchDeclarations(srcPath, async () => {
await copyDeclarations(srcPath, esmFolder);
}),
argv.skipTsc ? null : emitDeclarations(tsconfigPath, esmFolder, { watch: true }),
watchDeclarations(esmFolder, async (filename) => {
const dtsFile = path.resolve(esmFolder, filename);
await postProcessDts(dtsFile, { esmFolder, buildFolder, modernFolder });
}),
]);
}
}

yargs(process.argv.slice(2))
Expand All @@ -104,11 +160,17 @@ yargs(process.argv.slice(2))
description:
'Builds a project with a fix for https://github.com/microsoft/TypeScript/issues/39117',
builder: (command) => {
return command.option('skipTsc', {
type: 'boolean',
default: false,
describe: 'Set to `true` if you want the legacy behavior of just copying .d.ts files.',
});
return command
.option('skipTsc', {
type: 'boolean',
default: false,
describe: 'Set to `true` if you want the legacy behavior of just copying .d.ts files.',
})
.option('watch', {
type: 'boolean',
default: false,
describe: 'Set to `true` if you want to build types upon changes.',
});
},
handler: main,
})
Expand Down