Skip to content

Commit

Permalink
chore: use TLA in build scripts (#13365)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB authored Oct 3, 2022
1 parent 286dab2 commit 23d7b56
Show file tree
Hide file tree
Showing 2 changed files with 301 additions and 313 deletions.
323 changes: 158 additions & 165 deletions scripts/buildTs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,186 +16,179 @@ import pLimit from 'p-limit';
import stripJsonComments from 'strip-json-comments';
import {getPackages} from './buildUtils.mjs';

(async () => {
const packages = getPackages();

const packagesWithTs = packages.filter(p =>
fs.existsSync(path.resolve(p.packageDir, 'tsconfig.json')),
);

const {stdout: allWorkspacesString} = await execa('yarn', [
'workspaces',
'list',
'--json',
]);

const workspacesWithTs = new Map(
JSON.parse(`[${allWorkspacesString.split('\n').join(',')}]`)
.filter(({location}) =>
packagesWithTs.some(({packageDir}) => packageDir.endsWith(location)),
)
.map(({location, name}) => [name, location]),
const packages = getPackages();

const packagesWithTs = packages.filter(p =>
fs.existsSync(path.resolve(p.packageDir, 'tsconfig.json')),
);

const {stdout: allWorkspacesString} = await execa('yarn', [
'workspaces',
'list',
'--json',
]);

const workspacesWithTs = new Map(
JSON.parse(`[${allWorkspacesString.split('\n').join(',')}]`)
.filter(({location}) =>
packagesWithTs.some(({packageDir}) => packageDir.endsWith(location)),
)
.map(({location, name}) => [name, location]),
);

packagesWithTs.forEach(({packageDir, pkg}) => {
assert.ok(pkg.types, `Package ${pkg.name} is missing \`types\` field`);

assert.strictEqual(
pkg.types,
pkg.main.replace(/\.js$/, '.d.ts'),
`\`main\` and \`types\` field of ${pkg.name} does not match`,
);

packagesWithTs.forEach(({packageDir, pkg}) => {
assert.ok(pkg.types, `Package ${pkg.name} is missing \`types\` field`);

assert.strictEqual(
pkg.types,
pkg.main.replace(/\.js$/, '.d.ts'),
`\`main\` and \`types\` field of ${pkg.name} does not match`,
);

const jestDependenciesOfPackage = Object.keys(pkg.dependencies || {})
.concat(Object.keys(pkg.devDependencies || {}))
.filter(dep => workspacesWithTs.has(dep))
.filter(dep => {
// nothing should depend on these
if (dep === 'jest-circus' || dep === 'jest-jasmine2') {
const jestDependenciesOfPackage = Object.keys(pkg.dependencies || {})
.concat(Object.keys(pkg.devDependencies || {}))
.filter(dep => workspacesWithTs.has(dep))
.filter(dep => {
// nothing should depend on these
if (dep === 'jest-circus' || dep === 'jest-jasmine2') {
return false;
}

// these are just `require.resolve`-ed
if (pkg.name === 'jest-config') {
if (dep === '@jest/test-sequencer' || dep === 'babel-jest') {
return false;
}
}

return true;
})
.map(dep =>
path.relative(
packageDir,
`${packageDir}/../../${workspacesWithTs.get(dep)}`,
),
)
.sort();

// these are just `require.resolve`-ed
if (pkg.name === 'jest-config') {
if (dep === '@jest/test-sequencer' || dep === 'babel-jest') {
return false;
}
}

return true;
})
.map(dep =>
path.relative(
packageDir,
`${packageDir}/../../${workspacesWithTs.get(dep)}`,
),
)
.sort();

if (jestDependenciesOfPackage.length > 0) {
const tsConfig = JSON.parse(
stripJsonComments(
fs.readFileSync(`${packageDir}/tsconfig.json`, 'utf8'),
),
);

const references = tsConfig.references.map(({path}) => path);

assert.deepStrictEqual(
references,
jestDependenciesOfPackage,
`Expected declared references to match dependencies in package ${
pkg.name
}. Got:\n\n${references.join(
'\n',
)}\nExpected:\n\n${jestDependenciesOfPackage.join('\n')}`,
);
}
});

const args = [
'tsc',
'-b',
...packagesWithTs.map(({packageDir}) => packageDir),
...process.argv.slice(2),
];

console.log(chalk.inverse(' Building TypeScript definition files '));

try {
await execa('yarn', args, {stdio: 'inherit'});
console.log(
chalk.inverse.green(' Successfully built TypeScript definition files '),
if (jestDependenciesOfPackage.length > 0) {
const tsConfig = JSON.parse(
stripJsonComments(fs.readFileSync(`${packageDir}/tsconfig.json`, 'utf8')),
);
} catch (e) {
console.error(
chalk.inverse.red(' Unable to build TypeScript definition files '),

const references = tsConfig.references.map(({path}) => path);

assert.deepStrictEqual(
references,
jestDependenciesOfPackage,
`Expected declared references to match dependencies in package ${
pkg.name
}. Got:\n\n${references.join(
'\n',
)}\nExpected:\n\n${jestDependenciesOfPackage.join('\n')}`,
);
throw e;
}
});

console.log(chalk.inverse(' Validating TypeScript definition files '));
const args = [
'tsc',
'-b',
...packagesWithTs.map(({packageDir}) => packageDir),
...process.argv.slice(2),
];

// we want to limit the number of processes we spawn
const cpus = Math.max(1, os.cpus().length - 1);
console.log(chalk.inverse(' Building TypeScript definition files '));

const typesReferenceDirective = '/// <reference types';
const typesNodeReferenceDirective = `${typesReferenceDirective}="node" />`;
try {
await execa('yarn', args, {stdio: 'inherit'});
console.log(
chalk.inverse.green(' Successfully built TypeScript definition files '),
);
} catch (e) {
console.error(
chalk.inverse.red(' Unable to build TypeScript definition files '),
);
throw e;
}

console.log(chalk.inverse(' Validating TypeScript definition files '));

// we want to limit the number of processes we spawn
const cpus = Math.max(1, os.cpus().length - 1);

const typesReferenceDirective = '/// <reference types';
const typesNodeReferenceDirective = `${typesReferenceDirective}="node" />`;

try {
const mutex = pLimit(cpus);
await Promise.all(
packagesWithTs.map(({packageDir, pkg}) =>
mutex(async () => {
const buildDir = path.resolve(packageDir, 'build/**/*.d.ts');

const globbed = await globby([buildDir]);

const files = await Promise.all(
globbed.map(file =>
Promise.all([file, fs.promises.readFile(file, 'utf8')]),
),
);

const filesWithTypeReferences = files
.filter(([, content]) => content.includes(typesReferenceDirective))
.filter(hit => hit.length > 0);

const filesWithReferences = filesWithTypeReferences
.map(([name, content]) => [
name,
content
.split('\n')
.map(line => line.trim())
.filter(line => line.includes(typesReferenceDirective))
.filter(line => line !== typesNodeReferenceDirective)
.join('\n'),
])
.filter(([, content]) => content.length > 0)
.filter(hit => hit.length > 0)
.map(([file, references]) =>
chalk.red(
`${chalk.bold(
file,
)} has the following non-node type references:\n\n${references}\n`,
),
)
.join('\n\n');

try {
const mutex = pLimit(cpus);
await Promise.all(
packagesWithTs.map(({packageDir, pkg}) =>
mutex(async () => {
const buildDir = path.resolve(packageDir, 'build/**/*.d.ts');
if (filesWithReferences) {
throw new Error(filesWithReferences);
}

const globbed = await globby([buildDir]);
const filesWithNodeReference = filesWithTypeReferences.map(
([filename]) => filename,
);

const files = await Promise.all(
globbed.map(file =>
Promise.all([file, fs.promises.readFile(file, 'utf8')]),
),
if (filesWithNodeReference.length > 0) {
assert.ok(
pkg.dependencies,
`Package \`${pkg.name}\` is missing \`dependencies\``,
);

const filesWithTypeReferences = files
.filter(([, content]) => content.includes(typesReferenceDirective))
.filter(hit => hit.length > 0);

const filesWithReferences = filesWithTypeReferences
.map(([name, content]) => [
name,
content
.split('\n')
.map(line => line.trim())
.filter(line => line.includes(typesReferenceDirective))
.filter(line => line !== typesNodeReferenceDirective)
.join('\n'),
])
.filter(([, content]) => content.length > 0)
.filter(hit => hit.length > 0)
.map(([file, references]) =>
chalk.red(
`${chalk.bold(
file,
)} has the following non-node type references:\n\n${references}\n`,
),
)
.join('\n\n');

if (filesWithReferences) {
throw new Error(filesWithReferences);
}

const filesWithNodeReference = filesWithTypeReferences.map(
([filename]) => filename,
assert.strictEqual(
pkg.dependencies['@types/node'],
'*',
`Package \`${pkg.name}\` is missing a dependency on \`@types/node\``,
);
}
}),
),
);
} catch (e) {
console.error(
chalk.inverse.red(' Unable to validate TypeScript definition files '),
);

if (filesWithNodeReference.length > 0) {
assert.ok(
pkg.dependencies,
`Package \`${pkg.name}\` is missing \`dependencies\``,
);
assert.strictEqual(
pkg.dependencies['@types/node'],
'*',
`Package \`${pkg.name}\` is missing a dependency on \`@types/node\``,
);
}
}),
),
);
} catch (e) {
console.error(
chalk.inverse.red(' Unable to validate TypeScript definition files '),
);

throw e;
}
throw e;
}

console.log(
chalk.inverse.green(' Successfully validated TypeScript definition files '),
);
})().catch(error => {
console.error('Got error', error.stack);
process.exitCode = 1;
});
console.log(
chalk.inverse.green(' Successfully validated TypeScript definition files '),
);
Loading

0 comments on commit 23d7b56

Please sign in to comment.