@nx/devkit
Version:
504 lines (503 loc) • 21.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NX_VERSION = void 0;
exports.getDependencyVersionFromPackageJson = getDependencyVersionFromPackageJson;
exports.addDependenciesToPackageJson = addDependenciesToPackageJson;
exports.removeDependenciesFromPackageJson = removeDependenciesFromPackageJson;
exports.ensurePackage = ensurePackage;
const fs_1 = require("fs");
const module_1 = require("module");
const devkit_exports_1 = require("nx/src/devkit-exports");
const devkit_internals_1 = require("nx/src/devkit-internals");
const path_1 = require("path");
const semver_1 = require("semver");
const install_packages_task_1 = require("../tasks/install-packages-task");
const catalog_1 = require("./catalog");
const UNIDENTIFIED_VERSION = 'UNIDENTIFIED_VERSION';
const NON_SEMVER_TAGS = {
'*': 2,
[UNIDENTIFIED_VERSION]: 2,
next: 1,
latest: 0,
previous: -1,
legacy: -2,
};
function getDependencyVersionFromPackageJson(treeOrPackageName, packageNameOrRoot, packageJsonPathOrObjectOrRoot, dependencyLookup) {
if (typeof treeOrPackageName !== 'string') {
return getDependencyVersionFromPackageJsonFromTree(treeOrPackageName, packageNameOrRoot, packageJsonPathOrObjectOrRoot, dependencyLookup);
}
else {
return getDependencyVersionFromPackageJsonFromFileSystem(treeOrPackageName, packageNameOrRoot, packageJsonPathOrObjectOrRoot, dependencyLookup);
}
}
/**
* Tree-based implementation for getDependencyVersionFromPackageJson
*/
function getDependencyVersionFromPackageJsonFromTree(tree, packageName, packageJsonPathOrObject = 'package.json', dependencyLookup = [
'dependencies',
'devDependencies',
]) {
let packageJson;
if (typeof packageJsonPathOrObject === 'object') {
packageJson = packageJsonPathOrObject;
}
else if (tree.exists(packageJsonPathOrObject)) {
packageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPathOrObject);
}
else {
return null;
}
let version = null;
for (const section of dependencyLookup) {
const foundVersion = packageJson[section]?.[packageName];
if (foundVersion) {
version = foundVersion;
break;
}
}
// Resolve catalog reference if needed
const manager = (0, catalog_1.getCatalogManager)(tree.root);
if (version && manager?.isCatalogReference(version)) {
version = manager.resolveCatalogReference(tree, packageName, version);
}
return version;
}
/**
* Filesystem-based implementation for getDependencyVersionFromPackageJson
*/
function getDependencyVersionFromPackageJsonFromFileSystem(packageName, root = devkit_exports_1.workspaceRoot, packageJsonPathOrObject = 'package.json', dependencyLookup = [
'dependencies',
'devDependencies',
]) {
let packageJson;
if (typeof packageJsonPathOrObject === 'object') {
packageJson = packageJsonPathOrObject;
}
else {
const packageJsonPath = (0, path_1.resolve)(root, packageJsonPathOrObject);
if ((0, fs_1.existsSync)(packageJsonPath)) {
packageJson = (0, devkit_exports_1.readJsonFile)(packageJsonPath);
}
else {
return null;
}
}
let version = null;
for (const section of dependencyLookup) {
const foundVersion = packageJson[section]?.[packageName];
if (foundVersion) {
version = foundVersion;
break;
}
}
// Resolve catalog reference if needed
const manager = (0, catalog_1.getCatalogManager)(root);
if (version && manager?.isCatalogReference(version)) {
version = manager.resolveCatalogReference(root, packageName, version);
}
return version;
}
function filterExistingDependencies(dependencies, existingAltDependencies) {
if (!existingAltDependencies) {
return dependencies;
}
return Object.keys(dependencies ?? {})
.filter((d) => !existingAltDependencies[d])
.reduce((acc, d) => ({ ...acc, [d]: dependencies[d] }), {});
}
function cleanSemver(tree, version, packageName) {
const manager = (0, catalog_1.getCatalogManager)(tree.root);
if (manager?.isCatalogReference(version)) {
const resolvedVersion = manager.resolveCatalogReference(tree, packageName, version);
if (!resolvedVersion) {
throw new Error(`Failed to resolve catalog reference '${version}' for package '${packageName}'`);
}
return (0, semver_1.clean)(resolvedVersion) ?? (0, semver_1.coerce)(resolvedVersion);
}
return (0, semver_1.clean)(version) ?? (0, semver_1.coerce)(version);
}
function isIncomingVersionGreater(tree, incomingVersion, existingVersion, packageName) {
// the existing version might be a catalog reference, so we need to resolve
// it if that's the case
let resolvedExistingVersion = existingVersion;
const manager = (0, catalog_1.getCatalogManager)(tree.root);
if (manager?.isCatalogReference(existingVersion)) {
const resolved = manager.resolveCatalogReference(tree, packageName, existingVersion);
if (!resolved) {
// catalog is supported, but failed to resolve, we throw an error
throw new Error(`Failed to resolve catalog reference '${existingVersion}' for package '${packageName}'`);
}
resolvedExistingVersion = resolved;
}
// if version is in the format of "latest", "next" or similar - keep it, otherwise try to parse it
const incomingVersionCompareBy = incomingVersion in NON_SEMVER_TAGS
? incomingVersion
: (cleanSemver(tree, incomingVersion, packageName)?.toString() ??
UNIDENTIFIED_VERSION);
const existingVersionCompareBy = resolvedExistingVersion in NON_SEMVER_TAGS
? resolvedExistingVersion
: (cleanSemver(tree, resolvedExistingVersion, packageName)?.toString() ??
UNIDENTIFIED_VERSION);
if (incomingVersionCompareBy in NON_SEMVER_TAGS &&
existingVersionCompareBy in NON_SEMVER_TAGS) {
return (NON_SEMVER_TAGS[incomingVersionCompareBy] >
NON_SEMVER_TAGS[existingVersionCompareBy]);
}
if (incomingVersionCompareBy in NON_SEMVER_TAGS ||
existingVersionCompareBy in NON_SEMVER_TAGS) {
return true;
}
return (0, semver_1.gt)(cleanSemver(tree, incomingVersion, packageName), cleanSemver(tree, resolvedExistingVersion, packageName));
}
function updateExistingAltDependenciesVersion(tree, dependencies, existingAltDependencies, workspaceRootPath) {
return Object.keys(existingAltDependencies || {})
.filter((d) => {
if (!dependencies[d]) {
return false;
}
const incomingVersion = dependencies[d];
const existingVersion = existingAltDependencies[d];
return isIncomingVersionGreater(tree, incomingVersion, existingVersion, d);
})
.reduce((acc, d) => ({ ...acc, [d]: dependencies[d] }), {});
}
function updateExistingDependenciesVersion(tree, dependencies, existingDependencies = {}, workspaceRootPath) {
return Object.keys(dependencies)
.filter((d) => {
if (!existingDependencies[d]) {
return true;
}
const incomingVersion = dependencies[d];
const existingVersion = existingDependencies[d];
return isIncomingVersionGreater(tree, incomingVersion, existingVersion, d);
})
.reduce((acc, d) => ({ ...acc, [d]: dependencies[d] }), {});
}
/**
* Add Dependencies and Dev Dependencies to package.json
*
* For example:
* ```typescript
* addDependenciesToPackageJson(tree, { react: 'latest' }, { jest: 'latest' })
* ```
* This will **add** `react` and `jest` to the dependencies and devDependencies sections of package.json respectively.
*
* @param tree Tree representing file system to modify
* @param dependencies Dependencies to be added to the dependencies section of package.json
* @param devDependencies Dependencies to be added to the devDependencies section of package.json
* @param packageJsonPath Path to package.json
* @param keepExistingVersions If true, prevents existing dependencies from being bumped to newer versions
* @returns Callback to install dependencies only if necessary, no-op otherwise
*/
function addDependenciesToPackageJson(tree, dependencies, devDependencies, packageJsonPath = 'package.json', keepExistingVersions) {
const currentPackageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPath);
/** Dependencies to install that are not met in dev dependencies */
let filteredDependencies = filterExistingDependencies(dependencies, currentPackageJson.devDependencies);
/** Dev dependencies to install that are not met in dependencies */
let filteredDevDependencies = filterExistingDependencies(devDependencies, currentPackageJson.dependencies);
// filtered dependencies should consist of:
// - dependencies of the same type that are not present
// by default, filtered dependencies also include these (unless keepExistingVersions is true):
// - dependencies of the same type that have greater version
// - specified dependencies of the other type that have greater version and are already installed as current type
filteredDependencies = {
...updateExistingDependenciesVersion(tree, filteredDependencies, currentPackageJson.dependencies, tree.root),
...updateExistingAltDependenciesVersion(tree, devDependencies, currentPackageJson.dependencies, tree.root),
};
filteredDevDependencies = {
...updateExistingDependenciesVersion(tree, filteredDevDependencies, currentPackageJson.devDependencies, tree.root),
...updateExistingAltDependenciesVersion(tree, dependencies, currentPackageJson.devDependencies, tree.root),
};
if (keepExistingVersions) {
filteredDependencies = removeExistingDependencies(filteredDependencies, currentPackageJson.dependencies);
filteredDevDependencies = removeExistingDependencies(filteredDevDependencies, currentPackageJson.devDependencies);
}
else {
filteredDependencies = removeLowerVersions(tree, filteredDependencies, currentPackageJson.dependencies, tree.root);
filteredDevDependencies = removeLowerVersions(tree, filteredDevDependencies, currentPackageJson.devDependencies, tree.root);
}
if (requiresAddingOfPackages(tree, currentPackageJson, filteredDependencies, filteredDevDependencies, tree.root)) {
const { catalogUpdates, directDependencies, directDevDependencies } = splitDependenciesByCatalogType(tree, filteredDependencies, filteredDevDependencies, packageJsonPath);
writeCatalogDependencies(tree, catalogUpdates);
writeDirectDependencies(tree, packageJsonPath, directDependencies, directDevDependencies);
return () => {
(0, install_packages_task_1.installPackagesTask)(tree);
};
}
return () => { };
}
function splitDependenciesByCatalogType(tree, filteredDependencies, filteredDevDependencies, packageJsonPath) {
const allFilteredUpdates = {
...filteredDependencies,
...filteredDevDependencies,
};
const catalogUpdates = [];
let directDependencies = { ...filteredDependencies };
let directDevDependencies = { ...filteredDevDependencies };
const manager = (0, catalog_1.getCatalogManager)(tree.root);
if (!manager) {
return {
catalogUpdates: [],
directDependencies: filteredDependencies,
directDevDependencies: filteredDevDependencies,
};
}
const existingCatalogDeps = (0, catalog_1.getCatalogDependenciesFromPackageJson)(tree, packageJsonPath, manager);
if (!existingCatalogDeps.size) {
return {
catalogUpdates: [],
directDependencies: filteredDependencies,
directDevDependencies: filteredDevDependencies,
};
}
// Check filtered results for catalog references or existing catalog dependencies
for (const [packageName, version] of Object.entries(allFilteredUpdates)) {
if (!existingCatalogDeps.has(packageName)) {
continue;
}
let catalogName = existingCatalogDeps.get(packageName);
const catalogRef = catalogName ? `catalog:${catalogName}` : 'catalog:';
try {
manager.validateCatalogReference(tree, packageName, catalogRef);
catalogUpdates.push({ packageName, version, catalogName });
// Remove from direct updates since this will be handled via catalog
delete directDependencies[packageName];
delete directDevDependencies[packageName];
}
catch (error) {
devkit_exports_1.output.error({
title: 'Invalid catalog reference',
bodyLines: [
`Invalid catalog reference "${catalogRef}" for package "${packageName}".`,
error.message,
],
});
throw new Error(`Could not update "${packageName}" to version "${version}". See above for more details.`);
}
}
return { catalogUpdates, directDependencies, directDevDependencies };
}
function writeCatalogDependencies(tree, catalogUpdates) {
if (!catalogUpdates.length) {
return;
}
const manager = (0, catalog_1.getCatalogManager)(tree.root);
manager.updateCatalogVersions(tree, catalogUpdates);
}
function writeDirectDependencies(tree, packageJsonPath, dependencies, devDependencies) {
(0, devkit_exports_1.updateJson)(tree, packageJsonPath, (json) => {
json.dependencies = {
...(json.dependencies || {}),
...dependencies,
};
json.devDependencies = {
...(json.devDependencies || {}),
...devDependencies,
};
json.dependencies = sortObjectByKeys(json.dependencies);
json.devDependencies = sortObjectByKeys(json.devDependencies);
return json;
});
}
/**
* @returns The the incoming dependencies that are higher than the existing verions
**/
function removeLowerVersions(tree, incomingDeps, existingDeps, workspaceRootPath) {
return Object.keys(incomingDeps).reduce((acc, d) => {
if (!existingDeps?.[d] ||
isIncomingVersionGreater(tree, incomingDeps[d], existingDeps[d], d)) {
acc[d] = incomingDeps[d];
}
return acc;
}, {});
}
function removeExistingDependencies(incomingDeps, existingDeps) {
return Object.keys(incomingDeps).reduce((acc, d) => {
if (!existingDeps?.[d]) {
acc[d] = incomingDeps[d];
}
return acc;
}, {});
}
/**
* Remove Dependencies and Dev Dependencies from package.json
*
* For example:
* ```typescript
* removeDependenciesFromPackageJson(tree, ['react'], ['jest'])
* ```
* This will **remove** `react` and `jest` from the dependencies and devDependencies sections of package.json respectively.
*
* @param dependencies Dependencies to be removed from the dependencies section of package.json
* @param devDependencies Dependencies to be removed from the devDependencies section of package.json
* @returns Callback to uninstall dependencies only if necessary. undefined is returned if changes are not necessary.
*/
function removeDependenciesFromPackageJson(tree, dependencies, devDependencies, packageJsonPath = 'package.json') {
const currentPackageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPath);
if (requiresRemovingOfPackages(currentPackageJson, dependencies, devDependencies)) {
(0, devkit_exports_1.updateJson)(tree, packageJsonPath, (json) => {
if (json.dependencies) {
for (const dep of dependencies) {
delete json.dependencies[dep];
}
json.dependencies = sortObjectByKeys(json.dependencies);
}
if (json.devDependencies) {
for (const devDep of devDependencies) {
delete json.devDependencies[devDep];
}
json.devDependencies = sortObjectByKeys(json.devDependencies);
}
return json;
});
}
return () => {
(0, install_packages_task_1.installPackagesTask)(tree);
};
}
function sortObjectByKeys(obj) {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
return obj;
}
return Object.keys(obj)
.sort()
.reduce((result, key) => {
return {
...result,
[key]: obj[key],
};
}, {});
}
/**
* Verifies whether the given packageJson dependencies require an update
* given the deps & devDeps passed in
*/
function requiresAddingOfPackages(tree, packageJsonFile, deps, devDeps, workspaceRootPath) {
let needsDepsUpdate = false;
let needsDevDepsUpdate = false;
packageJsonFile.dependencies = packageJsonFile.dependencies || {};
packageJsonFile.devDependencies = packageJsonFile.devDependencies || {};
if (Object.keys(deps).length > 0) {
needsDepsUpdate = Object.keys(deps).some((entry) => {
const incomingVersion = deps[entry];
if (packageJsonFile.dependencies[entry]) {
const existingVersion = packageJsonFile.dependencies[entry];
return isIncomingVersionGreater(tree, incomingVersion, existingVersion, entry);
}
if (packageJsonFile.devDependencies[entry]) {
const existingVersion = packageJsonFile.devDependencies[entry];
return isIncomingVersionGreater(tree, incomingVersion, existingVersion, entry);
}
return true;
});
}
if (Object.keys(devDeps).length > 0) {
needsDevDepsUpdate = Object.keys(devDeps).some((entry) => {
const incomingVersion = devDeps[entry];
if (packageJsonFile.devDependencies[entry]) {
const existingVersion = packageJsonFile.devDependencies[entry];
return isIncomingVersionGreater(tree, incomingVersion, existingVersion, entry);
}
if (packageJsonFile.dependencies[entry]) {
const existingVersion = packageJsonFile.dependencies[entry];
return isIncomingVersionGreater(tree, incomingVersion, existingVersion, entry);
}
return true;
});
}
return needsDepsUpdate || needsDevDepsUpdate;
}
/**
* Verifies whether the given packageJson dependencies require an update
* given the deps & devDeps passed in
*/
function requiresRemovingOfPackages(packageJsonFile, deps, devDeps) {
let needsDepsUpdate = false;
let needsDevDepsUpdate = false;
packageJsonFile.dependencies = packageJsonFile.dependencies || {};
packageJsonFile.devDependencies = packageJsonFile.devDependencies || {};
if (deps.length > 0) {
needsDepsUpdate = deps.some((entry) => packageJsonFile.dependencies[entry]);
}
if (devDeps.length > 0) {
needsDevDepsUpdate = devDeps.some((entry) => packageJsonFile.devDependencies[entry]);
}
return needsDepsUpdate || needsDevDepsUpdate;
}
const packageMapCache = new Map();
function ensurePackage(pkgOrTree, requiredVersionOrPackage, maybeRequiredVersion, _) {
let pkg;
let requiredVersion;
if (typeof pkgOrTree === 'string') {
pkg = pkgOrTree;
requiredVersion = requiredVersionOrPackage;
}
else {
// Old Signature
pkg = requiredVersionOrPackage;
requiredVersion = maybeRequiredVersion;
}
if (packageMapCache.has(pkg)) {
return packageMapCache.get(pkg);
}
try {
return require(pkg);
}
catch (e) {
if (e.code === 'ERR_REQUIRE_ESM') {
// The package is installed, but is an ESM package.
// The consumer of this function can import it as needed.
return null;
}
else if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
}
if (process.env.NX_DRY_RUN && process.env.NX_DRY_RUN !== 'false') {
throw new Error('NOTE: This generator does not support --dry-run. If you are running this in Nx Console, it should execute fine once you hit the "Generate" button.\n');
}
const { tempDir } = (0, devkit_internals_1.installPackageToTmp)(pkg, requiredVersion);
addToNodePath((0, path_1.join)(devkit_exports_1.workspaceRoot, 'node_modules'));
addToNodePath((0, path_1.join)(tempDir, 'node_modules'));
// Re-initialize the added paths into require
module_1.Module._initPaths();
try {
const result = require(require.resolve(pkg, {
paths: [tempDir],
}));
packageMapCache.set(pkg, result);
return result;
}
catch (e) {
if (e.code === 'ERR_REQUIRE_ESM') {
// The package is installed, but is an ESM package.
// The consumer of this function can import it as needed.
packageMapCache.set(pkg, null);
return null;
}
throw e;
}
}
function addToNodePath(dir) {
// NODE_PATH is a delimited list of paths.
// The delimiter is different for windows.
const delimiter = require('os').platform() === 'win32' ? ';' : ':';
const paths = process.env.NODE_PATH
? process.env.NODE_PATH.split(delimiter)
: [];
// The path is already in the node path
if (paths.includes(dir)) {
return;
}
// Add the tmp path
paths.push(dir);
// Update the env variable.
process.env.NODE_PATH = paths.join(delimiter);
}
function getInstalledPackageModuleVersion(pkg) {
return require((0, path_1.join)(pkg, 'package.json')).version;
}
/**
* @description The version of Nx used by the workspace. Returns null if no version is found.
*/
exports.NX_VERSION = getInstalledPackageModuleVersion('nx');