Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions bazel/api-golden/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ts_project(
deps = [
"//bazel:node_modules/@microsoft/api-extractor",
"//bazel:node_modules/@types/node",
"//bazel:node_modules/piscina",
"//bazel:node_modules/typescript",
],
)
Expand Down
45 changes: 35 additions & 10 deletions bazel/api-golden/index_npm_packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {normalizePathToPosix} from './path-normalize.js';
import {readFileSync} from 'fs';
import {testApiGolden} from './test_api_report.js';
import * as fs from 'fs';
import {Piscina} from 'piscina';

/** Interface describing contents of a `package.json`. */
export interface PackageJson {
Expand Down Expand Up @@ -40,11 +41,11 @@ async function main(
const packageJsonPath = path.join(npmPackageDir, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as PackageJson;
const entryPoints = findEntryPointsWithinNpmPackage(npmPackageDir, packageJson);
const outdatedGoldens: string[] = [];

let allTestsSucceeding = true;
const worker = new Piscina<Parameters<typeof testApiGolden>, string>({
filename: path.resolve(__dirname, './test_api_report.js'),
});

for (const {subpath, typesEntryPointPath} of entryPoints) {
const processEntryPoint = async (subpath: string, typesEntryPointPath: string) => {
// API extractor generates API reports as markdown files. For each types
// entry-point we maintain a separate golden file. These golden files are
// based on the name of the defining NodeJS exports subpath in the NPM package,
Expand All @@ -53,32 +54,56 @@ async function main(
const goldenFilePath = path.join(goldenDir, goldenName);
const moduleName = normalizePathToPosix(path.join(packageJson.name, subpath));

const actual = await testApiGolden(
// Run API extractor in child processes. This is because API extractor is very
// synchronous. This allows us to significantly speed up golden testing.
const actual = await worker.run([
typesEntryPointPath,
stripExportPattern,
typeNames,
packageJsonPath,
moduleName,
);
]);

if (actual === null) {
console.error(`Could not generate API golden for subpath: "${subpath}". See errors above.`);
process.exit(1);
}

if (approveGolden) {
fs.mkdirSync(path.dirname(goldenFilePath), {recursive: true});
fs.writeFileSync(goldenFilePath, actual, 'utf8');
await fs.promises.mkdir(path.dirname(goldenFilePath), {recursive: true});
await fs.promises.writeFile(goldenFilePath, actual, 'utf8');
} else {
const expected = fs.readFileSync(goldenFilePath, 'utf8');
const expected = await fs.promises.readFile(goldenFilePath, 'utf8');
if (actual !== expected) {
// Keep track of outdated goldens for error message.
outdatedGoldens.push(goldenName);
allTestsSucceeding = false;
return false;
}
}

return true;
};

const outdatedGoldens: string[] = [];
const tasks: Promise<boolean>[] = [];
// Process in batches. Otherwise we risk out of memory errors.
const batchSize = 10;

for (let i = 0; i < entryPoints.length; i += batchSize) {
const batchEntryPoints = entryPoints.slice(i, i + batchSize);

for (const {subpath, typesEntryPointPath} of batchEntryPoints) {
tasks.push(processEntryPoint(subpath, typesEntryPointPath));
}

// Wait for new batch.
await Promise.all(tasks);
}

// Wait for final batch/retrieve all results.
const results = await Promise.all(tasks);
const allTestsSucceeding = results.every((r) => r === true);

if (outdatedGoldens.length) {
console.error(chalk.red(`The following goldens are outdated:`));
outdatedGoldens.forEach((name) => console.info(`- ${name}`));
Expand Down
12 changes: 6 additions & 6 deletions bazel/api-golden/test_api_report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export async function testApiGolden(
customPackageName: string,
): Promise<string | null> {
const tempDir =
process.env.TEST_TMPDIR ?? fs.mkdtempSync(path.join(os.tmpdir(), 'api-golden-rule'));
process.env.TEST_TMPDIR ??
(await fs.promises.mkdtemp(path.join(os.tmpdir(), 'api-golden-rule')));
const rjsMode = process.env['RJS_MODE'] === 'true';

let resolvedTypePackages: Awaited<ReturnType<typeof resolveTypePackages>> | null = null;
Expand Down Expand Up @@ -144,8 +145,8 @@ export async function testApiGolden(
if (!result.succeeded) {
return null;
}
const reportOut = fs.readFileSync(reportTmpOutPath, 'utf8');
fs.rmSync(reportTmpOutPath);
const reportOut = await fs.promises.readFile(reportTmpOutPath, 'utf8');
await fs.promises.rm(reportTmpOutPath);
return reportOut;
}

Expand All @@ -167,7 +168,6 @@ async function processExtractorMessage(message: ExtractorMessage) {
}
}

/** Resolves the `package.json` of the workspace executing this action. */
function resolveWorkspacePackageJsonPath(): string {
return path.resolve(`./package.json`);
export default function (args: Parameters<typeof testApiGolden>) {
return testApiGolden(...args);
}
5 changes: 3 additions & 2 deletions bazel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
"name": "@devinfra/bazel",
"dependencies": {
"@microsoft/api-extractor": "7.52.2",
"typescript": "5.8.2",
"@types/node": "22.14.0"
"@types/node": "22.14.0",
"piscina": "^4.9.2",
"typescript": "5.8.2"
},
"pnpm": {
"onlyBuiltDependencies": []
Expand Down
178 changes: 178 additions & 0 deletions bazel/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.