Initial commit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dinlo
2026-05-31 18:44:04 +08:00
commit 436a9631fc
8616 changed files with 1389957 additions and 0 deletions
@@ -0,0 +1,10 @@
import { PM } from "./packageManager";
import { TraversedDependency } from "./types";
import { TraversalNodeModulesCollector } from "./traversalNodeModulesCollector";
export declare class BunNodeModulesCollector extends TraversalNodeModulesCollector {
readonly installOptions: {
manager: PM;
lockfile: string;
};
protected getDependenciesTree(pm: PM): Promise<TraversedDependency>;
}
@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BunNodeModulesCollector = void 0;
const builder_util_1 = require("builder-util");
const packageManager_1 = require("./packageManager");
const traversalNodeModulesCollector_1 = require("./traversalNodeModulesCollector");
class BunNodeModulesCollector extends traversalNodeModulesCollector_1.TraversalNodeModulesCollector {
constructor() {
super(...arguments);
this.installOptions = { manager: packageManager_1.PM.BUN, lockfile: "bun.lock" };
}
async getDependenciesTree(pm) {
builder_util_1.log.info(null, "note: bun does not support any CLI for dependency tree extraction, utilizing file traversal collector instead");
return super.getDependenciesTree(pm);
}
}
exports.BunNodeModulesCollector = BunNodeModulesCollector;
//# sourceMappingURL=bunNodeModulesCollector.js.map
@@ -0,0 +1 @@
{"version":3,"file":"bunNodeModulesCollector.js","sourceRoot":"","sources":["../../src/node-module-collector/bunNodeModulesCollector.ts"],"names":[],"mappings":";;;AAAA,+CAAkC;AAClC,qDAAqC;AAErC,mFAA+E;AAE/E,MAAa,uBAAwB,SAAQ,6DAA6B;IAA1E;;QACkB,mBAAc,GAAG,EAAE,OAAO,EAAE,mBAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAA;IAM5E,CAAC;IAJW,KAAK,CAAC,mBAAmB,CAAC,EAAM;QACxC,kBAAG,CAAC,IAAI,CAAC,IAAI,EAAE,+GAA+G,CAAC,CAAA;QAC/H,OAAO,KAAK,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAA;IACtC,CAAC;CACF;AAPD,0DAOC","sourcesContent":["import { log } from \"builder-util\"\nimport { PM } from \"./packageManager\"\nimport { TraversedDependency } from \"./types\"\nimport { TraversalNodeModulesCollector } from \"./traversalNodeModulesCollector\"\n\nexport class BunNodeModulesCollector extends TraversalNodeModulesCollector {\n public readonly installOptions = { manager: PM.BUN, lockfile: \"bun.lock\" }\n\n protected async getDependenciesTree(pm: PM): Promise<TraversedDependency> {\n log.info(null, \"note: bun does not support any CLI for dependency tree extraction, utilizing file traversal collector instead\")\n return super.getDependenciesTree(pm)\n }\n}\n"]}
+123
View File
@@ -0,0 +1,123 @@
/**
BSD 2-Clause License
Copyright (c) 2016-present, Yarn Contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* High-level node_modules hoisting algorithm recipe
*
* 1. Take input dependency graph and start traversing it,
* as you visit new node in the graph - clone it if there can be multiple paths
* to access the node from the graph root to the node, e.g. essentially represent
* the graph with a tree as you go, to make hoisting possible.
* 2. You want to hoist every node possible to the top root node first,
* then to each of its children etc, so you need to keep track what is your current
* root node into which you are hoisting
* 3. Traverse the dependency graph from the current root node and for each package name
* that can be potentially hoisted to the current root node build a list of idents
* in descending hoisting preference. You will check in next steps whether most preferred ident
* for the given package name can be hoisted first, and if not, then you check the
* less preferred ident, etc, until either some ident will be hoisted
* or you run out of idents to check
* (no need to convert the graph to the tree when you build this preference map).
* 4. The children of the root node are already "hoisted", so you need to start
* from the dependencies of these children. You take some child and
* sort its dependencies so that regular dependencies without peer dependencies
* will come first and then those dependencies that peer depend on them.
* This is needed to make algorithm more efficient and hoist nodes which are easier
* to hoist first and then handle peer dependent nodes.
* 5. You take this sorted list of dependencies and check if each of them can be
* hoisted to the current root node. To answer is the node can be hoisted you check
* your constraints - require promise and peer dependency promise.
* The possible answers can be: YES - the node is hoistable to the current root,
* NO - the node is not hoistable to the current root
* and DEPENDS - the node is hoistable to the root if nodes X, Y, Z are hoistable
* to the root. The case DEPENDS happens when all the require and other
* constraints are met, except peer dependency constraints. Note, that the nodes
* that are not package idents currently at the top of preference list are considered
* to have the answer NO right away, before doing any other constraint checks.
* 6. When you have hoistable answer for each dependency of a node you then build
* a list of nodes that are NOT hoistable. These are the nodes that have answer NO
* and the nodes that DEPENDS on these nodes. All the other nodes are hoistable,
* those that have answer YES and those that have answer DEPENDS,
* because they are cyclically dependent on each another
* 7. You hoist all the hoistable nodes to the current root and continue traversing
* the tree. Note, you need to track newly added nodes to the current root,
* because after you finished tree traversal you want to come back to these new nodes
* first thing and hoist everything from each of them to the current tree root.
* 8. After you have finished traversing newly hoisted current root nodes
* it means you cannot hoist anything to the current tree root and you need to pick
* the next node as current tree root and run the algorithm again
* until you run out of candidates for current tree root.
*/
type PackageName = string;
export declare enum HoisterDependencyKind {
REGULAR = 0,
WORKSPACE = 1,
EXTERNAL_SOFT_LINK = 2
}
export type HoisterTree = {
name: PackageName;
identName: PackageName;
reference: string;
dependencies: Set<HoisterTree>;
peerNames: Set<PackageName>;
hoistPriority?: number;
dependencyKind?: HoisterDependencyKind;
};
export type HoisterResult = {
name: PackageName;
identName: PackageName;
references: Set<string>;
dependencies: Set<HoisterResult>;
};
type Locator = string;
declare enum DebugLevel {
NONE = -1,
PERF = 0,
CHECK = 1,
REASONS = 2,
INTENSIVE_CHECK = 9
}
export type HoistOptions = {
/** Runs self-checks after hoisting is finished */
check?: boolean;
/** Debug level */
debugLevel?: DebugLevel;
/** Hoist borders are defined by parent node locator and its dependency name. The dependency is considered a border, nothing can be hoisted past this dependency, but dependency can be hoisted */
hoistingLimits?: Map<Locator, Set<PackageName>>;
};
/**
* Hoists package tree.
*
* The root node of a tree must has id: '.'.
* This function does not mutate its arguments, it hoists and returns tree copy.
*
* @param tree package tree (cycles in the tree are allowed)
*
* @returns hoisted tree copy
*/
export declare const hoist: (tree: HoisterTree, opts?: HoistOptions) => HoisterResult;
export {};
+884
View File
@@ -0,0 +1,884 @@
"use strict";
// copy from https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-nm/sources/hoist.ts
/**
BSD 2-Clause License
Copyright (c) 2016-present, Yarn Contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.hoist = exports.HoisterDependencyKind = void 0;
var HoisterDependencyKind;
(function (HoisterDependencyKind) {
HoisterDependencyKind[HoisterDependencyKind["REGULAR"] = 0] = "REGULAR";
HoisterDependencyKind[HoisterDependencyKind["WORKSPACE"] = 1] = "WORKSPACE";
HoisterDependencyKind[HoisterDependencyKind["EXTERNAL_SOFT_LINK"] = 2] = "EXTERNAL_SOFT_LINK";
})(HoisterDependencyKind || (exports.HoisterDependencyKind = HoisterDependencyKind = {}));
var Hoistable;
(function (Hoistable) {
Hoistable[Hoistable["YES"] = 0] = "YES";
Hoistable[Hoistable["NO"] = 1] = "NO";
Hoistable[Hoistable["DEPENDS"] = 2] = "DEPENDS";
})(Hoistable || (Hoistable = {}));
const makeLocator = (name, reference) => `${name}@${reference}`;
const makeIdent = (name, reference) => {
const hashIdx = reference.indexOf(`#`);
// Strip virtual reference part, we don't need it for hoisting purposes
const realReference = hashIdx >= 0 ? reference.substring(hashIdx + 1) : reference;
return makeLocator(name, realReference);
};
var DebugLevel;
(function (DebugLevel) {
DebugLevel[DebugLevel["NONE"] = -1] = "NONE";
DebugLevel[DebugLevel["PERF"] = 0] = "PERF";
DebugLevel[DebugLevel["CHECK"] = 1] = "CHECK";
DebugLevel[DebugLevel["REASONS"] = 2] = "REASONS";
DebugLevel[DebugLevel["INTENSIVE_CHECK"] = 9] = "INTENSIVE_CHECK";
})(DebugLevel || (DebugLevel = {}));
/**
* Hoists package tree.
*
* The root node of a tree must has id: '.'.
* This function does not mutate its arguments, it hoists and returns tree copy.
*
* @param tree package tree (cycles in the tree are allowed)
*
* @returns hoisted tree copy
*/
const hoist = (tree, opts = {}) => {
const debugLevel = opts.debugLevel || Number(process.env.NM_DEBUG_LEVEL || DebugLevel.NONE);
const check = opts.check || debugLevel >= DebugLevel.INTENSIVE_CHECK;
const hoistingLimits = opts.hoistingLimits || new Map();
const options = { check, debugLevel, hoistingLimits, fastLookupPossible: true };
let startTime;
if (options.debugLevel >= DebugLevel.PERF)
startTime = Date.now();
const treeCopy = cloneTree(tree, options);
let anotherRoundNeeded = false;
let round = 0;
do {
const result = hoistTo(treeCopy, [treeCopy], new Set([treeCopy.locator]), new Map(), options);
anotherRoundNeeded = result.anotherRoundNeeded || result.isGraphChanged;
options.fastLookupPossible = false;
round++;
} while (anotherRoundNeeded);
if (options.debugLevel >= DebugLevel.PERF)
console.log(`hoist time: ${Date.now() - startTime}ms, rounds: ${round}`);
if (options.debugLevel >= DebugLevel.CHECK) {
const prevTreeDump = dumpDepTree(treeCopy);
const isGraphChanged = hoistTo(treeCopy, [treeCopy], new Set([treeCopy.locator]), new Map(), options).isGraphChanged;
if (isGraphChanged)
throw new Error(`The hoisting result is not terminal, prev tree:\n${prevTreeDump}, next tree:\n${dumpDepTree(treeCopy)}`);
const checkLog = selfCheck(treeCopy);
if (checkLog) {
throw new Error(`${checkLog}, after hoisting finished:\n${dumpDepTree(treeCopy)}`);
}
}
if (options.debugLevel >= DebugLevel.REASONS)
console.log(dumpDepTree(treeCopy));
return shrinkTree(treeCopy);
};
exports.hoist = hoist;
const getZeroRoundUsedDependencies = (rootNodePath) => {
const rootNode = rootNodePath[rootNodePath.length - 1];
const usedDependencies = new Map();
const seenNodes = new Set();
const addUsedDependencies = (node) => {
if (seenNodes.has(node))
return;
seenNodes.add(node);
for (const dep of node.hoistedDependencies.values())
usedDependencies.set(dep.name, dep);
for (const dep of node.dependencies.values()) {
if (!node.peerNames.has(dep.name)) {
addUsedDependencies(dep);
}
}
};
addUsedDependencies(rootNode);
return usedDependencies;
};
const getUsedDependencies = (rootNodePath) => {
const rootNode = rootNodePath[rootNodePath.length - 1];
const usedDependencies = new Map();
const seenNodes = new Set();
const hiddenDependencies = new Set();
const addUsedDependencies = (node, hiddenDependencies) => {
if (seenNodes.has(node))
return;
seenNodes.add(node);
for (const dep of node.hoistedDependencies.values()) {
if (!hiddenDependencies.has(dep.name)) {
let reachableDependency;
for (const node of rootNodePath) {
reachableDependency = node.dependencies.get(dep.name);
if (reachableDependency) {
usedDependencies.set(reachableDependency.name, reachableDependency);
}
}
}
}
const childrenHiddenDependencies = new Set();
for (const dep of node.dependencies.values())
childrenHiddenDependencies.add(dep.name);
for (const dep of node.dependencies.values()) {
if (!node.peerNames.has(dep.name)) {
addUsedDependencies(dep, childrenHiddenDependencies);
}
}
};
addUsedDependencies(rootNode, hiddenDependencies);
return usedDependencies;
};
/**
* This method clones the node and returns cloned node copy, if the node was not previously decoupled.
*
* The node is considered decoupled if there is no multiple parents to any node
* on the path from the dependency graph root up to this node. This means that there are no other
* nodes in dependency graph that somehow transitively use this node and hence node can be hoisted without
* side effects.
*
* The process of node decoupling is done by going from root node of the graph up to the node in concern
* and decoupling each node on this graph path.
*
* @param node original node
*
* @returns decoupled node
*/
const decoupleGraphNode = (parent, node) => {
if (node.decoupled)
return node;
const { name, references, ident, locator, dependencies, originalDependencies, hoistedDependencies, peerNames, reasons, isHoistBorder, hoistPriority, dependencyKind, hoistedFrom, hoistedTo, } = node;
// To perform node hoisting from parent node we must clone parent nodes up to the root node,
// because some other package in the tree might depend on the parent package where hoisting
// cannot be performed
const clone = {
name,
references: new Set(references),
ident,
locator,
dependencies: new Map(dependencies),
originalDependencies: new Map(originalDependencies),
hoistedDependencies: new Map(hoistedDependencies),
peerNames: new Set(peerNames),
reasons: new Map(reasons),
decoupled: true,
isHoistBorder,
hoistPriority,
dependencyKind,
hoistedFrom: new Map(hoistedFrom),
hoistedTo: new Map(hoistedTo),
};
const selfDep = clone.dependencies.get(name);
if (selfDep && selfDep.ident == clone.ident) // Update self-reference
{
clone.dependencies.set(name, clone);
}
parent.dependencies.set(clone.name, clone);
return clone;
};
/**
* Builds a map of most preferred packages that might be hoisted to the root node.
*
* The values in the map are idents sorted by preference from most preferred to less preferred.
* If the root node has already some version of a package, the value array will contain only
* one element, since it is not possible for other versions of a package to be hoisted.
*
* @param rootNode root node
* @param preferenceMap preference map
*/
const getHoistIdentMap = (rootNode, preferenceMap) => {
const identMap = new Map([[rootNode.name, [rootNode.ident]]]);
for (const dep of rootNode.dependencies.values()) {
if (!rootNode.peerNames.has(dep.name)) {
identMap.set(dep.name, [dep.ident]);
}
}
const keyList = Array.from(preferenceMap.keys());
keyList.sort((key1, key2) => {
const entry1 = preferenceMap.get(key1);
const entry2 = preferenceMap.get(key2);
if (entry2.hoistPriority !== entry1.hoistPriority) {
return entry2.hoistPriority - entry1.hoistPriority;
}
else {
const entry1Usages = entry1.dependents.size + entry1.peerDependents.size;
const entry2Usages = entry2.dependents.size + entry2.peerDependents.size;
return entry2Usages - entry1Usages;
}
});
for (const key of keyList) {
const name = key.substring(0, key.indexOf(`@`, 1));
const ident = key.substring(name.length + 1);
if (!rootNode.peerNames.has(name)) {
let idents = identMap.get(name);
if (!idents) {
idents = [];
identMap.set(name, idents);
}
if (idents.indexOf(ident) < 0) {
idents.push(ident);
}
}
}
return identMap;
};
/**
* Gets regular node dependencies only and sorts them in the order so that
* peer dependencies come before the dependency that rely on them.
*
* @param node graph node
* @returns sorted regular dependencies
*/
const getSortedRegularDependencies = (node) => {
const dependencies = new Set();
const addDep = (dep, seenDeps = new Set()) => {
if (seenDeps.has(dep))
return;
seenDeps.add(dep);
for (const peerName of dep.peerNames) {
if (!node.peerNames.has(peerName)) {
const peerDep = node.dependencies.get(peerName);
if (peerDep && !dependencies.has(peerDep)) {
addDep(peerDep, seenDeps);
}
}
}
dependencies.add(dep);
};
for (const dep of node.dependencies.values()) {
if (!node.peerNames.has(dep.name)) {
addDep(dep);
}
}
return dependencies;
};
/**
* Performs hoisting all the dependencies down the tree to the root node.
*
* The algorithm used here reduces dependency graph by deduplicating
* instances of the packages while keeping:
* 1. Regular dependency promise: the package should require the exact version of the dependency
* that was declared in its `package.json`
* 2. Peer dependency promise: the package and its direct parent package
* must use the same instance of the peer dependency
*
* The regular and peer dependency promises are kept while performing transform
* on tree branches of packages at a time:
* `root package` -> `parent package 1` ... `parent package n` -> `dependency`
* We check wether we can hoist `dependency` to `root package`, this boils down basically
* to checking:
* 1. Wether `root package` does not depend on other version of `dependency`
* 2. Wether all the peer dependencies of a `dependency` had already been hoisted from all `parent packages`
*
* If many versions of the `dependency` can be hoisted to the `root package` we choose the most used
* `dependency` version in the project among them.
*
* This function mutates the tree.
*
* @param tree package dependencies graph
* @param rootNode root node to hoist to
* @param rootNodePath root node path in the tree
* @param rootNodePathLocators a set of locators for nodes that lead from the top of the tree up to root node
* @param options hoisting options
*/
const hoistTo = (tree, rootNodePath, rootNodePathLocators, parentShadowedNodes, options, seenNodes = new Set()) => {
const rootNode = rootNodePath[rootNodePath.length - 1];
if (seenNodes.has(rootNode))
return { anotherRoundNeeded: false, isGraphChanged: false };
seenNodes.add(rootNode);
const preferenceMap = buildPreferenceMap(rootNode);
const hoistIdentMap = getHoistIdentMap(rootNode, preferenceMap);
const usedDependencies = tree == rootNode ? new Map() : options.fastLookupPossible ? getZeroRoundUsedDependencies(rootNodePath) : getUsedDependencies(rootNodePath);
let wasStateChanged;
let anotherRoundNeeded = false;
let isGraphChanged = false;
const hoistIdents = new Map(Array.from(hoistIdentMap.entries()).map(([k, v]) => [k, v[0]]));
const shadowedNodes = new Map();
do {
const result = hoistGraph(tree, rootNodePath, rootNodePathLocators, usedDependencies, hoistIdents, hoistIdentMap, parentShadowedNodes, shadowedNodes, options);
if (result.isGraphChanged)
isGraphChanged = true;
if (result.anotherRoundNeeded)
anotherRoundNeeded = true;
wasStateChanged = false;
for (const [name, idents] of hoistIdentMap) {
if (idents.length > 1 && !rootNode.dependencies.has(name)) {
hoistIdents.delete(name);
idents.shift();
hoistIdents.set(name, idents[0]);
wasStateChanged = true;
}
}
} while (wasStateChanged);
for (const dependency of rootNode.dependencies.values()) {
if (!rootNode.peerNames.has(dependency.name) && !rootNodePathLocators.has(dependency.locator)) {
rootNodePathLocators.add(dependency.locator);
const result = hoistTo(tree, [...rootNodePath, dependency], rootNodePathLocators, shadowedNodes, options);
if (result.isGraphChanged)
isGraphChanged = true;
if (result.anotherRoundNeeded)
anotherRoundNeeded = true;
rootNodePathLocators.delete(dependency.locator);
}
}
return { anotherRoundNeeded, isGraphChanged };
};
const hasUnhoistedDependencies = (node) => {
for (const [subName, subDependency] of node.dependencies) {
if (!node.peerNames.has(subName) && subDependency.ident !== node.ident) {
return true;
}
}
return false;
};
const getNodeHoistInfo = (rootNode, rootNodePathLocators, nodePath, node, usedDependencies, hoistIdents, hoistIdentMap, shadowedNodes, { outputReason, fastLookupPossible }) => {
let reasonRoot;
let reason = null;
let dependsOn = new Set();
if (outputReason) {
reasonRoot = `${Array.from(rootNodePathLocators)
.map(x => prettyPrintLocator(x))
.join(``)}`;
}
const parentNode = nodePath[nodePath.length - 1];
// We cannot hoist self-references
const isSelfReference = node.ident === parentNode.ident;
let isHoistable = !isSelfReference;
if (outputReason && !isHoistable)
reason = `- self-reference`;
if (isHoistable) {
isHoistable = node.dependencyKind !== HoisterDependencyKind.WORKSPACE;
if (outputReason && !isHoistable) {
reason = `- workspace`;
}
}
if (isHoistable && node.dependencyKind === HoisterDependencyKind.EXTERNAL_SOFT_LINK) {
isHoistable = !hasUnhoistedDependencies(node);
if (outputReason && !isHoistable) {
reason = `- external soft link with unhoisted dependencies`;
}
}
if (isHoistable) {
isHoistable = !rootNode.peerNames.has(node.name);
if (outputReason && !isHoistable) {
reason = `- cannot shadow peer: ${prettyPrintLocator(rootNode.originalDependencies.get(node.name).locator)} at ${reasonRoot}`;
}
}
if (isHoistable) {
let isNameAvailable = false;
const usedDep = usedDependencies.get(node.name);
isNameAvailable = !usedDep || usedDep.ident === node.ident;
if (outputReason && !isNameAvailable)
reason = `- filled by: ${prettyPrintLocator(usedDep.locator)} at ${reasonRoot}`;
if (isNameAvailable) {
for (let idx = nodePath.length - 1; idx >= 1; idx--) {
const parent = nodePath[idx];
const parentDep = parent.dependencies.get(node.name);
if (parentDep && parentDep.ident !== node.ident) {
isNameAvailable = false;
let shadowedNames = shadowedNodes.get(parentNode);
if (!shadowedNames) {
shadowedNames = new Set();
shadowedNodes.set(parentNode, shadowedNames);
}
shadowedNames.add(node.name);
if (outputReason) {
reason = `- filled by ${prettyPrintLocator(parentDep.locator)} at ${nodePath
.slice(0, idx)
.map(x => prettyPrintLocator(x.locator))
.join(``)}`;
}
break;
}
}
}
isHoistable = isNameAvailable;
}
if (isHoistable) {
const hoistedIdent = hoistIdents.get(node.name);
isHoistable = hoistedIdent === node.ident;
if (outputReason && !isHoistable) {
reason = `- filled by: ${prettyPrintLocator(hoistIdentMap.get(node.name)[0])} at ${reasonRoot}`;
}
}
if (isHoistable) {
let arePeerDepsSatisfied = true;
const checkList = new Set(node.peerNames);
for (let idx = nodePath.length - 1; idx >= 1; idx--) {
const parent = nodePath[idx];
for (const name of checkList) {
if (parent.peerNames.has(name) && parent.originalDependencies.has(name))
continue;
const parentDepNode = parent.dependencies.get(name);
if (parentDepNode && rootNode.dependencies.get(name) !== parentDepNode) {
if (idx === nodePath.length - 1) {
dependsOn.add(parentDepNode);
}
else {
dependsOn = null;
arePeerDepsSatisfied = false;
if (outputReason) {
reason = `- peer dependency ${prettyPrintLocator(parentDepNode.locator)} from parent ${prettyPrintLocator(parent.locator)} was not hoisted to ${reasonRoot}`;
}
}
}
checkList.delete(name);
}
if (!arePeerDepsSatisfied) {
break;
}
}
isHoistable = arePeerDepsSatisfied;
}
if (isHoistable && !fastLookupPossible) {
for (const origDep of node.hoistedDependencies.values()) {
const usedDep = usedDependencies.get(origDep.name) || rootNode.dependencies.get(origDep.name);
if (!usedDep || origDep.ident !== usedDep.ident) {
isHoistable = false;
if (outputReason)
reason = `- previously hoisted dependency mismatch, needed: ${prettyPrintLocator(origDep.locator)}, available: ${prettyPrintLocator(usedDep === null || usedDep === void 0 ? void 0 : usedDep.locator)}`;
break;
}
}
}
if (dependsOn !== null && dependsOn.size > 0) {
return { isHoistable: Hoistable.DEPENDS, dependsOn, reason };
}
else {
return { isHoistable: isHoistable ? Hoistable.YES : Hoistable.NO, reason };
}
};
const getAliasedLocator = (node) => `${node.name}@${node.locator}`;
/**
* Performs actual graph transformation, by hoisting packages to the root node.
*
* @param tree dependency tree
* @param rootNodePath root node path in the tree
* @param rootNodePathLocators a set of locators for nodes that lead from the top of the tree up to root node
* @param usedDependencies map of dependency nodes from parents of root node used by root node and its children via parent lookup
* @param hoistIdents idents that should be attempted to be hoisted to the root node
*/
const hoistGraph = (tree, rootNodePath, rootNodePathLocators, usedDependencies, hoistIdents, hoistIdentMap, parentShadowedNodes, shadowedNodes, options) => {
const rootNode = rootNodePath[rootNodePath.length - 1];
const seenNodes = new Set();
let anotherRoundNeeded = false;
let isGraphChanged = false;
const hoistNodeDependencies = (nodePath, locatorPath, aliasedLocatorPath, parentNode, newNodes) => {
if (seenNodes.has(parentNode))
return;
const nextLocatorPath = [...locatorPath, getAliasedLocator(parentNode)];
const nextAliasedLocatorPath = [...aliasedLocatorPath, getAliasedLocator(parentNode)];
const dependantTree = new Map();
const hoistInfos = new Map();
for (const subDependency of getSortedRegularDependencies(parentNode)) {
const hoistInfo = getNodeHoistInfo(rootNode, rootNodePathLocators, [rootNode, ...nodePath, parentNode], subDependency, usedDependencies, hoistIdents, hoistIdentMap, shadowedNodes, { outputReason: options.debugLevel >= DebugLevel.REASONS, fastLookupPossible: options.fastLookupPossible });
hoistInfos.set(subDependency, hoistInfo);
if (hoistInfo.isHoistable === Hoistable.DEPENDS) {
for (const node of hoistInfo.dependsOn) {
const nodeDependants = dependantTree.get(node.name) || new Set();
nodeDependants.add(subDependency.name);
dependantTree.set(node.name, nodeDependants);
}
}
}
const unhoistableNodes = new Set();
const addUnhoistableNode = (node, hoistInfo, reason) => {
if (!unhoistableNodes.has(node)) {
unhoistableNodes.add(node);
hoistInfos.set(node, { isHoistable: Hoistable.NO, reason });
for (const dependantName of dependantTree.get(node.name) || []) {
addUnhoistableNode(parentNode.dependencies.get(dependantName), hoistInfo, options.debugLevel >= DebugLevel.REASONS
? `- peer dependency ${prettyPrintLocator(node.locator)} from parent ${prettyPrintLocator(parentNode.locator)} was not hoisted`
: ``);
}
}
};
for (const [node, hoistInfo] of hoistInfos)
if (hoistInfo.isHoistable === Hoistable.NO)
addUnhoistableNode(node, hoistInfo, hoistInfo.reason);
let wereNodesHoisted = false;
for (const node of hoistInfos.keys()) {
if (!unhoistableNodes.has(node)) {
isGraphChanged = true;
const shadowedNames = parentShadowedNodes.get(parentNode);
if (shadowedNames && shadowedNames.has(node.name))
anotherRoundNeeded = true;
wereNodesHoisted = true;
parentNode.dependencies.delete(node.name);
parentNode.hoistedDependencies.set(node.name, node);
parentNode.reasons.delete(node.name);
const hoistedNode = rootNode.dependencies.get(node.name);
if (options.debugLevel >= DebugLevel.REASONS) {
const hoistedFrom = Array.from(locatorPath)
.concat([parentNode.locator])
.map(x => prettyPrintLocator(x))
.join(``);
let hoistedFromArray = rootNode.hoistedFrom.get(node.name);
if (!hoistedFromArray) {
hoistedFromArray = [];
rootNode.hoistedFrom.set(node.name, hoistedFromArray);
}
hoistedFromArray.push(hoistedFrom);
parentNode.hoistedTo.set(node.name, Array.from(rootNodePath)
.map(x => prettyPrintLocator(x.locator))
.join(``));
}
// Add hoisted node to root node, in case it is not already there
if (!hoistedNode) {
// Avoid adding other version of root node to itself
if (rootNode.ident !== node.ident) {
rootNode.dependencies.set(node.name, node);
newNodes.add(node);
}
}
else {
for (const reference of node.references) {
hoistedNode.references.add(reference);
}
}
}
}
if (parentNode.dependencyKind === HoisterDependencyKind.EXTERNAL_SOFT_LINK && wereNodesHoisted)
anotherRoundNeeded = true;
if (options.check) {
const checkLog = selfCheck(tree);
if (checkLog) {
throw new Error(`${checkLog}, after hoisting dependencies of ${[rootNode, ...nodePath, parentNode].map(x => prettyPrintLocator(x.locator)).join(``)}:\n${dumpDepTree(tree)}`);
}
}
const children = getSortedRegularDependencies(parentNode);
for (const node of children) {
if (unhoistableNodes.has(node)) {
const hoistInfo = hoistInfos.get(node);
const hoistableIdent = hoistIdents.get(node.name);
if ((hoistableIdent === node.ident || !parentNode.reasons.has(node.name)) && hoistInfo.isHoistable !== Hoistable.YES)
parentNode.reasons.set(node.name, hoistInfo.reason);
if (!node.isHoistBorder && nextAliasedLocatorPath.indexOf(getAliasedLocator(node)) < 0) {
seenNodes.add(parentNode);
const decoupledNode = decoupleGraphNode(parentNode, node);
hoistNodeDependencies([...nodePath, parentNode], nextLocatorPath, nextAliasedLocatorPath, decoupledNode, nextNewNodes);
seenNodes.delete(parentNode);
}
}
}
};
let newNodes;
let nextNewNodes = new Set(getSortedRegularDependencies(rootNode));
const aliasedRootNodePathLocators = Array.from(rootNodePath).map(x => getAliasedLocator(x));
do {
newNodes = nextNewNodes;
nextNewNodes = new Set();
for (const dep of newNodes) {
if (dep.locator === rootNode.locator || dep.isHoistBorder)
continue;
const decoupledDependency = decoupleGraphNode(rootNode, dep);
hoistNodeDependencies([], Array.from(rootNodePathLocators), aliasedRootNodePathLocators, decoupledDependency, nextNewNodes);
}
} while (nextNewNodes.size > 0);
return { anotherRoundNeeded, isGraphChanged };
};
const selfCheck = (tree) => {
const log = [];
const seenNodes = new Set();
const parents = new Set();
const checkNode = (node, parentDeps, parent) => {
if (seenNodes.has(node))
return;
seenNodes.add(node);
if (parents.has(node))
return;
const dependencies = new Map(parentDeps);
for (const dep of node.dependencies.values())
if (!node.peerNames.has(dep.name))
dependencies.set(dep.name, dep);
for (const origDep of node.originalDependencies.values()) {
const dep = dependencies.get(origDep.name);
const prettyPrintTreePath = () => `${Array.from(parents)
.concat([node])
.map(x => prettyPrintLocator(x.locator))
.join(``)}`;
if (node.peerNames.has(origDep.name)) {
const parentDep = parentDeps.get(origDep.name);
if (parentDep !== dep || !parentDep || parentDep.ident !== origDep.ident) {
log.push(`${prettyPrintTreePath()} - broken peer promise: expected ${origDep.ident} but found ${parentDep ? parentDep.ident : parentDep}`);
}
}
else {
const hoistedFrom = parent.hoistedFrom.get(node.name);
const originalHoistedTo = node.hoistedTo.get(origDep.name);
const prettyHoistedFrom = `${hoistedFrom ? ` hoisted from ${hoistedFrom.join(`, `)}` : ``}`;
const prettyOriginalHoistedTo = `${originalHoistedTo ? ` hoisted to ${originalHoistedTo}` : ``}`;
const prettyNodePath = `${prettyPrintTreePath()}${prettyHoistedFrom}`;
if (!dep) {
log.push(`${prettyNodePath} - broken require promise: no required dependency ${origDep.name}${prettyOriginalHoistedTo} found`);
}
else if (dep.ident !== origDep.ident) {
log.push(`${prettyNodePath} - broken require promise for ${origDep.name}${prettyOriginalHoistedTo}: expected ${origDep.ident}, but found: ${dep.ident}`);
}
}
}
parents.add(node);
for (const dep of node.dependencies.values()) {
if (!node.peerNames.has(dep.name)) {
checkNode(dep, dependencies, node);
}
}
parents.delete(node);
};
checkNode(tree, tree.dependencies, tree);
return log.join(`\n`);
};
/**
* Creates a clone of package tree with extra fields used for hoisting purposes.
*
* @param tree package tree clone
*/
const cloneTree = (tree, options) => {
const { identName, name, reference, peerNames } = tree;
const treeCopy = {
name,
references: new Set([reference]),
locator: makeLocator(identName, reference),
ident: makeIdent(identName, reference),
dependencies: new Map(),
originalDependencies: new Map(),
hoistedDependencies: new Map(),
peerNames: new Set(peerNames),
reasons: new Map(),
decoupled: true,
isHoistBorder: true,
hoistPriority: 0,
dependencyKind: HoisterDependencyKind.WORKSPACE,
hoistedFrom: new Map(),
hoistedTo: new Map(),
};
const seenNodes = new Map([[tree, treeCopy]]);
const addNode = (node, parentNode) => {
let workNode = seenNodes.get(node);
const isSeen = !!workNode;
if (!workNode) {
const { name, identName, reference, peerNames, hoistPriority, dependencyKind } = node;
const dependenciesNmHoistingLimits = options.hoistingLimits.get(parentNode.locator);
workNode = {
name,
references: new Set([reference]),
locator: makeLocator(identName, reference),
ident: makeIdent(identName, reference),
dependencies: new Map(),
originalDependencies: new Map(),
hoistedDependencies: new Map(),
peerNames: new Set(peerNames),
reasons: new Map(),
decoupled: true,
isHoistBorder: dependenciesNmHoistingLimits ? dependenciesNmHoistingLimits.has(name) : false,
hoistPriority: hoistPriority || 0,
dependencyKind: dependencyKind || HoisterDependencyKind.REGULAR,
hoistedFrom: new Map(),
hoistedTo: new Map(),
};
seenNodes.set(node, workNode);
}
parentNode.dependencies.set(node.name, workNode);
parentNode.originalDependencies.set(node.name, workNode);
if (!isSeen) {
for (const dep of node.dependencies) {
addNode(dep, workNode);
}
}
else {
const seenCoupledNodes = new Set();
const markNodeCoupled = (node) => {
if (seenCoupledNodes.has(node))
return;
seenCoupledNodes.add(node);
node.decoupled = false;
for (const dep of node.dependencies.values()) {
if (!node.peerNames.has(dep.name)) {
markNodeCoupled(dep);
}
}
};
markNodeCoupled(workNode);
}
};
for (const dep of tree.dependencies)
addNode(dep, treeCopy);
return treeCopy;
};
const getIdentName = (locator) => locator.substring(0, locator.indexOf(`@`, 1));
/**
* Creates a clone of hoisted package tree with extra fields removed
*
* @param tree stripped down hoisted package tree clone
*/
const shrinkTree = (tree) => {
const treeCopy = {
name: tree.name,
identName: getIdentName(tree.locator),
references: new Set(tree.references),
dependencies: new Set(),
};
const seenNodes = new Set([tree]);
const addNode = (node, parentWorkNode, parentNode) => {
const isSeen = seenNodes.has(node);
let resultNode;
if (parentWorkNode === node) {
resultNode = parentNode;
}
else {
const { name, references, locator } = node;
resultNode = {
name,
identName: getIdentName(locator),
references,
dependencies: new Set(),
};
}
parentNode.dependencies.add(resultNode);
if (!isSeen) {
seenNodes.add(node);
for (const dep of node.dependencies.values()) {
if (!node.peerNames.has(dep.name)) {
addNode(dep, node, resultNode);
}
}
seenNodes.delete(node);
}
};
for (const dep of tree.dependencies.values())
addNode(dep, tree, treeCopy);
return treeCopy;
};
/**
* Builds mapping, where key is an alias + dependent package ident and the value is the list of
* parent package idents who depend on this package.
*
* @param rootNode package tree root node
*
* @returns preference map
*/
const buildPreferenceMap = (rootNode) => {
const preferenceMap = new Map();
const seenNodes = new Set([rootNode]);
const getPreferenceKey = (node) => `${node.name}@${node.ident}`;
const getOrCreatePreferenceEntry = (node) => {
const key = getPreferenceKey(node);
let entry = preferenceMap.get(key);
if (!entry) {
entry = { dependents: new Set(), peerDependents: new Set(), hoistPriority: 0 };
preferenceMap.set(key, entry);
}
return entry;
};
const addDependent = (dependent, node) => {
const isSeen = !!seenNodes.has(node);
const entry = getOrCreatePreferenceEntry(node);
entry.dependents.add(dependent.ident);
if (!isSeen) {
seenNodes.add(node);
for (const dep of node.dependencies.values()) {
const entry = getOrCreatePreferenceEntry(dep);
entry.hoistPriority = Math.max(entry.hoistPriority, dep.hoistPriority);
if (node.peerNames.has(dep.name)) {
entry.peerDependents.add(node.ident);
}
else {
addDependent(node, dep);
}
}
}
};
for (const dep of rootNode.dependencies.values())
if (!rootNode.peerNames.has(dep.name))
addDependent(rootNode, dep);
return preferenceMap;
};
const prettyPrintLocator = (locator) => {
if (!locator)
return `none`;
const idx = locator.indexOf(`@`, 1);
let name = locator.substring(0, idx);
if (name.endsWith(`$wsroot$`))
name = `wh:${name.replace(`$wsroot$`, ``)}`;
const reference = locator.substring(idx + 1);
if (reference === `workspace:.`) {
return `.`;
}
else if (!reference) {
return `${name}`;
}
else {
let version = (reference.indexOf(`#`) > 0 ? reference.split(`#`)[1] : reference).replace(`npm:`, ``);
if (reference.startsWith(`virtual`))
name = `v:${name}`;
if (version.startsWith(`workspace`)) {
name = `w:${name}`;
version = ``;
}
return `${name}${version ? `@${version}` : ``}`;
}
};
const MAX_NODES_TO_DUMP = 50000;
/**
* Pretty-prints dependency tree in the `yarn why`-like format
*
* The function is used for troubleshooting purposes only.
*
* @param pkg node_modules tree
*
* @returns sorted node_modules tree
*/
const dumpDepTree = (tree) => {
let nodeCount = 0;
const dumpPackage = (pkg, parents, prefix = ``) => {
if (nodeCount > MAX_NODES_TO_DUMP || parents.has(pkg))
return ``;
nodeCount++;
const dependencies = Array.from(pkg.dependencies.values()).sort((n1, n2) => {
if (n1.name === n2.name) {
return 0;
}
else {
return n1.name > n2.name ? 1 : -1;
}
});
let str = ``;
parents.add(pkg);
for (let idx = 0; idx < dependencies.length; idx++) {
const dep = dependencies[idx];
if (!pkg.peerNames.has(dep.name) && dep !== pkg) {
const reason = pkg.reasons.get(dep.name);
const identName = getIdentName(dep.locator);
str += `${prefix}${idx < dependencies.length - 1 ? `├─` : `└─`}${(parents.has(dep) ? `>` : ``) + (identName !== dep.name ? `a:${dep.name}:` : ``) + prettyPrintLocator(dep.locator) + (reason ? ` ${reason}` : ``)}\n`;
str += dumpPackage(dep, parents, `${prefix}${idx < dependencies.length - 1 ? `` : ` `}`);
}
}
parents.delete(pkg);
return str;
};
const treeDump = dumpPackage(tree, new Set());
return treeDump + (nodeCount > MAX_NODES_TO_DUMP ? `\nTree is too large, part of the tree has been dunped\n` : ``);
};
//# sourceMappingURL=hoist.js.map
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
import { Nullish } from "builder-util-runtime";
import { TmpDir } from "temp-file";
import { NpmNodeModulesCollector } from "./npmNodeModulesCollector";
import { getPackageManagerCommand, PM } from "./packageManager";
import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector";
import { BunNodeModulesCollector } from "./bunNodeModulesCollector";
import { Lazy } from "lazy-val";
import { TraversalNodeModulesCollector } from "./traversalNodeModulesCollector";
export { getPackageManagerCommand, PM };
export declare function getCollectorByPackageManager(pm: PM, rootDir: string, tempDirManager: TmpDir): NpmNodeModulesCollector | PnpmNodeModulesCollector | TraversalNodeModulesCollector | BunNodeModulesCollector;
export declare const determinePackageManagerEnv: ({ projectDir, appDir, workspaceRoot }: {
projectDir: string;
appDir: string;
workspaceRoot: string | Nullish;
}) => Lazy<{
pm: PM;
workspaceRoot: Promise<string | undefined>;
}>;
+130
View File
@@ -0,0 +1,130 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.determinePackageManagerEnv = exports.PM = exports.getPackageManagerCommand = void 0;
exports.getCollectorByPackageManager = getCollectorByPackageManager;
const npmNodeModulesCollector_1 = require("./npmNodeModulesCollector");
const packageManager_1 = require("./packageManager");
Object.defineProperty(exports, "getPackageManagerCommand", { enumerable: true, get: function () { return packageManager_1.getPackageManagerCommand; } });
Object.defineProperty(exports, "PM", { enumerable: true, get: function () { return packageManager_1.PM; } });
const pnpmNodeModulesCollector_1 = require("./pnpmNodeModulesCollector");
const yarnBerryNodeModulesCollector_1 = require("./yarnBerryNodeModulesCollector");
const yarnNodeModulesCollector_1 = require("./yarnNodeModulesCollector");
const bunNodeModulesCollector_1 = require("./bunNodeModulesCollector");
const lazy_val_1 = require("lazy-val");
const builder_util_1 = require("builder-util");
const fs = require("fs-extra");
const path = require("path");
const traversalNodeModulesCollector_1 = require("./traversalNodeModulesCollector");
function getCollectorByPackageManager(pm, rootDir, tempDirManager) {
switch (pm) {
case packageManager_1.PM.PNPM:
return new pnpmNodeModulesCollector_1.PnpmNodeModulesCollector(rootDir, tempDirManager);
case packageManager_1.PM.YARN:
return new yarnNodeModulesCollector_1.YarnNodeModulesCollector(rootDir, tempDirManager);
case packageManager_1.PM.YARN_BERRY:
return new yarnBerryNodeModulesCollector_1.YarnBerryNodeModulesCollector(rootDir, tempDirManager);
case packageManager_1.PM.BUN:
return new bunNodeModulesCollector_1.BunNodeModulesCollector(rootDir, tempDirManager);
case packageManager_1.PM.NPM:
return new npmNodeModulesCollector_1.NpmNodeModulesCollector(rootDir, tempDirManager);
case packageManager_1.PM.TRAVERSAL:
return new traversalNodeModulesCollector_1.TraversalNodeModulesCollector(rootDir, tempDirManager);
}
}
const determinePackageManagerEnv = ({ projectDir, appDir, workspaceRoot }) => new lazy_val_1.Lazy(async () => {
const availableDirs = [workspaceRoot, projectDir, appDir].filter((it) => !(0, builder_util_1.isEmptyOrSpaces)(it));
const pm = await (0, packageManager_1.detectPackageManager)(availableDirs);
const root = await findWorkspaceRoot(pm.pm, projectDir);
if (root != null) {
// re-detect package manager from workspace root, this seems particularly necessary for pnpm workspaces
const actualPm = await (0, packageManager_1.detectPackageManager)([root]);
builder_util_1.log.info({ pm: actualPm.pm, config: actualPm.corepackConfig, resolved: actualPm.resolvedDirectory, projectDir }, `detected workspace root for project using ${actualPm.detectionMethod}`);
return {
pm: actualPm.pm,
workspaceRoot: Promise.resolve(actualPm.resolvedDirectory),
};
}
return {
pm: pm.pm,
workspaceRoot: Promise.resolve(pm.resolvedDirectory),
};
});
exports.determinePackageManagerEnv = determinePackageManagerEnv;
async function findWorkspaceRoot(pm, cwd) {
let command;
switch (pm) {
case packageManager_1.PM.PNPM:
command = { command: "pnpm", args: ["--workspace-root", "exec", "pwd"] };
break;
case packageManager_1.PM.YARN_BERRY:
command = { command: "yarn", args: ["workspaces", "list", "--json"] };
break;
case packageManager_1.PM.YARN: {
command = { command: "yarn", args: ["workspaces", "info", "--silent"] };
break;
}
case packageManager_1.PM.BUN:
command = { command: "bun", args: ["pm", "ls", "--json"] };
break;
case packageManager_1.PM.NPM:
default:
command = { command: "npm", args: ["prefix", "-w"] };
break;
}
const output = await (0, builder_util_1.spawn)(command.command, command.args, { cwd, stdio: ["ignore", "pipe", "ignore"] })
.then(async (it) => {
const out = it === null || it === void 0 ? void 0 : it.trim();
if (!out) {
return undefined;
}
if (pm === packageManager_1.PM.YARN) {
JSON.parse(out); // if JSON valid, workspace detected
return findNearestPackageJsonWithWorkspacesField(cwd);
}
else if (pm === packageManager_1.PM.BUN) {
const json = JSON.parse(out);
if (Array.isArray(json) && json.length > 0) {
return findNearestPackageJsonWithWorkspacesField(cwd);
}
}
else if (pm === packageManager_1.PM.YARN_BERRY) {
const lines = out
.split("\n")
.map(l => l.trim())
.filter(Boolean);
for (const line of lines) {
const parsed = JSON.parse(line);
if (parsed.location != null) {
const potential = path.resolve(cwd, parsed.location);
return (await (0, builder_util_1.exists)(potential)) ? findNearestPackageJsonWithWorkspacesField(potential) : undefined;
}
}
}
return out.length === 0 || out === "undefined" ? undefined : out;
})
.catch(() => findNearestPackageJsonWithWorkspacesField(cwd));
return output;
}
async function findNearestPackageJsonWithWorkspacesField(dir) {
let current = dir;
while (true) {
const pkgPath = path.join(current, "package.json");
try {
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
if (pkg.workspaces) {
builder_util_1.log.debug({ path: current }, "identified workspace root");
return current;
}
}
catch {
// ignore
}
const parent = path.dirname(current);
if (parent === current) {
break;
}
current = parent;
}
return undefined;
}
//# sourceMappingURL=index.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,67 @@
import { LogLevel } from "builder-util";
import { PackageJson } from "./types";
import * as fs from "fs-extra";
export declare enum LogMessageByKey {
PKG_DUPLICATE_REF = "duplicate dependency references",
PKG_NOT_FOUND = "cannot find path for dependency",
PKG_NOT_ON_DISK = "dependency not found on disk",
PKG_SELF_REF = "self-referential dependencies",
PKG_OPTIONAL_NOT_INSTALLED = "missing optional dependencies",
PKG_COLLECTOR_OUTPUT = "collector stderr output"
}
export declare const logMessageLevelByKey: Record<LogMessageByKey, LogLevel>;
export type Package = {
packageDir: string;
packageJson: PackageJson;
};
type JsonCache = Record<string, Promise<PackageJson | null>>;
type RealPathCache = Record<string, Promise<string>>;
type ExistsCache = Record<string, Promise<boolean>>;
type LstatCache = Record<string, Promise<fs.Stats | null>>;
type PackageCache = Record<string, Promise<Package | null>>;
type LogSummaryCache = Record<LogMessageByKey, string[]>;
export declare class ModuleManager {
/** Cache for package.json contents (readJson) */
readonly json: JsonCache;
/** Cache for resolved real paths (if symlink, realpath; otherwise resolve) */
readonly realPath: RealPathCache;
/** Cache for file/directory existence checks */
readonly exists: ExistsCache;
/** Cache for lstat results */
readonly lstat: LstatCache;
/** Cache for package lookups (key: "packageName||fromDir||semverRange"). Use helper function `versionedCacheKey` */
readonly packageData: PackageCache;
/** For logging purposes, just track all dependencies for each key */
readonly logSummary: LogSummaryCache;
private readonly jsonMap;
private readonly realPathMap;
private readonly existsMap;
private readonly lstatMap;
private readonly packageDataMap;
private readonly logSummaryMap;
constructor();
private createLogSummarySyncProxy;
private createAsyncProxy;
versionedCacheKey(pkg: {
name: string;
path: string;
semver?: string;
}): string;
protected locatePackageVersionFromCacheKey(key: string): Promise<Package | null>;
locatePackageVersion({ parentDir, pkgName, requiredRange }: {
parentDir: string;
pkgName: string;
requiredRange?: string;
}): Promise<Package | null>;
private semverSatisfies;
/**
* Upward search (hoisted)
*/
private upwardSearch;
/**
* Breadth-first downward search from parentDir/node_modules
* Looks for node_modules/\*\/node_modules/pkgName (and deeper)
*/
private downwardSearch;
}
export {};
+276
View File
@@ -0,0 +1,276 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModuleManager = exports.logMessageLevelByKey = exports.LogMessageByKey = void 0;
const builder_util_1 = require("builder-util");
const fs = require("fs-extra");
const path = require("path");
const semver = require("semver");
var LogMessageByKey;
(function (LogMessageByKey) {
LogMessageByKey["PKG_DUPLICATE_REF"] = "duplicate dependency references";
LogMessageByKey["PKG_NOT_FOUND"] = "cannot find path for dependency";
LogMessageByKey["PKG_NOT_ON_DISK"] = "dependency not found on disk";
LogMessageByKey["PKG_SELF_REF"] = "self-referential dependencies";
LogMessageByKey["PKG_OPTIONAL_NOT_INSTALLED"] = "missing optional dependencies";
LogMessageByKey["PKG_COLLECTOR_OUTPUT"] = "collector stderr output";
})(LogMessageByKey || (exports.LogMessageByKey = LogMessageByKey = {}));
exports.logMessageLevelByKey = {
[LogMessageByKey.PKG_DUPLICATE_REF]: "info",
[LogMessageByKey.PKG_NOT_FOUND]: "warn",
[LogMessageByKey.PKG_NOT_ON_DISK]: "warn",
[LogMessageByKey.PKG_SELF_REF]: "debug",
[LogMessageByKey.PKG_OPTIONAL_NOT_INSTALLED]: "info",
[LogMessageByKey.PKG_COLLECTOR_OUTPUT]: "warn",
};
class ModuleManager {
constructor() {
this.jsonMap = new Map();
this.realPathMap = new Map();
this.existsMap = new Map();
this.lstatMap = new Map();
this.packageDataMap = new Map();
this.logSummaryMap = new Map();
this.logSummary = this.createLogSummarySyncProxy();
this.exists = this.createAsyncProxy(this.existsMap, (p) => (0, builder_util_1.exists)(p));
this.json = this.createAsyncProxy(this.jsonMap, (p) => fs.readJson(p).catch(() => null));
this.lstat = this.createAsyncProxy(this.lstatMap, (p) => fs.lstat(p).catch(() => null));
this.packageData = this.createAsyncProxy(this.packageDataMap, (p) => this.locatePackageVersionFromCacheKey(p).catch(() => null));
this.realPath = this.createAsyncProxy(this.realPathMap, async (p) => {
const filePath = path.resolve(p);
const stat = await this.lstat[filePath];
return (stat === null || stat === void 0 ? void 0 : stat.isSymbolicLink()) ? fs.realpath(filePath) : filePath;
});
}
createLogSummarySyncProxy() {
return new Proxy({}, {
get: (_, key) => {
if (!this.logSummaryMap.has(key)) {
this.logSummaryMap.set(key, []);
}
return this.logSummaryMap.get(key);
},
set: (_, key, value) => {
this.logSummaryMap.set(key, value);
return true;
},
has: (_, key) => {
return this.logSummaryMap.has(key);
},
// Add these to make Object.entries() work
ownKeys: _ => {
return Array.from(this.logSummaryMap.keys());
},
getOwnPropertyDescriptor: (_, key) => {
if (this.logSummaryMap.has(key)) {
return {
enumerable: true,
configurable: true,
};
}
return undefined;
},
});
}
// this allows dot-notation access while still supporting async retrieval
// e.g., cache.packageJson[somePath] returns Promise<PackageJson>
createAsyncProxy(map, compute) {
return new Proxy({}, {
async get(_, key) {
if (map.has(key)) {
return Promise.resolve(map.get(key));
}
return await Promise.resolve(compute(key)).then(value => {
map.set(key, value);
return value;
});
},
set(_, key, value) {
map.set(key, value);
return true;
},
has(_, key) {
return map.has(key);
},
});
}
versionedCacheKey(pkg) {
return [pkg.name, pkg.path, pkg.semver || ""].join("||");
}
async locatePackageVersionFromCacheKey(key) {
const [name, fromDir, semverRange] = key.split("||");
const result = await this.locatePackageVersion({ parentDir: fromDir, pkgName: name, requiredRange: semverRange });
if (result == null) {
return null;
}
return { ...result, packageDir: await this.realPath[result.packageDir] };
}
async locatePackageVersion({ parentDir, pkgName, requiredRange }) {
// 1) check direct parent node_modules/pkgName first
const direct = path.join(path.resolve(parentDir), "node_modules", pkgName, "package.json");
if (await this.exists[direct]) {
const json = await this.json[direct];
if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(direct), packageJson: json };
}
}
// 2) upward hoisted search, then 3) downward non-hoisted search
return (await this.upwardSearch(parentDir, pkgName, requiredRange)) || (await this.downwardSearch(parentDir, pkgName, requiredRange)) || null;
}
semverSatisfies(found, range) {
if ((0, builder_util_1.isEmptyOrSpaces)(range) || range === "*") {
return true;
}
if (range === found) {
return true;
}
if (semver.validRange(range) == null) {
// ignore, we can't verify non-semver ranges
// e.g. git urls, file:, patch:, etc. Example:
// "@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.43#~/.yarn/patches/@ai-sdk-google-npm-2.0.43-689ed559b3.patch"
builder_util_1.log.debug({ found, range }, "unable to validate semver version range, assuming match");
return true;
}
try {
return semver.satisfies(found, range);
}
catch {
// fallback: simple equality or basic prefix handling (^, ~)
if (range.startsWith("^") || range.startsWith("~")) {
const r = range.slice(1);
return r === found;
}
// if range is like "8.x" or "8.*" match major
const m = range.match(/^(\d+)[.(*|x)]*/);
const fm = found.match(/^(\d+)\./);
if (m && fm) {
return m[1] === fm[1];
}
return false;
}
}
/**
* Upward search (hoisted)
*/
async upwardSearch(parentDir, pkgName, requiredRange) {
let current = path.resolve(parentDir);
const root = path.parse(current).root;
while (true) {
const candidate = path.join(current, "node_modules", pkgName, "package.json");
if (await this.exists[candidate]) {
const json = await this.json[candidate];
if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(candidate), packageJson: json };
}
// otherwise keep searching upward (we may find a different hoisted version)
}
if (current === root) {
break;
}
const parent = path.dirname(current);
if (parent === current) {
break;
}
current = parent;
}
return null;
}
/**
* Breadth-first downward search from parentDir/node_modules
* Looks for node_modules/\*\/node_modules/pkgName (and deeper)
*/
async downwardSearch(parentDir, pkgName, requiredRange, maxExplored = 2000, maxDepth = 6) {
var _a, _b, _c, _d;
const start = path.join(path.resolve(parentDir), "node_modules");
if (!(await this.exists[start]) || !((_a = (await this.lstat[start])) === null || _a === void 0 ? void 0 : _a.isDirectory())) {
return null;
}
const visited = new Set();
const queue = [{ dir: start, depth: 0 }];
let explored = 0;
while (queue.length > 0) {
const { dir, depth } = queue.shift();
if (explored++ > maxExplored) {
break;
}
if (depth > maxDepth) {
continue;
}
let entries;
try {
entries = await fs.readdir(dir);
}
catch {
continue;
}
for (const entry of entries) {
if (entry.startsWith(".")) {
continue;
}
const entryPath = path.join(dir, entry);
// handle scoped packages @scope/name
if (entry.startsWith("@")) {
// queue the scope directory itself to explore its children
if ((await this.exists[entryPath]) && ((_b = (await this.lstat[entryPath])) === null || _b === void 0 ? void 0 : _b.isDirectory())) {
const scopeEntries = await fs.readdir(entryPath);
for (const sc of scopeEntries) {
const scPath = path.join(entryPath, sc);
// check scPath/node_modules/pkgName
const candidatePkgJson = path.join(scPath, "node_modules", pkgName, "package.json");
if (await this.exists[candidatePkgJson]) {
const json = await this.json[candidatePkgJson];
if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(candidatePkgJson), packageJson: json };
}
}
// enqueue scPath/node_modules to explore further
const scNodeModules = path.join(scPath, "node_modules");
if ((await this.exists[scNodeModules]) && ((_c = (await this.lstat[scNodeModules])) === null || _c === void 0 ? void 0 : _c.isDirectory())) {
if (!visited.has(scNodeModules)) {
visited.add(scNodeModules);
queue.push({ dir: scNodeModules, depth: depth + 1 });
}
}
}
}
continue;
}
// check for direct candidate: entry/node_modules/pkgName
try {
const stat = await this.lstat[entryPath];
if (!(stat === null || stat === void 0 ? void 0 : stat.isDirectory())) {
continue;
}
}
catch {
continue;
}
const candidatePkgJson = path.join(entryPath, "node_modules", pkgName, "package.json");
if (await this.exists[candidatePkgJson]) {
const json = await this.json[candidatePkgJson];
if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(candidatePkgJson), packageJson: json };
}
}
// also check entry/node_modules directly for pkgName (some layouts)
const candidateDirect = path.join(entryPath, pkgName, "package.json");
if (await this.exists[candidateDirect]) {
const json = await this.json[candidateDirect];
if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(candidateDirect), packageJson: json };
}
}
// enqueue entry/node_modules for deeper traversal
const nextNodeModules = path.join(entryPath, "node_modules");
if ((await this.exists[nextNodeModules]) && ((_d = (await this.lstat[nextNodeModules])) === null || _d === void 0 ? void 0 : _d.isDirectory())) {
if (!visited.has(nextNodeModules)) {
visited.add(nextNodeModules);
queue.push({ dir: nextNodeModules, depth: depth + 1 });
}
}
}
}
return null;
}
}
exports.ModuleManager = ModuleManager;
//# sourceMappingURL=moduleManager.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,121 @@
import { TmpDir } from "builder-util";
import { Lazy } from "lazy-val";
import { ModuleManager } from "./moduleManager";
import { PM } from "./packageManager";
import type { Dependency, DependencyGraph, NodeModuleInfo, PackageJson } from "./types";
export declare abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDepType, OptionalDepType>, OptionalDepType> {
protected readonly rootDir: string;
private readonly tempDirManager;
private readonly nodeModules;
protected readonly allDependencies: Map<string, ProdDepType>;
protected readonly productionGraph: DependencyGraph;
protected readonly cache: ModuleManager;
protected isHoisted: Lazy<boolean>;
constructor(rootDir: string, tempDirManager: TmpDir);
/**
* Retrieves and collects all Node.js modules for a given package.
*
* This method orchestrates the entire module collection process by:
* 1. Fetching the dependency tree from the package manager
* 2. Collecting all dependencies recursively
* 3. Extracting workspace references if applicable
* 4. Building a production dependency graph
* 5. Hoisting the dependencies to their final locations
* 6. Resolving and returning module information
*/
getNodeModules({ packageName }: {
packageName: string;
}): Promise<{
nodeModules: NodeModuleInfo[];
logSummary: ModuleManager["logSummary"];
}>;
abstract readonly installOptions: {
manager: PM;
lockfile: string;
};
protected abstract getArgs(): string[];
protected abstract extractProductionDependencyGraph(tree: Dependency<ProdDepType, OptionalDepType>, dependencyId: string): Promise<void>;
protected abstract collectAllDependencies(tree: Dependency<ProdDepType, OptionalDepType>, appPackageName: string): Promise<void>;
/**
* Retrieves the dependency tree from the package manager.
*
* Executes the appropriate package manager command to fetch the dependency tree and writes
* the output to a temporary file. Includes retry logic to handle transient failures such as
* incomplete JSON output or missing files. Will retry up to 1 time with exponential backoff.
*/
protected getDependenciesTree(pm: PM): Promise<ProdDepType>;
/**
* Parses the dependencies tree from shell command output.
*
**/
protected parseDependenciesTree(shellOutput: string): ProdDepType | Promise<ProdDepType>;
protected extractJsonFromPollutedOutput<T>(shellOutput: string): T;
protected cacheKey(pkg: Pick<ProdDepType, "name" | "version" | "path">): string;
protected normalizePackageVersion(key: string, pkg: ProdDepType): {
id: string;
pkgOverride: ProdDepType & {
name: string;
};
};
/**
* Determines if a given dependency is a production dependency of a package.
*
* Checks both the dependencies and optionalDependencies of a package to see if
* the specified dependency name is listed.
*
* @param depName - The name of the dependency to check
* @param pkg - The package to search for the dependency in
* @returns True if the dependency is found in either dependencies or optionalDependencies, false otherwise
*/
protected isProdDependency(depName: string, pkg: ProdDepType): boolean;
protected locatePackageWithVersion(depTree: Pick<ProdDepType, "name" | "version" | "path">): Promise<{
packageDir: string;
packageJson: PackageJson;
} | null>;
/**
* Parses a dependency identifier string into name and version components.
*
* Handles both scoped packages (e.g., "@scope/pkg@1.2.3") and regular packages (e.g., "pkg@1.2.3").
* If the identifier is malformed or cannot be parsed, defaults to treating the entire string as
* the package name with an "unknown" version.
*/
protected parseNameVersion(identifier: string): {
name: string;
version: string;
};
/**
* Retrieves the dependency tree and handles workspace package self-references.
*
* If the project is a workspace project, this method removes the root package's self-reference
* from the dependency tree to avoid circular dependencies. It promotes the root package's
* direct dependencies to the top level of the tree.
*
* @param tree - The original dependency tree
* @param packageName - The name of the package to check for and remove from the tree
* @returns The extracted dependency subtree
*/
protected getTreeFromWorkspaces(tree: ProdDepType, packageName: string): ProdDepType;
private transformToHoisterTree;
private _getNodeModules;
asyncExec(command: string, args: string[], cwd?: string): Promise<{
stdout: string | undefined;
stderr: string | undefined;
}>;
/**
* Executes a command and streams its output to a file.
*
* Spawns a child process to execute the specified command with arguments, capturing stdout
* to a file. Handles Windows-specific quirks by wrapping .cmd files in a temporary .bat file
* when necessary. Enables corepack strict mode by default but allows process.env overrides.
*
* Special handling for `npm list` exit code 1, which is expected in certain scenarios.
*
* @param command - The command to execute
* @param args - Array of command-line arguments
* @param cwd - The working directory to execute the command in
* @param tempOutputFile - The path to the temporary file where stdout will be written
* @returns Promise that resolves when the command completes successfully or rejects if it fails
* @throws {Error} If the child process spawn fails or exits with a non-zero code
*/
streamCollectorCommandToFile(command: string, args: string[], cwd: string, tempOutputFile: string): Promise<void>;
}
@@ -0,0 +1,346 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeModulesCollector = void 0;
const builder_util_1 = require("builder-util");
const childProcess = require("child_process");
const fs = require("fs-extra");
const fs_extra_1 = require("fs-extra");
const lazy_val_1 = require("lazy-val");
const path = require("path");
const hoist_1 = require("./hoist");
const moduleManager_1 = require("./moduleManager");
const packageManager_1 = require("./packageManager");
class NodeModulesCollector {
constructor(rootDir, tempDirManager) {
this.rootDir = rootDir;
this.tempDirManager = tempDirManager;
this.nodeModules = [];
this.allDependencies = new Map();
this.productionGraph = {};
this.cache = new moduleManager_1.ModuleManager();
this.isHoisted = new lazy_val_1.Lazy(async () => {
const { manager } = this.installOptions;
const command = (0, packageManager_1.getPackageManagerCommand)(manager);
const config = (await this.asyncExec(command, ["config", "list"])).stdout;
if (config == null) {
builder_util_1.log.debug({ manager }, "unable to determine if node_modules are hoisted: no config output. falling back to hoisted mode");
return false;
}
const lines = Object.fromEntries(config.split("\n").map(line => line.split("=").map(s => s.trim())));
if (lines["node-linker"] === "hoisted") {
builder_util_1.log.debug({ manager }, "node_modules are hoisted");
return true;
}
return false;
});
}
/**
* Retrieves and collects all Node.js modules for a given package.
*
* This method orchestrates the entire module collection process by:
* 1. Fetching the dependency tree from the package manager
* 2. Collecting all dependencies recursively
* 3. Extracting workspace references if applicable
* 4. Building a production dependency graph
* 5. Hoisting the dependencies to their final locations
* 6. Resolving and returning module information
*/
async getNodeModules({ packageName }) {
const tree = await this.getDependenciesTree(this.installOptions.manager);
await this.collectAllDependencies(tree, packageName);
const realTree = this.getTreeFromWorkspaces(tree, packageName);
await this.extractProductionDependencyGraph(realTree, packageName);
const hoisterResult = (0, hoist_1.hoist)(this.transformToHoisterTree(this.productionGraph, packageName), {
check: builder_util_1.log.isDebugEnabled,
});
await this._getNodeModules(hoisterResult.dependencies, this.nodeModules);
builder_util_1.log.debug({ packageName, depCount: this.nodeModules.length }, "node modules collection complete");
return { nodeModules: this.nodeModules, logSummary: this.cache.logSummary };
}
/**
* Retrieves the dependency tree from the package manager.
*
* Executes the appropriate package manager command to fetch the dependency tree and writes
* the output to a temporary file. Includes retry logic to handle transient failures such as
* incomplete JSON output or missing files. Will retry up to 1 time with exponential backoff.
*/
async getDependenciesTree(pm) {
const command = (0, packageManager_1.getPackageManagerCommand)(pm);
const args = this.getArgs();
const tempOutputFile = await this.tempDirManager.getTempFile({
prefix: path.basename(command, path.extname(command)),
suffix: "output.json",
});
return (0, builder_util_1.retry)(async () => {
await this.streamCollectorCommandToFile(command, args, this.rootDir, tempOutputFile);
const shellOutput = await fs.readFile(tempOutputFile, { encoding: "utf8" });
const result = await Promise.resolve(this.parseDependenciesTree(shellOutput));
return result;
}, {
retries: 1,
interval: 2000,
backoff: 2000,
shouldRetry: async (error) => {
var _a;
const fields = { error: error.message, tempOutputFile, cwd: this.rootDir, packageManager: pm };
if (!(await (0, builder_util_1.exists)(tempOutputFile))) {
builder_util_1.log.debug(fields, "dependency tree output file missing, retrying");
return true;
}
const fileContent = await fs.readFile(tempOutputFile, { encoding: "utf8" });
fields.fileContentLength = fileContent.length.toString();
if (fileContent.trim().length === 0) {
builder_util_1.log.debug(fields, "dependency tree output file empty, retrying");
return true;
}
// extract small start/end sample for debugging purposes (e.g. polluted console output)
const lines = fileContent.split("\n");
const lineSampleSize = Math.min(5, lines.length / 2);
if (2 * lineSampleSize > 5) {
fields.sampleStart = lines.slice(0, lineSampleSize).join("\n");
fields.sampleEnd = lines.slice(-lineSampleSize).join("\n");
}
else {
fields.content = fileContent;
}
if ((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes("Unexpected end of JSON input")) {
builder_util_1.log.debug(fields, "JSON parse error in dependency tree, retrying");
return true;
}
builder_util_1.log.error(fields, "error parsing dependencies tree");
return false;
},
});
}
/**
* Parses the dependencies tree from shell command output.
*
**/
parseDependenciesTree(shellOutput) {
return this.extractJsonFromPollutedOutput(shellOutput);
}
extractJsonFromPollutedOutput(shellOutput) {
const consoleOutput = shellOutput.trim();
try {
// Please for the love of all that is holy, this should cover 99% of cases where npm/pnpm/yarn output is clean JSON
return JSON.parse(consoleOutput);
}
catch {
// ignore
}
// DEDICATED FALLBACK FOR POLLUTED OUTPUT, non-trivial to implement correctly, not needed in most cases, and highly inefficient
// Find the first index that starts with { or [
const bracketOpen = Math.max(consoleOutput.indexOf("{"), 0);
const bracketOpenSquare = Math.max(consoleOutput.indexOf("["), 0);
const start = Math.min(bracketOpen, bracketOpenSquare); // always non-negative due to Math.max above
for (let i = start; i < consoleOutput.length; i++) {
const slice = consoleOutput.slice(start, i + 1);
try {
return JSON.parse(slice);
}
catch {
// ignore, try next
}
}
throw new Error("No JSON content found in output");
}
cacheKey(pkg) {
const rel = path.relative(this.rootDir, pkg.path);
return `${pkg.name}::${pkg.version}::${rel !== null && rel !== void 0 ? rel : "."}`;
}
// We use the key (alias name) instead of value.name for npm aliased packages
// e.g., { "foo": { name: "@scope/bar", ... } } should be stored as "foo@version"
normalizePackageVersion(key, pkg) {
return { id: `${key}@${pkg.version}`, pkgOverride: { ...pkg, name: key } };
}
/**
* Determines if a given dependency is a production dependency of a package.
*
* Checks both the dependencies and optionalDependencies of a package to see if
* the specified dependency name is listed.
*
* @param depName - The name of the dependency to check
* @param pkg - The package to search for the dependency in
* @returns True if the dependency is found in either dependencies or optionalDependencies, false otherwise
*/
isProdDependency(depName, pkg) {
const prodDeps = { ...pkg.dependencies, ...pkg.optionalDependencies };
return prodDeps[depName] != null;
}
async locatePackageWithVersion(depTree) {
const result = await this.cache.locatePackageVersion({
parentDir: depTree.path,
pkgName: depTree.name,
requiredRange: depTree.version,
});
return result;
}
/**
* Parses a dependency identifier string into name and version components.
*
* Handles both scoped packages (e.g., "@scope/pkg@1.2.3") and regular packages (e.g., "pkg@1.2.3").
* If the identifier is malformed or cannot be parsed, defaults to treating the entire string as
* the package name with an "unknown" version.
*/
parseNameVersion(identifier) {
const lastAt = identifier.lastIndexOf("@");
if (lastAt <= 0) {
// fallback for scoped packages or malformed strings
return { name: identifier, version: "unknown" };
}
const name = identifier.slice(0, lastAt);
const version = identifier.slice(lastAt + 1);
return { name, version };
}
/**
* Retrieves the dependency tree and handles workspace package self-references.
*
* If the project is a workspace project, this method removes the root package's self-reference
* from the dependency tree to avoid circular dependencies. It promotes the root package's
* direct dependencies to the top level of the tree.
*
* @param tree - The original dependency tree
* @param packageName - The name of the package to check for and remove from the tree
* @returns The extracted dependency subtree
*/
getTreeFromWorkspaces(tree, packageName) {
if (tree.workspaces && tree.dependencies) {
for (const [key, value] of Object.entries(tree.dependencies)) {
if (key === packageName) {
return value;
}
}
}
return tree;
}
transformToHoisterTree(obj, key, nodes = new Map()) {
let node = nodes.get(key);
const { name, version } = this.parseNameVersion(key);
if (!node) {
node = {
name,
identName: name,
reference: version,
dependencies: new Set(),
peerNames: new Set(),
};
nodes.set(key, node);
const deps = (obj[key] || {}).dependencies || [];
for (const dep of deps) {
const child = this.transformToHoisterTree(obj, dep, nodes);
node.dependencies.add(child);
}
}
return node;
}
async _getNodeModules(dependencies, result) {
var _a;
if (dependencies.size === 0) {
return;
}
for (const d of dependencies.values()) {
const reference = [...d.references][0];
const key = `${d.name}@${reference}`;
const p = (_a = this.allDependencies.get(key)) === null || _a === void 0 ? void 0 : _a.path;
if (p === undefined) {
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_NOT_FOUND].push(key);
continue;
}
// fix npm list issue
// https://github.com/npm/cli/issues/8535
if (!(await this.cache.exists[p])) {
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_NOT_ON_DISK].push(key);
continue;
}
const node = {
name: d.name,
version: reference,
dir: await this.cache.realPath[p],
};
result.push(node);
if (d.dependencies.size > 0) {
node.dependencies = [];
await this._getNodeModules(d.dependencies, node.dependencies);
}
}
result.sort((a, b) => a.name.localeCompare(b.name));
}
async asyncExec(command, args, cwd = this.rootDir) {
const file = await this.tempDirManager.getTempFile({ prefix: "exec-", suffix: ".txt" });
try {
await this.streamCollectorCommandToFile(command, args, cwd, file);
const result = await fs.readFile(file, { encoding: "utf8" });
return { stdout: result === null || result === void 0 ? void 0 : result.trim(), stderr: undefined };
}
catch (error) {
builder_util_1.log.debug({ error: error.message }, "failed to execute command");
return { stdout: undefined, stderr: error.message };
}
}
/**
* Executes a command and streams its output to a file.
*
* Spawns a child process to execute the specified command with arguments, capturing stdout
* to a file. Handles Windows-specific quirks by wrapping .cmd files in a temporary .bat file
* when necessary. Enables corepack strict mode by default but allows process.env overrides.
*
* Special handling for `npm list` exit code 1, which is expected in certain scenarios.
*
* @param command - The command to execute
* @param args - Array of command-line arguments
* @param cwd - The working directory to execute the command in
* @param tempOutputFile - The path to the temporary file where stdout will be written
* @returns Promise that resolves when the command completes successfully or rejects if it fails
* @throws {Error} If the child process spawn fails or exits with a non-zero code
*/
async streamCollectorCommandToFile(command, args, cwd, tempOutputFile) {
const execName = path.basename(command, path.extname(command));
const isWindowsScriptFile = process.platform === "win32" && path.extname(command).toLowerCase() === ".cmd";
if (isWindowsScriptFile) {
// If the command is a Windows script file (.cmd), we need to wrap it in a .bat file to ensure it runs correctly with cmd.exe
// This is necessary because .cmd files are not directly executable in the same way as .bat files.
// We create a temporary .bat file that calls the .cmd file with the provided arguments. The .bat file will be executed by cmd.exe.
// Note: This is a workaround for Windows command execution quirks when using `shell: true`
const tempBatFile = await this.tempDirManager.getTempFile({
prefix: execName,
suffix: ".bat",
});
const batScript = `@echo off\r\n"${command}" %*\r\n`; // <-- CRLF required for .bat
await fs.writeFile(tempBatFile, batScript, { encoding: "utf8" });
command = "cmd.exe";
args = ["/c", `"${tempBatFile}"`, ...args];
}
await new Promise((resolve, reject) => {
const outStream = (0, fs_extra_1.createWriteStream)(tempOutputFile);
const child = childProcess.spawn(command, args, {
cwd,
env: { COREPACK_ENABLE_STRICT: "0", ...process.env }, // allow `process.env` overrides
shell: true, // `true`` is now required: https://github.com/electron-userland/electron-builder/issues/9488
});
let stderr = "";
child.stdout.pipe(outStream);
child.stderr.on("data", chunk => {
stderr += chunk.toString();
});
child.on("error", err => {
reject(new Error(`Node module collector spawn failed: ${err.message}`));
});
child.on("close", code => {
outStream.close();
// https://github.com/npm/npm/issues/17624
const shouldIgnore = code === 1 && "npm" === execName.toLowerCase() && args.includes("list");
if (shouldIgnore) {
builder_util_1.log.debug(null, "`npm list` returned non-zero exit code, but it MIGHT be expected (https://github.com/npm/npm/issues/17624). Check stderr for details.");
}
if (stderr.length > 0) {
builder_util_1.log.debug({ stderr }, "note: there was node module collector output on stderr");
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_COLLECTOR_OUTPUT].push(stderr);
}
const shouldResolve = code === 0 || shouldIgnore;
return shouldResolve ? resolve() : reject(new Error(`Node module collector process exited with code ${code}:\n${stderr}`));
});
});
}
}
exports.NodeModulesCollector = NodeModulesCollector;
//# sourceMappingURL=nodeModulesCollector.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
import { NodeModulesCollector } from "./nodeModulesCollector.js";
import { PM } from "./packageManager.js";
import { NpmDependency } from "./types.js";
export declare class NpmNodeModulesCollector extends NodeModulesCollector<NpmDependency, string> {
readonly installOptions: {
manager: PM;
lockfile: string;
};
protected getArgs(): string[];
protected collectAllDependencies(tree: NpmDependency): Promise<void>;
protected extractProductionDependencyGraph(tree: NpmDependency, dependencyId: string): Promise<void>;
private isDuplicatedNpmDependency;
protected isProdDependency(packageName: string, tree: NpmDependency): boolean;
}
@@ -0,0 +1,80 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NpmNodeModulesCollector = void 0;
const moduleManager_js_1 = require("./moduleManager.js");
const nodeModulesCollector_js_1 = require("./nodeModulesCollector.js");
const packageManager_js_1 = require("./packageManager.js");
class NpmNodeModulesCollector extends nodeModulesCollector_js_1.NodeModulesCollector {
constructor() {
super(...arguments);
this.installOptions = {
manager: packageManager_js_1.PM.NPM,
lockfile: "package-lock.json",
};
}
getArgs() {
return ["list", "-a", "--include", "prod", "--include", "optional", "--omit", "dev", "--json", "--long", "--silent", "--loglevel=error"];
}
async collectAllDependencies(tree) {
for (const [key, value] of Object.entries(tree.dependencies || {})) {
const { id: childDependencyId, pkgOverride } = this.normalizePackageVersion(key, value);
// Only skip if this exact version is already collected AND it's a duplicate reference
// We need to collect nested versions even if a different version exists at top level
if (this.isDuplicatedNpmDependency(value)) {
// This is a reference to a package already defined elsewhere in the tree
// Still add it to allDependencies if we haven't seen this exact version yet
if (!this.allDependencies.has(childDependencyId)) {
this.allDependencies.set(childDependencyId, pkgOverride);
}
this.cache.logSummary[moduleManager_js_1.LogMessageByKey.PKG_DUPLICATE_REF].push(childDependencyId);
continue;
}
// Always store this dependency and recurse into its children
this.allDependencies.set(childDependencyId, pkgOverride);
await this.collectAllDependencies(pkgOverride);
}
}
async extractProductionDependencyGraph(tree, dependencyId) {
if (this.productionGraph[dependencyId]) {
return;
}
const isDuplicateDep = this.isDuplicatedNpmDependency(tree);
const targetTree = isDuplicateDep ? this.allDependencies.get(dependencyId) : tree;
// Initialize with empty dependencies array first to mark this dependency as "in progress"
// After initialization, if there are libraries with the same name+version later, they will not be searched recursively again
// This will prevents infinite loops when circular dependencies are encountered.
this.productionGraph[dependencyId] = { dependencies: [] };
const collectedDependencies = [];
if (targetTree === null || targetTree === void 0 ? void 0 : targetTree.dependencies) {
for (const packageName in targetTree.dependencies) {
// Check against matching _dependencies
if (!this.isProdDependency(packageName, targetTree)) {
continue;
}
const dependency = targetTree.dependencies[packageName];
// Match first version's empty check
if (Object.keys(dependency).length === 0) {
continue;
}
const { id: childDependencyId, pkgOverride } = this.normalizePackageVersion(packageName, dependency);
await this.extractProductionDependencyGraph(pkgOverride, childDependencyId);
collectedDependencies.push(childDependencyId);
}
}
this.productionGraph[dependencyId] = { dependencies: collectedDependencies };
}
// Check: is package already included as a prod dependency due to another package?
// We need to check this to prevent infinite loops in case of duplicated dependencies
isDuplicatedNpmDependency(tree) {
const { _dependencies = {}, dependencies = {} } = tree;
const isDuplicateDep = Object.keys(_dependencies).length > 0 && Object.keys(dependencies).length === 0;
return isDuplicateDep;
}
// `npm list` provides explicit list of deps in _dependencies
isProdDependency(packageName, tree) {
var _a;
return ((_a = tree._dependencies) === null || _a === void 0 ? void 0 : _a[packageName]) != null;
}
}
exports.NpmNodeModulesCollector = NpmNodeModulesCollector;
//# sourceMappingURL=npmNodeModulesCollector.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
export declare enum PM {
PNPM = "pnpm",
YARN = "yarn",
YARN_BERRY = "yarn-berry",
BUN = "bun",
NPM = "npm",
TRAVERSAL = "traversal"
}
export declare function getPackageManagerCommand(pm: PM): string;
type PackageManagerSetup = {
pm: PM;
corepackConfig: string | undefined;
resolvedDirectory: string | undefined;
detectionMethod: string;
};
export declare function detectPackageManager(searchPaths: string[]): Promise<PackageManagerSetup>;
export {};
@@ -0,0 +1,144 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PM = void 0;
exports.getPackageManagerCommand = getPackageManagerCommand;
exports.detectPackageManager = detectPackageManager;
const builder_util_1 = require("builder-util");
const fs = require("fs-extra");
const path = require("path");
const which = require("which");
var PM;
(function (PM) {
PM["PNPM"] = "pnpm";
PM["YARN"] = "yarn";
PM["YARN_BERRY"] = "yarn-berry";
PM["BUN"] = "bun";
PM["NPM"] = "npm";
PM["TRAVERSAL"] = "traversal";
})(PM || (exports.PM = PM = {}));
// Cache for resolved paths
const pmPathCache = {
[PM.NPM]: undefined,
[PM.YARN]: undefined,
[PM.PNPM]: undefined,
[PM.YARN_BERRY]: undefined,
[PM.BUN]: undefined,
[PM.TRAVERSAL]: undefined,
};
function resolveCommand(pm) {
const fallback = pm === PM.YARN_BERRY ? "yarn" : pm;
if (process.platform !== "win32") {
return fallback;
}
try {
return which.sync(fallback);
}
catch {
// If `which` fails (not found), still return the fallback string
return fallback;
}
}
function getPackageManagerCommand(pm) {
if (pmPathCache[pm] !== undefined) {
return pmPathCache[pm];
}
const resolved = resolveCommand(pm);
pmPathCache[pm] = resolved;
return resolved;
}
async function detectPackageManager(searchPaths) {
var _a, _b;
let pm = null;
const dedupedPaths = Array.from(new Set(searchPaths)); // reduce file operations, dedupe paths since primary use case has projectDir === appDir
const resolveIfYarn = (pm, version, cwd) => (pm === PM.YARN ? detectYarnBerry(cwd, version) : pm);
for (const dir of dedupedPaths) {
const packageJsonPath = path.join(dir, "package.json");
const packageManager = (await (0, builder_util_1.exists)(packageJsonPath)) ? (_a = (await fs.readJson(packageJsonPath, "utf8"))) === null || _a === void 0 ? void 0 : _a.packageManager : undefined;
if (packageManager) {
const [pm, version] = packageManager.split("@");
if (Object.values(PM).includes(pm)) {
const resolvedPackageManager = await resolveIfYarn(pm, version, dir);
return { pm: resolvedPackageManager, corepackConfig: packageManager, resolvedDirectory: dir, detectionMethod: "packageManager field" };
}
}
pm = await detectPackageManagerByFile(dir);
if (pm) {
const resolvedPackageManager = await resolveIfYarn(pm, "", dir);
return { pm: resolvedPackageManager, resolvedDirectory: dir, corepackConfig: undefined, detectionMethod: "lock file" };
}
}
pm = detectPackageManagerByEnv() || PM.NPM;
const cwd = process.env.npm_package_json ? path.dirname(process.env.npm_package_json) : ((_b = process.env.INIT_CWD) !== null && _b !== void 0 ? _b : process.cwd());
const resolvedPackageManager = await resolveIfYarn(pm, "", cwd);
builder_util_1.log.info({ resolvedPackageManager, detected: cwd }, "packageManager not detected by file, falling back to environment detection");
return { pm: resolvedPackageManager, resolvedDirectory: undefined, corepackConfig: undefined, detectionMethod: "process environment" };
}
function detectPackageManagerByEnv() {
const priorityChecklist = [(key) => { var _a; return (_a = process.env.npm_config_user_agent) === null || _a === void 0 ? void 0 : _a.includes(key); }, (key) => { var _a; return (_a = process.env.npm_execpath) === null || _a === void 0 ? void 0 : _a.includes(key); }];
const pms = Object.values(PM).filter(pm => pm !== PM.YARN_BERRY);
for (const checker of priorityChecklist) {
for (const pm of pms) {
if (checker(pm)) {
return pm;
}
}
}
return null;
}
async function detectPackageManagerByFile(dir) {
const has = (file) => (0, builder_util_1.exists)(path.join(dir, file));
const detected = [];
if (await has("yarn.lock")) {
detected.push(PM.YARN);
}
if (await has("pnpm-lock.yaml")) {
detected.push(PM.PNPM);
}
if (await has("package-lock.json")) {
detected.push(PM.NPM);
}
if ((await has("bun.lock")) || (await has("bun.lockb"))) {
detected.push(PM.BUN);
}
if (detected.length === 1) {
return detected[0];
}
return null;
}
async function detectYarnBerry(cwd, version) {
var _a, _b, _c;
const checkBerry = () => {
try {
if (parseInt(version.split(".")[0]) > 1) {
return PM.YARN_BERRY;
}
}
catch (_error) {
builder_util_1.log.debug({ error: _error }, "cannot determine yarn version, assuming yarn v1");
// If `yarn` is not found or another error occurs, fall back to the regular Yarn since we're already determined in a Yarn project
}
return undefined;
};
if (version === "latest" || version === "berry") {
return PM.YARN_BERRY;
}
if (version.length > 0) {
return (_a = checkBerry()) !== null && _a !== void 0 ? _a : PM.YARN;
}
const lockPath = path.join(cwd, "yarn.lock");
if (!(await (0, builder_util_1.exists)(lockPath))) {
return (_b = checkBerry()) !== null && _b !== void 0 ? _b : PM.YARN;
}
// Read the first few lines of yarn.lock to determine the version
const firstBytes = (await fs.readFile(lockPath, "utf8")).split("\n").slice(0, 10).join("\n");
// Yarn v2+ (Berry) has a "__metadata:" block near the top
if (firstBytes.includes("__metadata:")) {
return PM.YARN_BERRY;
}
// Yarn v1 format is classic semi-YAML with comment header
if (firstBytes.includes("DO NOT EDIT THIS FILE DIRECTLY.")) {
return PM.YARN;
}
return (_c = checkBerry()) !== null && _c !== void 0 ? _c : PM.YARN;
}
//# sourceMappingURL=packageManager.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
import { NodeModulesCollector } from "./nodeModulesCollector";
import { PM } from "./packageManager";
import { PnpmDependency } from "./types";
export declare class PnpmNodeModulesCollector extends NodeModulesCollector<PnpmDependency, PnpmDependency> {
readonly installOptions: {
manager: PM;
lockfile: string;
};
protected getArgs(): string[];
protected extractProductionDependencyGraph(tree: PnpmDependency, dependencyId: string): Promise<void>;
protected collectAllDependencies(tree: PnpmDependency): Promise<void>;
protected parseDependenciesTree(jsonBlob: string): PnpmDependency;
}
@@ -0,0 +1,77 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PnpmNodeModulesCollector = void 0;
const builder_util_1 = require("builder-util");
const nodeModulesCollector_1 = require("./nodeModulesCollector");
const packageManager_1 = require("./packageManager");
class PnpmNodeModulesCollector extends nodeModulesCollector_1.NodeModulesCollector {
constructor() {
super(...arguments);
this.installOptions = {
manager: packageManager_1.PM.PNPM,
lockfile: "pnpm-lock.yaml",
};
}
getArgs() {
return ["list", "--prod", "--json", "--depth", "Infinity", "--silent", "--loglevel=error"];
}
async extractProductionDependencyGraph(tree, dependencyId) {
if (this.productionGraph[dependencyId]) {
return;
}
this.productionGraph[dependencyId] = { dependencies: [] };
const packageName = tree.name || tree.from;
const { packageJson } = (await this.cache.locatePackageVersion({ pkgName: packageName, parentDir: this.rootDir, requiredRange: tree.version })) || {};
const all = packageJson ? { ...packageJson.dependencies, ...packageJson.optionalDependencies } : { ...tree.dependencies, ...tree.optionalDependencies };
const optional = packageJson ? { ...packageJson.optionalDependencies } : {};
const deps = { ...(tree.dependencies || {}), ...(tree.optionalDependencies || {}) };
this.productionGraph[dependencyId] = { dependencies: [] };
const depPromises = Object.entries(deps).map(async ([packageName, dependency]) => {
// First check if it's in production dependencies
if (!all[packageName]) {
return undefined;
}
// Then check if optional dependency path exists (using actual resolved path)
if (optional[packageName]) {
const pkg = await this.cache.locatePackageVersion({ pkgName: packageName, parentDir: this.rootDir, requiredRange: dependency.version });
if (!pkg) {
builder_util_1.log.debug({ name: packageName, version: dependency.version, path: dependency.path }, `optional dependency doesn't exist, skipping - likely not installed`);
return undefined;
}
}
const { id: childDependencyId, pkgOverride } = this.normalizePackageVersion(packageName, dependency);
await this.extractProductionDependencyGraph(pkgOverride, childDependencyId);
return childDependencyId;
});
const collectedDependencies = [];
for (const dep of depPromises) {
const result = await dep;
if (result !== undefined) {
collectedDependencies.push(result);
}
}
this.productionGraph[dependencyId] = { dependencies: collectedDependencies };
}
async collectAllDependencies(tree) {
var _a, _b;
// Collect regular dependencies
for (const [key, value] of Object.entries(tree.dependencies || {})) {
const pkg = await this.cache.locatePackageVersion({ pkgName: key, parentDir: this.rootDir, requiredRange: value.version });
this.allDependencies.set(`${key}@${value.version}`, { ...value, path: (_a = pkg === null || pkg === void 0 ? void 0 : pkg.packageDir) !== null && _a !== void 0 ? _a : value.path });
await this.collectAllDependencies(value);
}
// Collect optional dependencies if they exist
for (const [key, value] of Object.entries(tree.optionalDependencies || {})) {
const pkg = await this.cache.locatePackageVersion({ pkgName: key, parentDir: this.rootDir, requiredRange: value.version });
this.allDependencies.set(`${key}@${value.version}`, { ...value, path: (_b = pkg === null || pkg === void 0 ? void 0 : pkg.packageDir) !== null && _b !== void 0 ? _b : value.path });
await this.collectAllDependencies(value);
}
}
parseDependenciesTree(jsonBlob) {
// pnpm returns an array of dependency trees
const dependencyTree = this.extractJsonFromPollutedOutput(jsonBlob);
return dependencyTree[0];
}
}
exports.PnpmNodeModulesCollector = PnpmNodeModulesCollector;
//# sourceMappingURL=pnpmNodeModulesCollector.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,18 @@
import { NodeModulesCollector } from "./nodeModulesCollector";
import { PM } from "./packageManager.js";
import { TraversedDependency } from "./types.js";
export declare class TraversalNodeModulesCollector extends NodeModulesCollector<TraversedDependency, TraversedDependency> {
installOptions: {
manager: PM;
lockfile: string;
};
protected getArgs(): string[];
protected getDependenciesTree(_pm: PM): Promise<TraversedDependency>;
protected collectAllDependencies(tree: TraversedDependency, appPackageName: string): Promise<void>;
protected extractProductionDependencyGraph(tree: TraversedDependency, dependencyId: string): Promise<void>;
/**
* Builds a dependency tree using only package.json dependencies and optionalDependencies.
* This skips devDependencies and uses Node.js module resolution (require.resolve).
*/
private buildNodeModulesTreeManually;
}
@@ -0,0 +1,122 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TraversalNodeModulesCollector = void 0;
const builder_util_1 = require("builder-util");
const path = require("path");
const moduleManager_1 = require("./moduleManager");
const nodeModulesCollector_1 = require("./nodeModulesCollector");
const packageManager_js_1 = require("./packageManager.js");
// manual traversal of node_modules for package managers without CLI support for dependency tree extraction (e.g., bun) OR as a fallback (e.g. corepack enabled w/ strict mode)
class TraversalNodeModulesCollector extends nodeModulesCollector_1.NodeModulesCollector {
constructor() {
super(...arguments);
this.installOptions = {
manager: packageManager_js_1.PM.TRAVERSAL,
lockfile: "none",
};
}
getArgs() {
return [];
}
getDependenciesTree(_pm) {
builder_util_1.log.info(null, "using manual traversal of node_modules to build dependency tree");
return this.buildNodeModulesTreeManually(this.rootDir, undefined);
}
async collectAllDependencies(tree, appPackageName) {
for (const [packageKey, value] of Object.entries({ ...tree.dependencies, ...tree.optionalDependencies })) {
const normalizedDep = this.normalizePackageVersion(packageKey, value);
this.allDependencies.set(normalizedDep.id, normalizedDep.pkgOverride);
await this.collectAllDependencies(value, appPackageName);
}
}
// we don't need to check optional dependencies here because they're pre-processed in `buildNodeModulesTreeManually`
async extractProductionDependencyGraph(tree, dependencyId) {
if (this.productionGraph[dependencyId]) {
return;
}
this.productionGraph[dependencyId] = { dependencies: [] };
const prodDependencies = { ...(tree.dependencies || {}), ...(tree.optionalDependencies || {}) };
const collectedDependencies = [];
for (const packageName in prodDependencies) {
const dependency = prodDependencies[packageName];
const { id: childDependencyId, pkgOverride } = this.normalizePackageVersion(packageName, dependency);
await this.extractProductionDependencyGraph(pkgOverride, childDependencyId);
collectedDependencies.push(childDependencyId);
}
this.productionGraph[dependencyId] = { dependencies: collectedDependencies };
}
/**
* Builds a dependency tree using only package.json dependencies and optionalDependencies.
* This skips devDependencies and uses Node.js module resolution (require.resolve).
*/
async buildNodeModulesTreeManually(baseDir, aliasName) {
// Track visited packages by their resolved path to prevent infinite loops
const visited = new Set();
const resolvedBaseDir = await this.cache.realPath[baseDir];
/**
* Recursively builds dependency tree starting from a package directory.
* @param packageDir - The directory of the package to process
* @param aliasName - Optional alias name for npm aliased dependencies (e.g., "foo": "npm:@scope/bar@1.0.0")
* When provided, this name is used instead of the package.json name for the module name,
* ensuring the package is copied to the correct location in node_modules.
*/
const buildFromPackage = async (packageDir, aliasName) => {
const pkgPath = path.join(packageDir, "package.json");
if (!(await this.cache.exists[pkgPath])) {
throw new Error(`package.json not found at ${pkgPath}`);
}
const pkg = (await this.cache.json[pkgPath]);
const resolvedPackageDir = await this.cache.realPath[packageDir];
// Use the alias name if provided, otherwise fall back to the package.json name
// This ensures npm aliased packages are copied to the correct location
const moduleName = aliasName !== null && aliasName !== void 0 ? aliasName : pkg.name;
// Use resolved path as the unique identifier to prevent circular dependencies
if (visited.has(resolvedPackageDir)) {
builder_util_1.log.debug({ name: moduleName, version: pkg.version, path: resolvedPackageDir }, "skipping already visited package");
return {
name: moduleName,
version: pkg.version,
path: resolvedPackageDir,
};
}
visited.add(resolvedPackageDir);
const buildPackage = async (dependencies, nullHandler) => {
const builtPackages = {};
for (const [depName, depVersion] of Object.entries(dependencies || {})) {
const pkg = await this.locatePackageWithVersion({ name: depName, version: depVersion, path: resolvedPackageDir });
const logFields = { parent: moduleName, dependency: depName, version: depVersion };
if (pkg == null) {
nullHandler(depName, depVersion);
continue;
}
// Skip if this dependency resolves to the base directory or any parent we're already processing
if (pkg.packageDir === resolvedPackageDir || pkg.packageDir === resolvedBaseDir) {
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_SELF_REF].push(`${depName}@${depVersion}`);
continue;
}
builder_util_1.log.debug(logFields, "processing production dependency");
builtPackages[depName] = await buildFromPackage(pkg.packageDir, depName);
}
return builtPackages;
};
const prodDeps = await buildPackage(pkg.dependencies, (depName, version) => {
builder_util_1.log.error({ parent: moduleName, dependency: depName, version }, "production dependency not found");
throw new Error(`Production dependency ${depName} not found for package ${moduleName}`);
});
const optionalDeps = await buildPackage(pkg.optionalDependencies, (depName, version) => {
builder_util_1.log.debug({ parent: moduleName, dependency: depName }, "optional dependency not installed, skipping");
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_OPTIONAL_NOT_INSTALLED].push(`${depName}@${version}`);
});
return {
name: moduleName,
version: pkg.version,
path: resolvedPackageDir,
dependencies: Object.keys(prodDeps).length > 0 ? prodDeps : undefined,
optionalDependencies: Object.keys(optionalDeps).length > 0 ? optionalDeps : undefined,
};
};
return buildFromPackage(baseDir, aliasName);
}
}
exports.TraversalNodeModulesCollector = TraversalNodeModulesCollector;
//# sourceMappingURL=traversalNodeModulesCollector.js.map
File diff suppressed because one or more lines are too long
+51
View File
@@ -0,0 +1,51 @@
export type PackageJson = {
name: string;
version: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
workspaces?: string[] | {
packages: string[];
};
};
export interface NodeModuleInfo {
name: string;
version: string;
dir: string;
dependencies?: Array<NodeModuleInfo>;
}
export type ParsedDependencyTree = {
readonly name: string;
readonly version: string;
readonly path: string;
readonly workspaces?: string[] | {
packages: string[];
};
};
export interface PnpmDependency extends Dependency<PnpmDependency, PnpmDependency> {
readonly from: string;
readonly resolved: string;
}
export interface NpmDependency extends Dependency<NpmDependency, string> {
readonly resolved?: string;
readonly _dependencies?: {
[packageName: string]: string;
};
}
export interface TraversedDependency extends Dependency<TraversedDependency, TraversedDependency> {
}
export type Dependency<T, V> = Dependencies<T, V> & ParsedDependencyTree;
export type Dependencies<T, V> = {
readonly dependencies?: {
[packageName: string]: T;
};
readonly optionalDependencies?: {
[packageName: string]: V;
};
};
export interface DependencyGraph {
[packageNameAndVersion: string]: {
readonly dependencies: string[];
};
}
+3
View File
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/node-module-collector/types.ts"],"names":[],"mappings":"","sourcesContent":["export type PackageJson = {\n name: string\n version: string\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n peerDependencies?: Record<string, string>\n optionalDependencies?: Record<string, string>\n workspaces?: string[] | { packages: string[] }\n}\n\nexport interface NodeModuleInfo {\n name: string\n version: string\n dir: string\n dependencies?: Array<NodeModuleInfo>\n}\n\nexport type ParsedDependencyTree = {\n readonly name: string\n readonly version: string\n readonly path: string\n readonly workspaces?: string[] | { packages: string[] } // we only use this at root level\n}\n\n// Note: `PnpmDependency` and `NpmDependency` include the output of `JSON.parse(...)` of `pnpm list` and `npm list` respectively\n// This object has a TON of info - a majority, if not all, of each dependency's package.json\n// We extract only what we need when constructing DependencyTree in `extractProductionDependencyTree`\nexport interface PnpmDependency extends Dependency<PnpmDependency, PnpmDependency> {\n readonly from: string\n readonly resolved: string\n}\n\nexport interface NpmDependency extends Dependency<NpmDependency, string> {\n readonly resolved?: string\n // implicit dependencies, returned only through `npm list`\n readonly _dependencies?: {\n [packageName: string]: string\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface TraversedDependency extends Dependency<TraversedDependency, TraversedDependency> {}\n\nexport type Dependency<T, V> = Dependencies<T, V> & ParsedDependencyTree\n\nexport type Dependencies<T, V> = {\n readonly dependencies?: {\n [packageName: string]: T\n }\n readonly optionalDependencies?: {\n [packageName: string]: V\n }\n}\n\nexport interface DependencyGraph {\n [packageNameAndVersion: string]: {\n readonly dependencies: string[]\n }\n}\n"]}
@@ -0,0 +1,15 @@
import { Lazy } from "lazy-val";
import { NpmNodeModulesCollector } from "./npmNodeModulesCollector";
import { PM } from "./packageManager";
import { NpmDependency } from "./types";
export declare class YarnBerryNodeModulesCollector extends NpmNodeModulesCollector {
readonly installOptions: {
manager: PM;
lockfile: string;
};
private yarnSetupInfo;
protected isHoisted: Lazy<boolean>;
protected getDependenciesTree(_pm: PM): Promise<NpmDependency>;
protected isProdDependency(packageName: string, tree: NpmDependency): boolean;
private detectYarnSetup;
}
@@ -0,0 +1,93 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.YarnBerryNodeModulesCollector = void 0;
const builder_util_1 = require("builder-util");
const lazy_val_1 = require("lazy-val");
const npmNodeModulesCollector_1 = require("./npmNodeModulesCollector");
const packageManager_1 = require("./packageManager");
// Only Yarn v1 uses CLI. We should use pnp.cjs for PnP, but we can't access the files due to virtual file paths within zipped modules.
// We fallback to npm node module collection (since Yarn Berry could have npm-like structure OR pnpm-like structure, depending on `nmHoistingLimits` configuration).
// In the latter case, we still can't assume `pnpm` is installed, so we still try to use npm collection as a best-effort attempt.
// If those fail, such as if using corepack, we attempt to manually build the tree.
class YarnBerryNodeModulesCollector extends npmNodeModulesCollector_1.NpmNodeModulesCollector {
constructor() {
super(...arguments);
this.installOptions = {
manager: packageManager_1.PM.YARN_BERRY,
lockfile: "yarn.lock",
};
this.yarnSetupInfo = new lazy_val_1.Lazy(async () => this.detectYarnSetup(this.rootDir));
this.isHoisted = new lazy_val_1.Lazy(async () => this.yarnSetupInfo.value.then(info => info.isHoisted));
}
async getDependenciesTree(_pm) {
const isPnp = await this.yarnSetupInfo.value.then(info => !!info.isPnP);
if (isPnp) {
builder_util_1.log.warn(null, "Yarn PnP extraction not supported directly due to virtual file paths (<package_name>.zip/<file_path>), utilizing NPM node module collector");
}
return super.getDependenciesTree(packageManager_1.PM.NPM);
}
isProdDependency(packageName, tree) {
var _a, _b;
return super.isProdDependency(packageName, tree) || ((_a = tree.dependencies) === null || _a === void 0 ? void 0 : _a[packageName]) != null || ((_b = tree.optionalDependencies) === null || _b === void 0 ? void 0 : _b[packageName]) != null;
}
async detectYarnSetup(rootDir) {
var _a, _b, _c, _d;
let yarnVersion = null;
let nodeLinker = null;
let nmHoistingLimits = null;
const output = await this.asyncExec("yarn", ["config", "--json"], rootDir);
if (!output.stdout) {
builder_util_1.log.debug(null, "Yarn config returned no output, assuming default Yarn v1 behavior (hoisted, non-PnP)");
return {
yarnVersion,
nodeLinker,
nmHoistingLimits,
isPnP: false,
isHoisted: true,
};
}
// Yarn 1: multi-line stream with type:"inspect" (not used in this file anyways)
// Yarn 23: multi-line stream with type:"inspect"
// Yarn 4: single JSON object, no "type"
const lines = output.stdout
.split("\n")
.map(l => l.trim())
.filter(Boolean);
let data = null;
for (const line of lines) {
try {
const parsed = JSON.parse(line);
// Yarn 4: direct object
if (parsed.rc || parsed.manifest) {
data = parsed;
break;
}
// Yarn 13: inspect event
if (parsed.type === "inspect") {
data = parsed.data;
break;
}
}
catch {
// ignore non-JSON lines
}
}
if (data) {
const rc = data.rc || data; // Yarn 4: rc in root; Yarn 23: rc inside data
yarnVersion = (_b = (_a = data.manifest) === null || _a === void 0 ? void 0 : _a.version) !== null && _b !== void 0 ? _b : null;
nodeLinker = (_c = rc.nodeLinker) !== null && _c !== void 0 ? _c : null;
nmHoistingLimits = (_d = rc.nmHoistingLimits) !== null && _d !== void 0 ? _d : null;
}
const isPnP = nodeLinker === "pnp";
const isHoisted = !isPnP && (nmHoistingLimits === "dependencies" || nmHoistingLimits === "workspaces");
return {
yarnVersion,
nodeLinker,
nmHoistingLimits,
isPnP,
isHoisted,
};
}
}
exports.YarnBerryNodeModulesCollector = YarnBerryNodeModulesCollector;
//# sourceMappingURL=yarnBerryNodeModulesCollector.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
import { NpmNodeModulesCollector } from "./npmNodeModulesCollector";
import { PM } from "./packageManager";
import { NpmDependency } from "./types";
export declare class YarnNodeModulesCollector extends NpmNodeModulesCollector {
readonly installOptions: {
manager: PM;
lockfile: string;
};
protected getDependenciesTree(_pm: PM): Promise<NpmDependency>;
}
@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.YarnNodeModulesCollector = void 0;
const npmNodeModulesCollector_1 = require("./npmNodeModulesCollector");
const packageManager_1 = require("./packageManager");
// Yarn Classic (v1) produces a hoisted node_modules structure similar to npm.
// Instead of parsing Yarn's custom NDJSON output, we leverage npm's list command
// which NpmNodeModulesCollector already handles.
class YarnNodeModulesCollector extends npmNodeModulesCollector_1.NpmNodeModulesCollector {
constructor() {
super(...arguments);
this.installOptions = {
manager: packageManager_1.PM.YARN,
lockfile: "yarn.lock",
};
}
async getDependenciesTree(_pm) {
return super.getDependenciesTree(packageManager_1.PM.NPM);
}
}
exports.YarnNodeModulesCollector = YarnNodeModulesCollector;
//# sourceMappingURL=yarnNodeModulesCollector.js.map
@@ -0,0 +1 @@
{"version":3,"file":"yarnNodeModulesCollector.js","sourceRoot":"","sources":["../../src/node-module-collector/yarnNodeModulesCollector.ts"],"names":[],"mappings":";;;AAAA,uEAAmE;AACnE,qDAAqC;AAGrC,8EAA8E;AAC9E,iFAAiF;AACjF,iDAAiD;AACjD,MAAa,wBAAyB,SAAQ,iDAAuB;IAArE;;QACkB,mBAAc,GAAG;YAC/B,OAAO,EAAE,mBAAE,CAAC,IAAI;YAChB,QAAQ,EAAE,WAAW;SACtB,CAAA;IAKH,CAAC;IAHW,KAAK,CAAC,mBAAmB,CAAC,GAAO;QACzC,OAAO,KAAK,CAAC,mBAAmB,CAAC,mBAAE,CAAC,GAAG,CAAC,CAAA;IAC1C,CAAC;CACF;AATD,4DASC","sourcesContent":["import { NpmNodeModulesCollector } from \"./npmNodeModulesCollector\"\nimport { PM } from \"./packageManager\"\nimport { NpmDependency } from \"./types\"\n\n// Yarn Classic (v1) produces a hoisted node_modules structure similar to npm.\n// Instead of parsing Yarn's custom NDJSON output, we leverage npm's list command\n// which NpmNodeModulesCollector already handles.\nexport class YarnNodeModulesCollector extends NpmNodeModulesCollector {\n public readonly installOptions = {\n manager: PM.YARN,\n lockfile: \"yarn.lock\",\n }\n\n protected async getDependenciesTree(_pm: PM): Promise<NpmDependency> {\n return super.getDependenciesTree(PM.NPM)\n }\n}\n"]}