@monorepo-utils/workspaces-to-typescript-project-references
Version:
Convert Workspaces to TypeScript's Project References
285 lines (278 loc) • 12.5 kB
JavaScript
import fs from "fs";
import path from "upath";
import commentJSON from "comment-json";
import { plugin as workspacesPlugin } from "./manager/workspaces";
import assert from "assert";
export const DEFAULT_TSCONFIGPATH = "tsconfig.json";
const sortReferences = (references) => {
return references.slice().sort((refA, refB) => (refA.path > refB.path ? 1 : -1));
};
const getReferenceInPackage = (tsconfigFilePath) => {
try {
const dirPath = path.dirname(tsconfigFilePath);
const fileName = path.basename(tsconfigFilePath);
const files = fs.readdirSync(dirPath);
const tsconfigFilePaths = files
.filter(file => file !== fileName && file.startsWith('tsconfig.') && file.endsWith('.json'))
.map(file => ({ path: `./${file}` }));
return tsconfigFilePaths;
}
catch (err) {
return [];
}
};
// when tsconfig.json → ../path/to/dir
// when tsconfig.x.json → ../path/to/dir/tsconfig.x.json
const getReferencePathToTsConfig = (options) => {
var _a;
const tsConfigPath = (_a = options.tsConfigPath) !== null && _a !== void 0 ? _a : DEFAULT_TSCONFIGPATH;
const tsConfigDirName = path.dirname(tsConfigPath);
const pathComponents = [options.packageLocation, tsConfigDirName];
// If the file name is not tsconfig.json (the default),
// then append it to the generated path
const tsConfigFileName = path.basename(tsConfigPath);
if (tsConfigFileName !== DEFAULT_TSCONFIGPATH) {
pathComponents.push(tsConfigFileName);
}
return path.join(...pathComponents);
};
const validateDuplicatedDependencies = (packageReferences) => {
const packageNameSet = new Set();
const errors = [];
packageReferences.forEach((ref) => {
if (packageNameSet.has(ref.name)) {
errors.push(new Error(`This package is deduplicated in dependencies and devDependencies: ${ref.name}
Please remove duplicate dependencies from dependencies or devDependencies.`));
return;
}
packageNameSet.add(ref.name);
});
return {
ok: errors.length === 0,
errors
};
};
export const toProjectReferences = (options) => {
const plugins = Array.isArray(options.plugins) && options.plugins.length > 0 ? options.plugins : [workspacesPlugin];
const pluginImplementations = plugins.map((plugin) => plugin(options));
// use first plugin
const supportPlugin = pluginImplementations.find((plugin) => {
return plugin.supports();
});
if (!supportPlugin) {
throw new Error("Not found supported plugin");
}
const relativeName = (pkgPath) => {
return path.relative(options.rootDir, pkgPath);
};
const allPackages = supportPlugin.getAllPackages();
const errors = [];
const canNotRecoverErrors = [];
allPackages.forEach((packageInfo) => {
var _a, _b, _c;
const tsconfigFilePath = (_b = (_a = options.tsConfigPathFinder) === null || _a === void 0 ? void 0 : _a.call(options, packageInfo.location)) !== null && _b !== void 0 ? _b : path.join(packageInfo.location, DEFAULT_TSCONFIGPATH);
if (!fs.existsSync(tsconfigFilePath)) {
// Skip has not tsconfig.json
return;
}
const tsconfigJSON = commentJSON.parse(fs.readFileSync(tsconfigFilePath, "utf-8"));
const references = supportPlugin.getDependencies(packageInfo.packageJSON);
const resolvedReferences = references
.map((reference) => {
const absolutePathOrNull = supportPlugin.resolve(reference);
if (!absolutePathOrNull) {
return;
}
if (!path.isAbsolute(absolutePathOrNull)) {
throw new Error(`Plugin#resolve should return absolute path: ${absolutePathOrNull}, plugin: ${supportPlugin}`);
}
return {
name: reference.name,
version: reference.version,
absolutePath: absolutePathOrNull
};
})
.filter((r) => Boolean(r));
const referenceValidateResult = validateDuplicatedDependencies(resolvedReferences);
// if found an error, do early return: https://github.com/azu/monorepo-utils/issues/56
if (!referenceValidateResult.ok) {
canNotRecoverErrors.push(...referenceValidateResult.errors);
return;
}
const newProjectReferences = resolvedReferences
.map((reference) => {
var _a;
const absolutePath = reference.absolutePath;
// Should ignore no-ts package
// https://github.com/azu/monorepo-utils/issues/53
const referencePackageTsConfigFilePath = path.resolve(absolutePath, (_a = options.tsConfigPath) !== null && _a !== void 0 ? _a : DEFAULT_TSCONFIGPATH);
if (!fs.existsSync(referencePackageTsConfigFilePath)) {
return;
}
// { path } value
const referenceTsConfigPath = getReferencePathToTsConfig({
packageLocation: absolutePath,
tsConfigPath: options.tsConfigPath
});
if (packageInfo.location === referenceTsConfigPath) {
const selfName = relativeName(packageInfo.location);
errors.push(new Error(`[${selfName}] Self dependencies is something wrong: ${selfName} refer to ${relativeName(referenceTsConfigPath)}`));
}
const basePackageLocation = path.dirname(path.resolve(packageInfo.location, tsconfigFilePath));
return {
path: path.relative(basePackageLocation, referenceTsConfigPath)
};
})
.filter((r) => Boolean(r));
if (options.includesLocal) {
const referenceInLocal = getReferenceInPackage(tsconfigFilePath);
newProjectReferences.push(...referenceInLocal);
}
const currentProjectReferences = (_c = tsconfigJSON["references"]) !== null && _c !== void 0 ? _c : [];
if (options.checkOnly) {
// check
try {
// create pure object for compare
const cleanCurrentProjectReferences = JSON.parse(JSON.stringify(commentJSON.parse(commentJSON.stringify(currentProjectReferences), undefined, true)));
// TODO: move sorting to updating logic when next major release
// https://github.com/azu/monorepo-utils/issues/44
assert.deepStrictEqual(sortReferences(cleanCurrentProjectReferences), sortReferences(newProjectReferences));
}
catch (error) {
const selfName = relativeName(packageInfo.location);
errors.push(new Error(`[${selfName}] ${error.message}`));
}
return;
}
else {
// update
if (errors.length === 0) {
if (currentProjectReferences.length === 0 && newProjectReferences.length === 0) {
// As to not add `references: []` needlessly
return;
}
tsconfigJSON["references"] = newProjectReferences;
const oldContents = commentJSON.stringify(commentJSON.parse(fs.readFileSync(tsconfigFilePath, { encoding: "utf-8" })), null, 2);
const newContents = commentJSON.stringify(tsconfigJSON, null, 2);
if (newContents !== oldContents) {
fs.writeFileSync(tsconfigFilePath, `${newContents}\n`, "utf-8");
}
}
}
});
if (canNotRecoverErrors.length > 0) {
return {
ok: false,
aggregateError: {
message: `workspaces-to-typescript-project-references found ${canNotRecoverErrors.length} errors.
- ${canNotRecoverErrors.map((error) => error.message.split("\n")[0]).join("\n- ")}
Please resolve these error before updates tsconfig.json
`,
errors: canNotRecoverErrors
}
};
}
if (errors.length > 0) {
return {
ok: false,
aggregateError: {
message: `workspaces-to-typescript-project-references found ${errors.length} errors.
Please update your tsconfig.json via following command.
$ workspaces-to-typescript-project-references
`,
errors
}
};
}
return {
ok: true
};
};
export const toRootProjectReferences = (options) => {
var _a, _b, _c, _d;
const plugins = Array.isArray(options.plugins) && options.plugins.length > 0 ? options.plugins : [workspacesPlugin];
const pluginImplementations = plugins.map((plugin) => plugin(options));
// use first plugin
const supportPlugin = pluginImplementations.find((plugin) => {
return plugin.supports();
});
if (!supportPlugin) {
throw new Error("Not found supported plugin");
}
const allPackages = supportPlugin.getAllPackages();
const errors = [];
const rootTsconfigFilePath = (_b = (_a = options.tsConfigPathFinder) === null || _a === void 0 ? void 0 : _a.call(options, options.rootDir)) !== null && _b !== void 0 ? _b : path.join(options.rootDir, DEFAULT_TSCONFIGPATH);
if (!fs.existsSync(rootTsconfigFilePath)) {
const tsConfigPath = (_c = options.tsConfigPath) !== null && _c !== void 0 ? _c : DEFAULT_TSCONFIGPATH;
return {
ok: false,
aggregateError: {
message: `Not found ${tsConfigPath} in ${rootTsconfigFilePath}`,
errors
}
};
}
const tsconfigJSON = commentJSON.parse(fs.readFileSync(rootTsconfigFilePath, "utf-8"));
const projectReferences = allPackages
.filter((pkg) => {
var _a, _b;
const tsconfigFilePath = (_b = (_a = options.tsConfigPathFinder) === null || _a === void 0 ? void 0 : _a.call(options, pkg.location)) !== null && _b !== void 0 ? _b : path.join(pkg.location, DEFAULT_TSCONFIGPATH);
// Skip if the package has not tsconfig.json
return fs.existsSync(tsconfigFilePath);
})
.map((pkg) => {
var _a;
const tsConfigPath = (_a = options.tsConfigPath) !== null && _a !== void 0 ? _a : DEFAULT_TSCONFIGPATH;
const pathComponents = [path.relative(options.rootDir, pkg.location)];
// If the file name is not tsconfig.json (the default),
// then append it to the generated path
const tsConfigFileName = path.basename(tsConfigPath);
if (tsConfigFileName !== DEFAULT_TSCONFIGPATH) {
pathComponents.push(tsConfigFileName);
}
return {
path: path.join(...pathComponents)
};
});
if (options.includesLocal) {
const referenceInLocal = getReferenceInPackage(rootTsconfigFilePath);
projectReferences.push(...referenceInLocal);
}
if (options.checkOnly) {
// check
try {
const currentProjectReferences = (_d = tsconfigJSON["references"]) !== null && _d !== void 0 ? _d : [];
const cleanCurrentProjectReferences = JSON.parse(JSON.stringify(commentJSON.parse(commentJSON.stringify(currentProjectReferences), undefined, true)));
// TODO: move sorting to updating logic when next major release
// https://github.com/azu/monorepo-utils/issues/44
assert.deepStrictEqual(sortReferences(cleanCurrentProjectReferences), sortReferences(projectReferences));
}
catch (error) {
errors.push(new Error(`[root] ${error.message}`));
}
}
else {
const updatedTsConfigJSON = Object.assign(Object.assign({}, tsconfigJSON), { references: projectReferences });
const oldContents = commentJSON.stringify(projectReferences, null, 2);
const newContents = commentJSON.stringify(updatedTsConfigJSON, null, 2);
if (newContents !== oldContents) {
fs.writeFileSync(rootTsconfigFilePath, `${newContents}\n`, "utf-8");
}
}
if (errors.length > 0) {
return {
ok: false,
aggregateError: {
message: `workspaces-to-typescript-project-references found ${errors.length} errors in root tsconfig.
Please update your <root>/tsconfig.json via following command.
$ workspaces-to-typescript-project-references --includesRoot
`,
errors
}
};
}
return {
ok: true
};
};
//# sourceMappingURL=index.js.map