jest/scripts/bundleTs.mjs

237 lines
5.9 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {createRequire} from 'module';
import * as path from 'path';
import {fileURLToPath} from 'url';
import {
CompilerState,
Extractor,
ExtractorConfig,
} from '@microsoft/api-extractor';
import chalk from 'chalk';
import {ESLint} from 'eslint';
import {glob} from 'glob';
import fs from 'graceful-fs';
import pkgDir from 'pkg-dir';
import {rimraf} from 'rimraf';
import {copyrightSnippet, getPackagesWithTsConfig} from './buildUtils.mjs';
const require = createRequire(import.meta.url);
const typescriptCompilerFolder = await pkgDir(require.resolve('typescript'));
const typesNodeReferenceDirective = '/// <reference types="node" />';
const excludedPackages = new Set(['@jest/globals', '@jest/test-globals']);
const packagesToBundle = getPackagesWithTsConfig().filter(
p => !excludedPackages.has(p.pkg.name),
);
console.log(chalk.inverse(' Extracting TypeScript definition files '));
const sharedExtractorConfig = {
$schema:
'https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json',
apiReport: {
enabled: false,
},
/**
* A list of NPM package names whose exports should be treated as part of this package.
*/
bundledPackages: [],
compiler: {
skipLibCheck: true,
},
docModel: {
enabled: false,
},
dtsRollup: {
enabled: true,
untrimmedFilePath: '<projectFolder>/dist/index.d.ts',
},
messages: {
compilerMessageReporting: {
default: {
logLevel: 'warning',
},
},
extractorMessageReporting: {
'ae-forgotten-export': {
logLevel: 'none',
},
'ae-missing-release-tag': {
logLevel: 'none',
},
default: {
logLevel: 'warning',
},
},
tsdocMessageReporting: {
default: {
logLevel: 'none',
},
},
},
tsdocMetadata: {
enabled: false,
},
};
await fs.promises.writeFile(
path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../api-extractor.json',
),
JSON.stringify(sharedExtractorConfig, null, 2),
);
const eslint = new ESLint({
cwd: process.cwd(),
fix: true,
overrideConfig: {
rules: {
// `d.ts` files are by nature `type` only imports, so it's just noise when looking at the file
'@typescript-eslint/consistent-type-imports': [
'error',
{prefer: 'no-type-imports'},
],
},
},
});
let compilerState;
await Promise.all(
packagesToBundle.map(async ({packageDir, pkg}) => {
const configFile = path.resolve(packageDir, 'api-extractor.json');
await fs.promises.writeFile(
configFile,
JSON.stringify(
{
extends: '../../api-extractor.json',
mainEntryPointFilePath: path.resolve(packageDir, pkg.types),
projectFolder: packageDir,
},
null,
2,
),
);
const extractorConfig = ExtractorConfig.loadFileAndPrepare(configFile);
if (!compilerState) {
compilerState = CompilerState.create(extractorConfig, {
additionalEntryPoints: packagesToBundle.map(({pkg, packageDir}) =>
path.resolve(packageDir, pkg.types),
),
typescriptCompilerFolder,
});
}
const extractorResult = Extractor.invoke(extractorConfig, {
compilerState,
localBuild: true,
showVerboseMessages: true,
typescriptCompilerFolder,
});
if (!extractorResult.succeeded || extractorResult.warningCount > 0) {
console.error(
chalk.inverse.red(' Unable to extract TypeScript definition files '),
);
throw new Error(
`API Extractor completed with ${extractorResult.errorCount} errors and ${extractorResult.warningCount} warnings`,
);
}
const filepath = extractorResult.extractorConfig.untrimmedFilePath;
let definitionFile = await fs.promises.readFile(filepath, 'utf8');
await rimraf(path.resolve(packageDir, 'build/**/*.d.ts'), {glob: true});
await fs.promises.rm(path.resolve(packageDir, 'dist/'), {
force: true,
recursive: true,
});
// this is invalid now, so remove it to not confuse `tsc`
await fs.promises.rm(path.resolve(packageDir, 'tsconfig.tsbuildinfo'), {
force: true,
recursive: true,
});
const dirsInBuild = await glob('**/', {
cwd: path.resolve(packageDir, 'build'),
});
await Promise.all(
dirsInBuild
.filter(dir => dir !== '.')
// reverse to delete deep directories first
.reverse()
.map(async dir => {
const dirToDelete = path.resolve(packageDir, 'build', dir);
try {
await fs.promises.rmdir(dirToDelete);
} catch (error) {
// e.g. `jest-jasmine2/build/jasmine` is not empty - ignore those errors
if (error.code !== 'ENOTEMPTY') {
throw error;
}
}
}),
);
definitionFile = definitionFile.replaceAll('\r\n', '\n');
const hasNodeTypesReference = definitionFile.includes(
typesNodeReferenceDirective,
);
if (hasNodeTypesReference) {
definitionFile = [
typesNodeReferenceDirective,
...definitionFile.split(typesNodeReferenceDirective),
].join('\n');
}
definitionFile = [
copyrightSnippet,
'',
...definitionFile.split(copyrightSnippet),
].join('\n');
const [lintResult] = await eslint.lintText(definitionFile, {
filePath: 'some-file.ts',
});
// if the autofixer did anything, the result is in `output`
const formattedContent = lintResult.output || definitionFile;
await fs.promises.writeFile(
filepath.replace(
`${path.sep}dist${path.sep}`,
`${path.sep}build${path.sep}`,
),
formattedContent,
);
}),
);
console.log(
chalk.inverse.green(' Successfully extracted TypeScript definition files '),
);