@nx/devkit
Version:
216 lines (215 loc) • 9.28 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.readCatalogConfigFromFs = readCatalogConfigFromFs;
exports.readCatalogConfigFromTree = readCatalogConfigFromTree;
exports.updateCatalogVersionsInFile = updateCatalogVersionsInFile;
// Keep in sync with packages/nx/src/utils/catalog/manager-utils.ts; the body below
// the imports is duplicated because @nx/devkit supports a range of nx majors and
// this logic isn't part of the nx surface it can import across that range.
const js_yaml_1 = require("@zkochan/js-yaml");
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const devkit_exports_1 = require("nx/src/devkit-exports");
const devkit_internals_1 = require("nx/src/devkit-internals");
const yaml_1 = require("yaml");
// Walks `path` resolving aliases at every step so structural checks can
// see through `key: *anchor` references. Neither `doc.getIn` nor
// `doc.hasIn` traverse aliases on their own. Returns the resolved node, or
// `undefined` if any ancestor isn't a map.
function resolveAt(doc, path) {
let node = doc.contents;
for (let i = 0; i < path.length; i++) {
if ((0, yaml_1.isAlias)(node))
node = node.resolve(doc);
if (!(0, yaml_1.isMap)(node))
return undefined;
node = node.get(path[i], true);
}
if ((0, yaml_1.isAlias)(node))
node = node.resolve(doc);
return node;
}
function isMapAt(doc, path) {
return (0, yaml_1.isMap)(resolveAt(doc, path));
}
function existsAt(doc, path) {
return resolveAt(doc, path) !== undefined;
}
// `doc.getIn` does not traverse aliases. For aliased paths it returns
// undefined even when the resolved node has a value. Use the alias-aware
// walk and unwrap scalar nodes to their JS value.
function getValueAt(doc, path) {
const node = resolveAt(doc, path);
return (0, yaml_1.isScalar)(node) ? node.value : node;
}
// Walks `targetPath` resolving aliases at every step and mutates the
// resolved map directly so anchors are preserved. When a step is missing
// or holds a null placeholder (`key:` with no value), creates a fresh map
// inside the current parent and carries over the placeholder's comments
// and anchor so they aren't dropped. A dropped anchor would leave any
// alias referencing it unresolved and make `String(doc)` throw. Falls back
// to `doc.setIn` when the current parent isn't a map: an empty-document
// root gets bootstrapped, while a non-map value where a catalog map was
// expected makes `setIn` throw, surfacing the malformed config.
function setThroughAliases(doc, targetPath, value) {
let parent = doc.contents;
if ((0, yaml_1.isAlias)(parent))
parent = parent.resolve(doc);
for (let i = 0; i < targetPath.length - 1; i++) {
if (!(0, yaml_1.isMap)(parent)) {
doc.setIn(targetPath, value);
return;
}
let next = parent.get(targetPath[i], true);
const nextWasAlias = (0, yaml_1.isAlias)(next);
if ((0, yaml_1.isAlias)(next))
next = next.resolve(doc);
const placeholder = (0, yaml_1.isScalar)(next) && next.value === null ? next : undefined;
if (next === undefined || placeholder) {
let cur = parent;
for (let j = i; j < targetPath.length - 1; j++) {
const fresh = new yaml_1.YAMLMap();
if (j === i && placeholder) {
if (placeholder.comment)
fresh.comment = placeholder.comment;
if (placeholder.commentBefore)
fresh.commentBefore = placeholder.commentBefore;
// Copy the anchor only for a directly-held placeholder. When it
// was reached through an alias, the anchor still lives on the
// definition node, so re-emitting it here would duplicate it.
if (!nextWasAlias && placeholder.anchor)
fresh.anchor = placeholder.anchor;
}
cur.set(targetPath[j], fresh);
cur = fresh;
}
cur.set(targetPath[targetPath.length - 1], value);
return;
}
parent = next;
}
if (!(0, yaml_1.isMap)(parent)) {
doc.setIn(targetPath, value);
return;
}
parent.set(targetPath[targetPath.length - 1], value);
}
function readCatalogConfigFromFs(filename, fullPath) {
try {
return (0, devkit_internals_1.readYamlFile)(fullPath);
}
catch (error) {
devkit_exports_1.output.warn({
title: `Unable to parse ${filename}`,
bodyLines: [error.toString()],
});
return null;
}
}
function readCatalogConfigFromTree(filename, tree) {
const content = tree.read(filename, 'utf-8');
try {
return (0, js_yaml_1.load)(content, { filename });
}
catch (error) {
devkit_exports_1.output.warn({
title: `Unable to parse ${filename}`,
bodyLines: [error.toString()],
});
return null;
}
}
function updateCatalogVersionsInFile(filename, treeOrRoot, updates) {
let checkExists;
let readYaml;
let writeYaml;
if (typeof treeOrRoot === 'string') {
const configPath = (0, node_path_1.join)(treeOrRoot, filename);
checkExists = () => (0, node_fs_1.existsSync)(configPath);
readYaml = () => (0, node_fs_1.readFileSync)(configPath, 'utf-8');
writeYaml = (content) => (0, node_fs_1.writeFileSync)(configPath, content, 'utf-8');
}
else {
checkExists = () => treeOrRoot.exists(filename);
readYaml = () => treeOrRoot.read(filename, 'utf-8');
writeYaml = (content) => treeOrRoot.write(filename, content);
}
if (!checkExists()) {
devkit_exports_1.output.warn({
title: `No ${filename} found`,
bodyLines: [
`Cannot update catalog versions without a ${filename} file.`,
`Create a ${filename} file to use catalogs.`,
],
});
return;
}
try {
// parseDocument keeps comments and anchors so a catalog bump doesn't
// rewrite the user's config file.
const lineCounter = new yaml_1.LineCounter();
const doc = (0, yaml_1.parseDocument)(readYaml(), { lineCounter });
// parseDocument collects syntax errors instead of throwing; surface them
// now, with their line/column detail, rather than failing later in
// `String(doc)` with a generic message or skipping a no-op write on a
// malformed file (the previous js-yaml `load()` threw here).
if (doc.errors.length > 0) {
throw new Error(doc.errors.map((e) => e.message).join('\n'));
}
// A dangling alias (`*ref` with no matching `&ref`) is not a syntax error,
// so parseDocument leaves it out of doc.errors. Surface it here with the
// same line/column detail the old js-yaml `load()` reported; otherwise a
// broken reference is silently overwritten or written back untouched.
const unresolvedAliases = [];
(0, yaml_1.visit)(doc, {
Alias(_key, node) {
if (node.resolve(doc) !== undefined)
return;
const pos = node.range ? lineCounter.linePos(node.range[0]) : undefined;
unresolvedAliases.push(pos
? `Unresolved alias "${node.source}" at line ${pos.line}, column ${pos.col}`
: `Unresolved alias "${node.source}"`);
},
});
if (unresolvedAliases.length > 0) {
throw new Error(unresolvedAliases.join('\n'));
}
let hasChanges = false;
for (const update of updates) {
const { packageName, version, catalogName } = update;
const normalizedCatalogName = catalogName === 'default' ? undefined : catalogName;
let targetPath;
if (!normalizedCatalogName) {
// An empty `catalog:` placeholder must not claim the default route
// when `catalogs.default` is populated; that would create a
// duplicate-default config rejected by pnpm.
if (isMapAt(doc, ['catalog'])) {
targetPath = ['catalog', packageName];
}
else if (existsAt(doc, ['catalogs', 'default'])) {
targetPath = ['catalogs', 'default', packageName];
}
else {
targetPath = ['catalog', packageName];
}
}
else {
targetPath = ['catalogs', normalizedCatalogName, packageName];
}
if (getValueAt(doc, targetPath) !== version) {
setThroughAliases(doc, targetPath, version);
hasChanges = true;
}
}
if (hasChanges) {
writeYaml(String(doc));
}
}
catch (error) {
devkit_exports_1.output.error({
title: 'Failed to update catalog versions',
bodyLines: [error instanceof Error ? error.message : String(error)],
});
throw error;
}
}