UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

224 lines (223 loc) 9.58 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.hoistWorkspacePnpmConfig = hoistWorkspacePnpmConfig; const fs_1 = require("fs"); const js_yaml_1 = require("js-yaml"); /** * pnpm workspace-scoped fields that must live at the workspace root. * When present in sub-project package.json files, pnpm emits: * * WARN The field "<field>" was found in <path>. This will not take * effect. You should configure "<field>" at the root of the workspace * instead. * * Crucially, the WARN also means the values are silently ignored — CVE * overrides defined only in projects/api/package.json never reach the * install resolver. Hoisting them to the root fixes both the warning * and the actual dependency-resolution behavior. */ const WORKSPACE_SCOPED_PNPM_FIELDS = ['overrides', 'onlyBuiltDependencies', 'ignoredOptionalDependencies']; /** * pnpm 11 renamed `onlyBuiltDependencies` (string array) to `allowBuilds` * (a `{ pkg: boolean }` map). A migrated sub-project pnpm-workspace.yaml * usually carries BOTH for cross-version compatibility, but we must not * rely on the array twin always being present: we normalise `allowBuilds` * back into `onlyBuiltDependencies` before hoisting (see * `normalizeAllowBuilds`) so the build-allowlist survives into the pnpm-10 * monorepo root even when the file only carries the pnpm-11 object form. */ const PNPM11_BUILD_KEY = 'allowBuilds'; /** * Hoist workspace-scoped pnpm config from sub-projects into the root * package.json. After this runs, sub-project package.json files no * longer have `overrides`, `onlyBuiltDependencies`, or * `ignoredOptionalDependencies`, and the root package.json contains * the merged union. * * Two sources are read per sub-project, because the two starters store * their pnpm config differently: * * 1. `<sub>/package.json` `pnpm` block — nest-server-starter, and * nuxt-base-template before its pnpm-11 migration. * 2. `<sub>/pnpm-workspace.yaml` — nuxt-base-template after the * migration. pnpm 11 silently ignores the `pnpm` block in * package.json, so the template moved overrides into * pnpm-workspace.yaml. Inside a monorepo that nested file would * (a) not be hoisted by the old package.json-only logic, regressing * the CVE overrides, and (b) declare a nested workspace root that * conflicts with the monorepo's own pnpm-workspace.yaml. We hoist * its fields into the root package.json (the lt-monorepo root pins * pnpm@10 via `packageManager`, where package.json#pnpm IS honored) * and remove the now-redundant nested file. * * Symlinked sub-projects are skipped entirely: in `--frontend-link` / * `--api-link` mode `projects/app` (or `projects/api`) points at the * user's local framework checkout, and stripping its config or deleting * its pnpm-workspace.yaml would corrupt that source repo. * * Idempotent: running twice has the same effect as running once. * * @param options.filesystem Gluegun filesystem tool * @param options.projectDir Workspace root (contains pnpm-workspace.yaml) * @param options.subProjects Sub-project dirs relative to projectDir */ function hoistWorkspacePnpmConfig(options) { var _a; const { filesystem, projectDir, subProjects } = options; const rootPkgPath = `${projectDir}/package.json`; if (!filesystem.exists(rootPkgPath)) return; const rootPkg = filesystem.read(rootPkgPath, 'json'); if (!rootPkg) return; (_a = rootPkg.pnpm) !== null && _a !== void 0 ? _a : (rootPkg.pnpm = {}); const rootPnpm = rootPkg.pnpm; let rootChanged = false; for (const subDir of subProjects) { const subPath = `${projectDir}/${subDir}`; if (!filesystem.exists(subPath)) continue; // Never mutate a symlinked sub-project — it points at the user's own // checkout in link mode. if (isSymlink(subPath)) continue; if (hoistFromSubPackageJson({ filesystem, rootPnpm, subPath })) { rootChanged = true; } if (hoistFromSubWorkspaceYaml({ filesystem, rootPnpm, subPath })) { rootChanged = true; } } if (rootChanged) { filesystem.write(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`); } } /** * Move the workspace-scoped pnpm fields from `source` into `rootPnpm`, * deleting each moved field from `source`. Returns true if anything moved. */ function hoistFields(rootPnpm, source) { let changed = false; for (const field of WORKSPACE_SCOPED_PNPM_FIELDS) { if (source[field] === undefined) continue; rootPnpm[field] = mergePnpmFieldValue(field, rootPnpm[field], source[field]); delete source[field]; changed = true; } return changed; } /** Source 1: the sub-project's package.json `pnpm` block. */ function hoistFromSubPackageJson(options) { const { filesystem, rootPnpm, subPath } = options; const subPkgPath = `${subPath}/package.json`; if (!filesystem.exists(subPkgPath)) return false; const subPkg = filesystem.read(subPkgPath, 'json'); if (!(subPkg === null || subPkg === void 0 ? void 0 : subPkg.pnpm)) return false; if (!hoistFields(rootPnpm, subPkg.pnpm)) return false; // If the sub-project's pnpm section is now empty, drop it entirely. if (Object.keys(subPkg.pnpm).length === 0) { delete subPkg.pnpm; } filesystem.write(subPkgPath, `${JSON.stringify(subPkg, null, 2)}\n`); return true; } /** Source 2: the sub-project's pnpm-workspace.yaml (pnpm-11 layout). */ function hoistFromSubWorkspaceYaml(options) { const { filesystem, rootPnpm, subPath } = options; const subWsPath = `${subPath}/pnpm-workspace.yaml`; if (!filesystem.exists(subWsPath)) return false; const raw = filesystem.read(subWsPath); if (!raw) return false; let parsed; try { parsed = (0, js_yaml_1.load)(raw); } catch (_a) { // Malformed YAML — leave it untouched rather than risk data loss. return false; } if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false; const ws = parsed; // Fold the pnpm-11 `allowBuilds` map into `onlyBuiltDependencies` so it is // hoisted rather than discarded — even when the array twin is absent. normalizeAllowBuilds(ws); if (!hoistFields(rootPnpm, ws)) return false; // A settings-only file (no `packages:`) exists solely to carry these // hoisted keys — once emptied it would only declare a nested workspace // root, so remove it. A file that declares `packages:` is a real (rare) // nested workspace; keep it minus the hoisted keys. if (Array.isArray(ws.packages) && ws.packages.length > 0) { filesystem.write(subWsPath, (0, js_yaml_1.dump)(ws)); } else { filesystem.remove(subWsPath); } return true; } /** Whether `path` is a symbolic link (false on any stat error). */ function isSymlink(path) { try { return (0, fs_1.lstatSync)(path).isSymbolicLink(); } catch (_a) { return false; } } /** * Merge two values for a pnpm workspace-scoped field. * * pnpm expects: * - `overrides` → object ({pkg: version}) * - `onlyBuiltDependencies` → array of strings * - `ignoredOptionalDependencies` → array of strings * * Arrays: deduplicated, alphabetically sorted union. * Objects: sub-project values take precedence over root (sub-projects * like nest-server-starter own the authoritative CVE override list for * their transitive deps; the root usually only seeds cross-cutting * handlebars/minimatch patches). */ function mergePnpmFieldValue(field, rootValue, subValue) { if (field === 'onlyBuiltDependencies' || field === 'ignoredOptionalDependencies') { const rootArr = Array.isArray(rootValue) ? rootValue : []; const subArr = Array.isArray(subValue) ? subValue : []; return Array.from(new Set([...rootArr, ...subArr])).sort((a, b) => a.localeCompare(b)); } const rootObj = rootValue && typeof rootValue === 'object' && !Array.isArray(rootValue) ? rootValue : {}; const subObj = subValue && typeof subValue === 'object' && !Array.isArray(subValue) ? subValue : {}; const merged = Object.assign(Object.assign({}, rootObj), subObj); return Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b))); } /** * Fold a pnpm-11 `allowBuilds: { pkg: boolean }` map into the pnpm-10 * `onlyBuiltDependencies: string[]` form (packages whose value is `true`), * unioned with any existing array, then remove the `allowBuilds` key so the * redundant object form does not linger. Mutates `ws` in place. * * No-op when `allowBuilds` is absent or not an object map — an unexpected * shape is left untouched rather than risk silent data loss. */ function normalizeAllowBuilds(ws) { const raw = ws[PNPM11_BUILD_KEY]; if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return; const allowed = Object.entries(raw) .filter(([, enabled]) => enabled === true) .map(([pkg]) => pkg); if (allowed.length > 0) { const existing = Array.isArray(ws.onlyBuiltDependencies) ? ws.onlyBuiltDependencies : []; // Order here is irrelevant — mergePnpmFieldValue sorts the union on hoist. ws.onlyBuiltDependencies = Array.from(new Set([...allowed, ...existing])); } delete ws[PNPM11_BUILD_KEY]; }