@decaf-ts/utils
Version:
module management utils for decaf-ts
308 lines • 13.3 kB
JavaScript
;
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