UNPKG

@decaf-ts/utils

Version:

module management utils for decaf-ts

308 lines 13.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NpmLinkCommand = void 0; const node_fs_1 = __importDefault(require("node:fs")); const node_path_1 = __importDefault(require("node:path")); const node_child_process_1 = require("node:child_process"); const command_js_1 = require("./../command.cjs"); const modules_command_js_1 = require("./modules.command.cjs"); const help_command_js_1 = require("./help.command.cjs"); const DEFAULT_EXCLUDES = ["@decaf-ts/utils", "@decaf-ts/logging"]; const options = { maxTraversal: { type: "string", default: "2", }, excludes: { type: "string", multiple: true, }, include: { type: "string", multiple: true, default: [], }, packages: { type: "string", multiple: true, default: [], }, mainPackagePath: { type: "string", default: "", }, operation: { type: "string", default: "link", }, }; function getScope(packageName) { return packageName.split("/")[0] || ""; } function getPackageName(packageName) { return packageName.split("/")[1] || packageName; } function getDependencyList(pkg) { return [ ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {}), ...Object.keys(pkg.peerDependencies || {}), ]; } function normalizeList(value) { if (Array.isArray(value)) { return value.map((item) => `${item}`.trim()).filter(Boolean); } if (typeof value === "string" && value.length > 0) { return value .split(",") .map((item) => item.trim()) .filter(Boolean); } return []; } function matchesPattern(value, pattern) { if (pattern.includes("*")) { const escaped = pattern .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") .replace(/\\\*/g, ".*"); const regex = new RegExp(`^${escaped}$`); return regex.test(value); } return (value === pattern || node_path_1.default.basename(value) === pattern || value.endsWith(`/${pattern}`)); } function readPackageJson(filePath) { try { return JSON.parse(node_fs_1.default.readFileSync(filePath, "utf8")); } catch { return undefined; } } function readInstalledPackages(moduleRoot) { const lockPath = node_path_1.default.join(moduleRoot, "package-lock.json"); let lock; try { lock = JSON.parse(node_fs_1.default.readFileSync(lockPath, "utf8")); } catch { return []; } const packages = lock.packages; if (!packages) return []; const prefix = "node_modules/"; return Object.keys(packages) .filter((key) => key.startsWith(prefix) && !key.slice(prefix.length).includes("/node_modules/")) .map((key) => key.slice(prefix.length)); } function isWithin(parent, candidate) { const rel = node_path_1.default.relative(parent, candidate); return !rel || (!rel.startsWith("..") && !node_path_1.default.isAbsolute(rel)); } class NpmLinkCommand extends command_js_1.Command { constructor() { super("NpmLinkCommand", options); } help() { (0, help_command_js_1.printCommandHelp)(this.log, "npm-link", "Link or unlink package outputs across git submodule workspaces.", "npm-link [options]", [ { flag: "--maxTraversal <depth>", description: "How many nested .gitmodules levels to traverse", defaultValue: "2", }, { flag: "--excludes <items...>", description: "Dependency names or patterns to ignore. Pass an empty string to clear the default excludes", defaultValue: "@decaf-ts/utils,@decaf-ts/logging", }, { flag: "--include <items...>", description: "Module names or paths to target explicitly", }, { flag: "--packages <items...>", description: "Additional non-scoped packages to link from --mainPackagePath", }, { flag: "--mainPackagePath <path>", description: "Source root for --packages dependencies (e.g. brain/node_modules/@decaf-ts)", }, { flag: "--operation <name>", description: "Operation to run in each module", defaultValue: "link", }, { flag: "-h, --help", description: "Show this help text and exit", }, ], [ "link symlinks each discovered dependency to its local source", "candidates are gathered from both package.json and package-lock.json (catches transitive deps)", "scoped dependencies are resolved to their git submodule path by matching the package name", "non-scoped dependencies passed via --packages are resolved from --mainPackagePath", "dependencies whose source lives inside the consuming module are skipped (self-reference)", "unlink removes those links and reinstalls dependencies via npm run do-install", "any other operation is passed through to npm in each selected module", ], [ "npm-link --operation link", "npm-link --operation unlink", "npm-link --packages @decaf-ts/* --mainPackagePath brain/node_modules/@decaf-ts --excludes @pdmfcsa/*", "npm-link --operation install --include modules/core", ]); } async run(answers) { const maxTraversal = Number.parseInt(`${answers.maxTraversal || "2"}`, 10); const include = normalizeList(answers.include); const packages = normalizeList(answers.packages); const mainPackagePath = `${answers.mainPackagePath || ""}`.trim(); const operation = `${answers.operation || "link"}`.trim() || "link"; const excludesRaw = answers.excludes; const excludesProvided = excludesRaw !== undefined && excludesRaw !== null; const excludes = excludesProvided ? normalizeList(excludesRaw) : []; const effectiveExcludes = excludesProvided ? excludes : DEFAULT_EXCLUDES; const sourceBasePath = mainPackagePath ? node_path_1.default.resolve(mainPackagePath) : process.cwd(); if (packages.length > 0 && !mainPackagePath) { console.log("--main-package-path is required when --packages includes non-scoped packages"); process.exit(1); return; } const outerPkg = readPackageJson(node_path_1.default.join(process.cwd(), "package.json")); if (!outerPkg || !outerPkg.name) { console.log("Could not determine the workspace package name"); process.exit(1); return; } const scope = getScope(outerPkg.name); const modules = (0, modules_command_js_1.readGitModulesDeep)(process.cwd(), Number.isFinite(maxTraversal) ? maxTraversal : 2); const moduleByName = new Map(); for (const moduleName of modules) { const moduleRoot = node_path_1.default.join(process.cwd(), moduleName); const mpkg = readPackageJson(node_path_1.default.join(moduleRoot, "package.json")); if (mpkg && mpkg.name) { moduleByName.set(mpkg.name, moduleName); } } const selectedModules = modules.filter((moduleName) => include.length > 0 ? include.some((pattern) => matchesPattern(moduleName, pattern)) : true); const shouldIgnoreDependency = (dependency) => effectiveExcludes.some((pattern) => matchesPattern(dependency, pattern)); const shouldLinkDependency = (dependency) => dependency.startsWith(scope) || packages.some((pattern) => matchesPattern(dependency, pattern)); const resolveSource = (dependency) => { const packageName = getPackageName(dependency); if (dependency.startsWith(scope)) { const modulePath = moduleByName.get(dependency); if (modulePath) { return node_path_1.default.join(process.cwd(), modulePath); } return node_path_1.default.join(sourceBasePath, packageName); } return node_path_1.default.join(sourceBasePath, packageName); }; for (const moduleName of selectedModules) { const moduleRoot = node_path_1.default.join(process.cwd(), moduleName); const pkg = readPackageJson(node_path_1.default.join(moduleRoot, "package.json")); if (!pkg) continue; const candidates = Array.from(new Set([...getDependencyList(pkg), ...readInstalledPackages(moduleRoot)])); const dependencies = candidates.filter((dep) => shouldLinkDependency(dep)); if (operation === "link") { for (const dependency of dependencies) { if (shouldIgnoreDependency(dependency)) continue; const innerCodePath = dependency.endsWith("styles") ? "dist" : "lib"; const sourceDir = resolveSource(dependency); if (!sourceDir) { console.log(`Skipping ${dependency} - could not resolve source`); continue; } const sourcePath = node_path_1.default.join(sourceDir, innerCodePath); if (!node_fs_1.default.existsSync(sourcePath)) { console.log(`Skipping ${dependency} as ${sourcePath} does not exist in the local workspace`); continue; } if (isWithin(moduleRoot, sourceDir)) { console.log(`Skipping ${dependency} in ${moduleName} - source is the module itself`); continue; } const depRoot = node_path_1.default.join(moduleRoot, "node_modules", dependency); const linkPath = node_path_1.default.join(depRoot, innerCodePath); try { console.log(`linking ${dependency} as a dependency of ${moduleName}`); // If the package root is itself a symlink (e.g. from a previous // whole-directory link), remove it so we restore the installed package try { if (node_fs_1.default.lstatSync(depRoot).isSymbolicLink()) { node_fs_1.default.rmSync(depRoot, { force: true, recursive: true }); } } catch { // depRoot doesn't exist — that's fine } node_fs_1.default.mkdirSync(depRoot, { recursive: true }); node_fs_1.default.rmSync(linkPath, { force: true, recursive: true }); node_fs_1.default.symlinkSync(node_path_1.default.relative(node_path_1.default.dirname(linkPath), sourcePath), linkPath, "dir"); } catch (error) { console.log(`Failed to link ${dependency} as a dependency of ${moduleName}: ${error}`); process.exit(1); } } continue; } if (operation === "unlink") { for (const dependency of dependencies) { if (shouldIgnoreDependency(dependency)) continue; console.log(`unlinking ${dependency} as a dependency of ${moduleName}`); try { node_fs_1.default.rmSync(node_path_1.default.join(moduleRoot, "node_modules", dependency), { force: true, recursive: true, }); } catch { process.exit(1); } } try { (0, node_child_process_1.execSync)("npm run do-install", { cwd: moduleRoot, env: process.env, stdio: "inherit", }); } catch { process.exit(1); } continue; } console.log(`${operation}ing ${moduleName}`); try { (0, node_child_process_1.execSync)(`npm ${operation}`, { cwd: moduleRoot, env: process.env, stdio: "inherit", }); } catch { process.exit(1); } } } } exports.NpmLinkCommand = NpmLinkCommand; //# sourceMappingURL=npm-link.command.js.map //# sourceMappingURL=npm-link.command.cjs.map