@deftomat/opinionated
Version:
Opinionated tooling for JavaScript & TypeScript projects.
110 lines (109 loc) • 3.98 kB
JavaScript
import chalk from 'chalk';
import { execa } from 'execa';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import { basename } from 'node:path';
import { isMonorepoContext } from './context.js';
import { allocateCore } from './cpu.js';
import { ToolError } from './errors.js';
const { bold, red, yellow } = chalk;
// TODO: Use source files instead of builded files in monorepos!
// Otherwise, your monorepo could contains an outdated build of package and
// any other package which depends on it will be type checked against this outdated declarations.
/**
* Return TRUE when the given context contains `tsconfig.json` file.
*/
export function containsTypeScript(context) {
if (isMonorepoContext(context))
return getTypeScriptPackages(context.packages).length > 0;
return isTypeScriptPackage({ path: context.packageRoot });
}
/**
* Runs TypeScript checks in the given context.
*/
export async function runTypeCheck(context) {
if (isMonorepoContext(context))
return runTscInAllPackages(context);
return runTscInPackage(context);
}
async function runTscInPackage(context) {
const { packageRoot } = context;
if (!isTypeScriptPackage({ path: packageRoot }))
return;
const result = await withTscResult(context)({ path: packageRoot });
if (!hasError(result))
return;
throw new ToolError(red(`Checkup failed with the following TypeScript errors:\n`), result.stdout);
}
async function runTscInAllPackages(context) {
const packages = getTypeScriptPackages(context.packages).map(withTscResult(context));
const results = await Promise.all(packages);
if (!results.some(hasError))
return;
const { length } = results.filter(hasError);
const header = length === 1
? red(`1 package failed with the following TypeScript errors:`)
: red(`${length} packages failed with the following TypeScript errors:`);
const formatted = results
.filter(hasError)
.map(({ name, stdout }) => {
const decorationLength = 76;
const decoration = new Array(decorationLength).fill('=').join('');
const spacing = new Array(Math.round((decorationLength - name.length - 8) / 2))
.fill(' ')
.join('');
return yellow(`\n${decoration}\n${spacing}Package ${bold(name)}\n${decoration}\n\n`) + stdout;
})
.join('');
throw new ToolError(header, formatted);
}
function withTscResult(context) {
const { projectRoot, cachePath } = context;
const binPath = `${projectRoot}/node_modules/.bin/tsc`;
return async (pkg) => {
const thread = await allocateCore();
const outDir = `${cachePath}/tsc/${createHash('sha1')
.update(pkg.path)
.digest()
.toString('hex')}`;
try {
await execa(binPath, [
'--allowJs',
'--checkJs',
'--noEmit',
'true',
'--outDir',
outDir,
'--incremental',
'--allowUnreachableCode',
'false',
'--pretty',
'--noUnusedLocals',
'--removeComments',
'--sourceMap',
'--declarationMap',
'false',
'--diagnostics',
'false',
'--assumeChangesOnlyAffectDirectDependencies',
'false'
], { cwd: pkg.path });
return { ...pkg, stdout: null };
}
catch (error) {
return { ...pkg, stdout: error.stdout };
}
finally {
thread.free();
}
};
}
function getTypeScriptPackages(packages) {
return packages.map(pkg => ({ name: basename(pkg), path: pkg })).filter(isTypeScriptPackage);
}
function hasError({ stdout }) {
return stdout != null;
}
function isTypeScriptPackage({ path }) {
return fs.existsSync(`${path}/tsconfig.json`);
}