@finos/legend-dev-utils
Version:
Legend Studio development utilities, helpers, and scripts
224 lines (214 loc) • 8.72 kB
JavaScript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { sep, resolve, basename, extname } from 'path';
import micromatch from 'micromatch';
import { execSync } from 'child_process';
import { lstatSync, existsSync } from 'fs';
import chalk from 'chalk';
import { getTsConfigJSON } from './TypescriptConfigUtils.js';
import { exitWithError, loadJSON } from './DevUtils.js';
const getDir = (file) =>
lstatSync(file).isDirectory() ? file : file.split(sep).slice(0, -1).join(sep);
const getTSProjectInfo = (dirname, projectPath, tsConfigFileName) => {
const projectFullPath = resolve(dirname, `${projectPath}`);
const dir = getDir(projectFullPath);
const tsConfigPath = !lstatSync(projectFullPath).isDirectory()
? projectFullPath
: resolve(projectFullPath, tsConfigFileName);
// NOTE: since tsconfig files don't inherit `references` it's safe to just get the file and extract
// `references` field instead of resolving the full tsconfig file which takes time.
const tsConfigFile = getTsConfigJSON(tsConfigPath);
const packageJsonPath = resolve(dir, 'package.json');
if (!existsSync(packageJsonPath)) {
// if `package.json` does not exists, there's nothing to check
return undefined;
}
const packageJson = loadJSON(packageJsonPath);
return {
dir,
path: projectFullPath,
projectReferences: tsConfigFile.references ?? [],
packageJson,
};
};
/**
* This script attempts to address a problem when using Typescript
* `project reference` in a monorepo project: that is the dependency
* has to be listed in both `package.json` and TS config.
* See https://github.com/microsoft/TypeScript/issues/25376
*
* Note that we only check for one-direction: that is if a Typescript
* project is listed as reference in another project, the corresponding
* package/module must be listed as a dependency in `package.json`
*
* Note that this script makes the assumption that the monorepo project
* uses project reference and organized in a particular way:
*
* - the root TS config lists all references to all child modules
* - each child module contains a TS config that lists all of its
* dependent modules by the name specified in their respective
* `package.json`
* - for each child module, `package.json` and the referenced `tsconfig.*.json`
* are in the same directory
*
* See https://github.com/RyanCavanaugh/learn-a
*/
export const checkProjectReferenceConfig = ({
rootDir,
tsConfigFileName = 'tsconfig.json',
/* micromatch glob patterns */
excludePackagePatterns = [],
excludeReferencePatterns = [],
}) => {
const errors = [];
try {
// resolve all projects referenced in the root TS config
// and build a lookup table between project and corresponding package
const rootTsConfigPath = resolve(rootDir, tsConfigFileName);
const projectIndex = new Map();
(getTsConfigJSON(rootTsConfigPath).references ?? [])
.map((ref) => ref.path)
.forEach((projectPath) => {
try {
const projectInfo = getTSProjectInfo(
rootDir,
projectPath,
tsConfigFileName,
);
const { dir, packageJson, projectReferences } = projectInfo;
if (projectInfo) {
projectIndex.set(dir, { packageJson, projectReferences });
}
} catch (e) {
errors.push(e.message);
}
});
// find all Typescript packages
const tsPackages = new Set();
const PACKAGE_JSON_PATTERN = /[\\/]package\.json$/;
const packageJsonFiles = execSync('git ls-files', { encoding: 'utf-8' })
.trim()
.split('\n')
.filter(
(file) => PACKAGE_JSON_PATTERN.test(file) && 'package.json' !== file, // omit the root `package.json`
);
packageJsonFiles.forEach((file) => {
const packageJsonPath = resolve(rootDir, `${file}`);
const dir = getDir(packageJsonPath);
const packageJson = loadJSON(packageJsonPath);
// if `tsconfig.json` file does not exists, this package is likely not written in Typescript, therefore we can skip it
// NOTE: this check seems rather optimistic, the TS config file could be named differently
// we might need to come up with a more sophisticated check (e.g. check `types` file in `package.json`)
if (!existsSync(resolve(dir, `tsconfig.json`))) {
return;
}
if (micromatch.isMatch(packageJson.name, excludePackagePatterns)) {
return;
}
// check if a package written in Typescript is not listed as a project reference in the root TS config
if (!projectIndex.has(dir)) {
errors.push(
`Project '${dir}' corresponding to package '${packageJson.name}' is not listed as a reference in root TypeScript config`,
);
} else {
tsPackages.add(packageJson.name);
}
});
projectIndex.forEach(({ packageJson, projectReferences }, dir) => {
const allDependencies = (
packageJson.dependencies ? Object.keys(packageJson.dependencies) : []
).concat(
packageJson.devDependencies
? Object.keys(packageJson.devDependencies)
: [],
);
const dependenciesToBeReferenced = new Set(
allDependencies.filter((dep) => tsPackages.has(dep)),
);
projectReferences
.map((ref) => ref.path)
.forEach((projectPath) => {
try {
const projectInfo = getTSProjectInfo(
dir,
projectPath,
tsConfigFileName,
);
if (
micromatch.isMatch(projectInfo.path, excludeReferencePatterns)
) {
return;
}
if (
tsConfigFileName !== 'tsconfig.json' &&
(!extname(projectPath) ||
tsConfigFileName !== basename(projectPath))
) {
errors.push(
`TypeScript project corresponding to package '${
packageJson.name
}' needs to reference TypeScript projects of similar types (expected: '${tsConfigFileName}', found: '${
!extname(projectPath) ? '' : basename(projectPath)
}')`,
);
}
if (!projectIndex.has(projectInfo.dir)) {
// check if a Typescript project is not listed as a reference the root TS config
errors.push(
`Root TypeScript config needs to reference TypeScript project '${projectInfo.dir}' corresponding to package '${projectInfo.packageJson.name}'`,
);
} else if (
!allDependencies.includes(projectInfo.packageJson.name)
) {
// check if a project reference is listed in TS config then its corresponding
// package must also be listed as a dependency in `package.json`
errors.push(
`Package '${packageJson.name}' needs to list package '${projectInfo.packageJson.name}' as a dependency`,
);
} else {
dependenciesToBeReferenced.delete(projectInfo.packageJson.name);
}
} catch (e) {
errors.push(`${e.message}`);
}
});
// check if a package written in Typescript that is a dependency of another package but not
// listed as a project reference in the TS config file of that dependent project
if (dependenciesToBeReferenced.size > 0) {
dependenciesToBeReferenced.forEach((dep) => {
errors.push(
`TypeScript project corresponding to package '${packageJson.name}' needs to reference TypeScript project corresponding to package '${dep}'`,
);
});
}
});
} catch (e) {
errors.push(`${e.message}`);
}
if (errors.length > 0) {
exitWithError(
`Found ${
errors.length
} issue(s) with Typescript project reference configuration:\n${errors
.map((msg) => `${chalk.red('\u2717')} ${msg}`)
.join('\n')}`,
);
} else {
console.log(
chalk.green('No issues with Typescript project reference found!'),
);
}
};