UNPKG

@nx/devkit

Version:

The Nx Devkit is used to customize Nx for different technologies and use cases. It contains many utility functions for reading and writing files, updating configuration, working with Abstract Syntax Trees(ASTs), and more. Learn more about [extending Nx by

216 lines (215 loc) • 9.28 kB
"use strict"; 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; } }