update electron to v43

This commit is contained in:
olcxja 2026-07-09 22:38:33 +02:00
commit fb6c8b6ee9
5385 changed files with 513060 additions and 123058 deletions

View file

@ -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>;
}

View file

@ -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

View file

@ -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"]}

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 {};

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

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>;
}>;

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

View file

@ -0,0 +1,93 @@
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_DUPLICATE_REF_UNRESOLVED = "unresolved 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_OPTIONAL_PLATFORM_NOT_INSTALLED = "platform-specific optional dependencies not bundled \u2014 add them to your project's optionalDependencies if your app requires them (pnpm 10+ does not auto-install transitive platform binaries)",
PKG_COLLECTOR_OUTPUT = "collector stderr output",
PKG_VERSION_OVERRIDDEN = "dependencies resolved to a version outside the declared range (installed version accepted \u2014 likely resolved via package manager overrides)"
}
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, skipDownwardSearch, }: {
/**
* The directory to start searching from. Typed optional because pnpm JSON output can omit
* the `path` field at runtime even when the TypeScript type says `string`. An undefined
* parentDir is treated as "package not found" rather than a crash.
*/
parentDir?: string;
/**
* The package name to locate. Typed optional for the same reason as parentDir: the pnpm
* list JSON can omit `name`/`from` fields (e.g. when the root package.json has no name),
* producing an undefined pkgName at runtime despite the TypeScript type.
*/
pkgName?: string;
requiredRange?: string;
/**
* When true, skip the BFS-based `downwardSearch`. Use for layouts that are guaranteed flat
* (e.g. pnpm's `.pnpm` virtual store), where the downward walk burns thousands of `readdir`
* / `lstat` calls and finds nothing.
*/
skipDownwardSearch?: boolean;
}): Promise<Package | null>;
private searchForPackage;
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;
/** Handle a non-scoped entry directory inside a node_modules folder. */
private processEntry;
/** Handle a scoped-package directory (@scope) inside a node_modules folder. */
private processScope;
/** Enqueue a node_modules directory for BFS only if it exists on disk and hasn't been visited. */
private enqueueIfExists;
}
export {};

View file

@ -0,0 +1,321 @@
"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_DUPLICATE_REF_UNRESOLVED"] = "unresolved 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_OPTIONAL_PLATFORM_NOT_INSTALLED"] = "platform-specific optional dependencies not bundled \u2014 add them to your project's optionalDependencies if your app requires them (pnpm 10+ does not auto-install transitive platform binaries)";
LogMessageByKey["PKG_COLLECTOR_OUTPUT"] = "collector stderr output";
LogMessageByKey["PKG_VERSION_OVERRIDDEN"] = "dependencies resolved to a version outside the declared range (installed version accepted \u2014 likely resolved via package manager overrides)";
})(LogMessageByKey || (exports.LogMessageByKey = LogMessageByKey = {}));
exports.logMessageLevelByKey = {
[LogMessageByKey.PKG_DUPLICATE_REF]: "info",
[LogMessageByKey.PKG_DUPLICATE_REF_UNRESOLVED]: "warn",
[LogMessageByKey.PKG_NOT_FOUND]: "warn",
[LogMessageByKey.PKG_NOT_ON_DISK]: "warn",
[LogMessageByKey.PKG_SELF_REF]: "debug",
[LogMessageByKey.PKG_OPTIONAL_NOT_INSTALLED]: "info",
[LogMessageByKey.PKG_OPTIONAL_PLATFORM_NOT_INSTALLED]: "warn",
[LogMessageByKey.PKG_COLLECTOR_OUTPUT]: "warn",
[LogMessageByKey.PKG_VERSION_OVERRIDDEN]: "debug",
};
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, skipDownwardSearch = false, }) {
if (!parentDir || !pkgName) {
return null;
}
const found = await this.searchForPackage(parentDir, pkgName, requiredRange, skipDownwardSearch);
if (found) {
return found;
}
// Second pass: find any installed version of the package when the declared-range search
// returns nothing. This handles package manager `overrides` (Bun, npm, pnpm, Yarn) that
// resolve a transitive dependency to a version intentionally outside its declared range.
// File-system results are already cached, so this pass costs only JS overhead.
if (requiredRange) {
const overrideResult = await this.searchForPackage(parentDir, pkgName, undefined, skipDownwardSearch);
if (overrideResult) {
builder_util_1.log.debug({ pkg: pkgName, declared: requiredRange, installed: overrideResult.packageJson.version }, "accepting installed package version that doesn't satisfy the declared range");
this.logSummary[LogMessageByKey.PKG_VERSION_OVERRIDDEN].push(`${pkgName}@${overrideResult.packageJson.version} (declared ${requiredRange})`);
return overrideResult;
}
}
return null;
}
async searchForPackage(parentDir, pkgName, requiredRange, skipDownwardSearch) {
// 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
const upward = await this.upwardSearch(parentDir, pkgName, requiredRange);
if (upward) {
return upward;
}
if (skipDownwardSearch) {
return null;
}
return (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;
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);
if (entry.startsWith("@")) {
const found = await this.processScope(entryPath, pkgName, requiredRange, depth, visited, queue);
if (found) {
return found;
}
continue;
}
const found = await this.processEntry(entryPath, pkgName, requiredRange, depth, visited, queue);
if (found) {
return found;
}
}
}
return null;
}
/** Handle a non-scoped entry directory inside a node_modules folder. */
async processEntry(entryPath, pkgName, requiredRange, depth, visited, queue) {
const stat = await this.lstat[entryPath];
if (!(stat === null || stat === void 0 ? void 0 : stat.isDirectory())) {
return null;
}
// Check entry/node_modules/pkgName (nested dep under a package)
const nested = path.join(entryPath, "node_modules", pkgName, "package.json");
if (await this.exists[nested]) {
const json = await this.json[nested];
if (json && this.semverSatisfies(json.version, requiredRange)) {
return { packageDir: path.dirname(nested), packageJson: json };
}
}
// Check entry/pkgName directly (some flat layouts place pkgName as a sibling)
const direct = path.join(entryPath, 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 };
}
}
await this.enqueueIfExists(path.join(entryPath, "node_modules"), depth, visited, queue);
return null;
}
/** Handle a scoped-package directory (@scope) inside a node_modules folder. */
async processScope(scopePath, pkgName, requiredRange, depth, visited, queue) {
var _a;
if (!(await this.exists[scopePath]) || !((_a = (await this.lstat[scopePath])) === null || _a === void 0 ? void 0 : _a.isDirectory())) {
return null;
}
let scopeEntries;
try {
scopeEntries = await fs.readdir(scopePath);
}
catch {
return null;
}
for (const sc of scopeEntries) {
const scPath = path.join(scopePath, sc);
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 };
}
}
await this.enqueueIfExists(path.join(scPath, "node_modules"), depth, visited, queue);
}
return null;
}
/** Enqueue a node_modules directory for BFS only if it exists on disk and hasn't been visited. */
async enqueueIfExists(nmPath, depth, visited, queue) {
var _a;
if (!visited.has(nmPath) && (await this.exists[nmPath]) && ((_a = (await this.lstat[nmPath])) === null || _a === void 0 ? void 0 : _a.isDirectory())) {
visited.add(nmPath);
queue.push({ dir: nmPath, depth: depth + 1 });
}
}
}
exports.ModuleManager = ModuleManager;
//# sourceMappingURL=moduleManager.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,137 @@
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;
protected logMissingDependency(pkgName: string): void;
protected 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. On Windows, wraps the invocation in `powershell.exe -EncodedCommand` (UTF-16LE
* base64) to avoid spawning `.cmd` shims directly and to eliminate shell-injection surface area.
* 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
*/
protected streamCollectorCommandToFile(command: string, args: string[], cwd: string, tempOutputFile: string): Promise<void>;
}
/**
* Build the argv for invoking a Windows command through `powershell.exe -EncodedCommand`.
*
* Each token is wrapped in a PowerShell single-quoted string (with embedded single quotes doubled),
* so no character is interpreted by a shell. The script:
* - pins `[Console]::OutputEncoding` to UTF-8 *without* a BOM so the JSON dependency tree is not
* corrupted by the console's OEM code page (and no BOM is prepended to break `JSON.parse`),
* - invokes the command via the call operator `&`,
* - re-emits the command's own exit code via `exit $LASTEXITCODE` (e.g. `npm list` returns 1 in
* expected scenarios, which the caller's shouldIgnore logic relies on).
*
* The whole script is base64-encoded as UTF-16LE per PowerShell's `-EncodedCommand` contract.
*/
export declare function buildPowerShellEncodedArgs(command: string, args: string[]): string[];

View file

@ -0,0 +1,428 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeModulesCollector = void 0;
exports.buildPowerShellEncodedArgs = buildPowerShellEncodedArgs;
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 node-linker setting; assuming non-hoisted (virtual store) layout");
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, _b;
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;
}
// Both indicate truncated/polluted PM output (a transient that re-running the command clears).
if (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes("Unexpected end of JSON input")) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes("No JSON content found in output"))) {
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) {
let at;
if (identifier.startsWith("@")) {
// Scoped package: find the version separator after the scope (e.g. "@scope/pkg@1.2.3")
const slashIndex = identifier.indexOf("/");
if (slashIndex === -1) {
return { name: identifier, version: "unknown" };
}
at = identifier.indexOf("@", slashIndex + 1);
}
else {
at = identifier.indexOf("@");
}
if (at <= 0) {
return { name: identifier, version: "unknown" };
}
return { name: identifier.slice(0, at), version: identifier.slice(at + 1) };
}
/**
* 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}`;
// Normalize the path to handle mixed separators from pnpm JSON output on Windows
const rawPath = (_a = this.allDependencies.get(key)) === null || _a === void 0 ? void 0 : _a.path;
const p = rawPath != null ? path.normalize(rawPath) : undefined;
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.logMissingDependency(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));
}
logMissingDependency(pkgName) {
const PLATFORM_PACKAGE_RE = /(linux|win32|darwin|freebsd|android)[-_](x64|arm64|ia32|arm|ppc64|s390x|loong64|riscv64|universal)/;
const diskLogKey = PLATFORM_PACKAGE_RE.test(pkgName) ? moduleManager_1.LogMessageByKey.PKG_OPTIONAL_PLATFORM_NOT_INSTALLED : moduleManager_1.LogMessageByKey.PKG_NOT_ON_DISK;
this.cache.logSummary[diskLogKey].push(pkgName);
}
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. On Windows, wraps the invocation in `powershell.exe -EncodedCommand` (UTF-16LE
* base64) to avoid spawning `.cmd` shims directly and to eliminate shell-injection surface area.
* 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) {
// Derive execName from the original command so the npm-list shouldIgnore check below keys off the
// real invocation (e.g. "npm"), not the "powershell" wrapper we spawn on Windows.
const execName = path.basename(command, path.extname(command));
// On Windows the package-manager command is typically a `.cmd` shim (npm.cmd/pnpm.cmd/yarn.cmd),
// which Node can no longer spawn directly (CVE-2024-27980). Rather than spawn with `shell: true` —
// which emits the DEP0190 "args with shell" deprecation warning and forces manual metacharacter
// escaping — wrap the invocation in a single PowerShell `-EncodedCommand`. The base64 (UTF-16LE)
// payload sidesteps every shell-quoting layer, and `powershell.exe` is a real executable we spawn
// directly with no shell. See buildPowerShellEncodedArgs for the UTF-8 / exit-code handling.
const [spawnCommand, spawnArgs] = process.platform === "win32" ? ["powershell.exe", buildPowerShellEncodedArgs(command, args)] : [command, args];
await new Promise((resolve, reject) => {
const outStream = (0, fs_extra_1.createWriteStream)(tempOutputFile);
const child = childProcess.spawn(spawnCommand, spawnArgs, {
cwd,
// Package manager invocations do not need signing/publishing credentials.
env: { COREPACK_ENABLE_STRICT: "0", ...(0, builder_util_1.stripSensitiveEnvVars)(process.env) },
});
let stderr = "";
// The process can close before all piped stdout has been flushed to disk. Resolving on the
// child's "close" alone races the write stream and lets the caller read a TRUNCATED file
// (manifesting as "No JSON content found in output"). Gate the settle on BOTH the child exit
// (for the code/stderr) and the write stream's "finish" (all bytes flushed).
let exitCode = null;
let childClosed = false;
let streamFinished = false;
let settled = false;
// `pipe` ends `outStream` when stdout EOFs, which triggers its "finish" once flushed.
child.stdout.pipe(outStream);
child.stderr.on("data", chunk => {
stderr += chunk.toString();
});
const fail = (err) => {
if (settled) {
return;
}
settled = true;
// Best-effort cleanup: stop the child and close the stream so we don't
// waste CPU writing to a broken fd after rejection.
try {
child.kill();
}
catch {
// ignore
}
try {
outStream.destroy();
}
catch {
// ignore
}
reject(err);
};
const settle = () => {
if (settled || !childClosed || !streamFinished) {
return;
}
const code = exitCode;
// 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");
// Only surface stderr as a user-visible warning when the exit code itself is unexpected.
// When shouldIgnore is true (npm list exit code 1) the stderr is an anticipated side
// effect of package-manager features like yarn resolutions or npm overrides that cause
// npm to report ELSPROBLEMS for aliased packages it considers "invalid".
if (!shouldIgnore) {
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_COLLECTOR_OUTPUT].push(stderr);
}
}
settled = true;
const shouldResolve = code === 0 || shouldIgnore;
return shouldResolve ? resolve() : reject(new Error(`Node module collector process exited with code ${code}:\n${stderr}`));
};
outStream.on("error", err => fail(new Error(`Node module collector failed writing output (${command}): ${err.message}`)));
outStream.on("finish", () => {
streamFinished = true;
settle();
});
child.on("error", err => {
fail(new Error(`Node module collector spawn (${command} ${JSON.stringify(args)}) failed: ${err.message}`));
});
child.on("close", code => {
exitCode = code;
childClosed = true;
settle();
});
});
}
}
exports.NodeModulesCollector = NodeModulesCollector;
/**
* Build the argv for invoking a Windows command through `powershell.exe -EncodedCommand`.
*
* Each token is wrapped in a PowerShell single-quoted string (with embedded single quotes doubled),
* so no character is interpreted by a shell. The script:
* - pins `[Console]::OutputEncoding` to UTF-8 *without* a BOM so the JSON dependency tree is not
* corrupted by the console's OEM code page (and no BOM is prepended to break `JSON.parse`),
* - invokes the command via the call operator `&`,
* - re-emits the command's own exit code via `exit $LASTEXITCODE` (e.g. `npm list` returns 1 in
* expected scenarios, which the caller's shouldIgnore logic relies on).
*
* The whole script is base64-encoded as UTF-16LE per PowerShell's `-EncodedCommand` contract.
*/
function buildPowerShellEncodedArgs(command, args) {
const psQuote = (value) => `'${value.replace(/'/g, "''")}'`;
const invocation = ["&", psQuote(command), ...args.map(psQuote)].join(" ");
const script = `[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false); ${invocation}; exit $LASTEXITCODE`;
const encoded = Buffer.from(script, "utf16le").toString("base64");
return ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded];
}
//# sourceMappingURL=nodeModulesCollector.js.map

File diff suppressed because one or more lines are too long

View file

@ -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;
}

View file

@ -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

View file

@ -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 {};

View file

@ -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

View file

@ -0,0 +1,44 @@
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;
};
private _allWorkspacePackages;
private _pnpmMajorVersion;
private readonly pnpmVersion;
/**
* Memo for `locateFromDepOrRoot`, keyed by `name@version`. pnpm's content-addressed virtual
* store guarantees that any given `name@version` resolves to a single location on disk, so
* once we've resolved a package we can short-circuit every subsequent lookup. This is the
* dominant speedup for large workspaces where the `pnpm list --json` tree contains the same
* `name@version` thousands of times (one entry per dependent).
*/
private readonly locateMemo;
/**
* Visited set for `collectDepsRecursively`, keyed by `name@version`. Without this we re-walk
* every shared subtree of the pnpm list output, exploding work in deep workspaces.
*/
private readonly collectedDeps;
/**
* Returns the workspace packages to iterate over, gated by detected pnpm version:
* - pnpm v11+: multi-entry workspace output return the full parsed array
* - pnpm < v11 / non-workspace / detection failure: single-tree behavior return only [0]
*/
private get allWorkspacePackages();
protected getArgs(): string[];
/**
* Locate a package version, preferring the dep's own reported path before falling back to rootDir.
* This is critical for pnpm non-hoisted (virtual store) setups where each package has its own
* nested node_modules. Searching only from rootDir can resolve the wrong version when multiple
* versions of a dep exist in the workspace.
*/
private locateFromDepOrRoot;
protected extractProductionDependencyGraph(tree: PnpmDependency, dependencyId: string): Promise<void>;
protected collectAllDependencies(_tree: PnpmDependency, _appPackageName: string): Promise<void>;
private collectDepsRecursively;
protected getTreeFromWorkspaces(tree: PnpmDependency, packageName: string): PnpmDependency;
protected parseDependenciesTree(jsonBlob: string): Promise<PnpmDependency>;
}

View file

@ -0,0 +1,191 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PnpmNodeModulesCollector = void 0;
const lazy_val_1 = require("lazy-val");
const moduleManager_1 = require("./moduleManager");
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",
};
// Raw backing field — all entries from `pnpm list --json`
this._allWorkspacePackages = [];
// Cached after parseDependenciesTree resolves the Lazy; 0 = safe default (treated as < v11)
this._pnpmMajorVersion = 0;
// Runs `pnpm --version` once and caches the major version number
this.pnpmVersion = new lazy_val_1.Lazy(async () => {
var _a;
const result = await this.asyncExec((0, packageManager_1.getPackageManagerCommand)(packageManager_1.PM.PNPM), ["--version"]);
const major = parseInt(((_a = result.stdout) !== null && _a !== void 0 ? _a : "0").split(".")[0], 10);
return isNaN(major) ? 0 : major;
});
/**
* Memo for `locateFromDepOrRoot`, keyed by `name@version`. pnpm's content-addressed virtual
* store guarantees that any given `name@version` resolves to a single location on disk, so
* once we've resolved a package we can short-circuit every subsequent lookup. This is the
* dominant speedup for large workspaces where the `pnpm list --json` tree contains the same
* `name@version` thousands of times (one entry per dependent).
*/
this.locateMemo = new Map();
/**
* Visited set for `collectDepsRecursively`, keyed by `name@version`. Without this we re-walk
* every shared subtree of the pnpm list output, exploding work in deep workspaces.
*/
this.collectedDeps = new Set();
}
/**
* Returns the workspace packages to iterate over, gated by detected pnpm version:
* - pnpm v11+: multi-entry workspace output return the full parsed array
* - pnpm < v11 / non-workspace / detection failure: single-tree behavior return only [0]
*/
get allWorkspacePackages() {
if (this._pnpmMajorVersion >= 11) {
return this._allWorkspacePackages;
}
return this._allWorkspacePackages.slice(0, 1);
}
getArgs() {
return ["list", "--prod", "--json", "--depth", "Infinity", "--silent", "--loglevel=error"];
}
/**
* Locate a package version, preferring the dep's own reported path before falling back to rootDir.
* This is critical for pnpm non-hoisted (virtual store) setups where each package has its own
* nested node_modules. Searching only from rootDir can resolve the wrong version when multiple
* versions of a dep exist in the workspace.
*/
async locateFromDepOrRoot(pkgName, parentPath, requiredRange) {
// pnpm's virtual store is content-addressed: every `name@version` lookup is deterministic,
// so memoize on the exact version. `requiredRange` is normally an exact version coming from
// the pnpm list output (e.g. `value.version`), which makes this cache hit on duplicates.
// Only memoize when we have a concrete version — semver ranges could resolve differently
// depending on what's installed at `parentPath` vs root, so skip the cache for those.
const memoKey = requiredRange && /^\d/.test(requiredRange) ? `${pkgName}@${requiredRange}` : null;
if (memoKey != null) {
const cached = this.locateMemo.get(memoKey);
if (cached != null) {
return cached;
}
}
// pnpm's default `.pnpm` virtual store is flat, so `downwardSearch` would burn thousands
// of `readdir`/`lstat` calls finding nothing. With `nodeLinker: hoisted`, however, the
// layout is a traditional nested `node_modules` tree where version-conflicted packages
// land at `<root>/node_modules/A/node_modules/B` — downward BFS is needed to find them.
const skipDownwardSearch = !(await this.isHoisted.value);
const promise = (async () => {
const fromDep = parentPath ? await this.cache.locatePackageVersion({ pkgName, parentDir: parentPath, requiredRange, skipDownwardSearch }) : null;
if (fromDep) {
return fromDep;
}
return this.cache.locatePackageVersion({ pkgName, parentDir: this.rootDir, requiredRange, skipDownwardSearch });
})();
if (memoKey != null) {
this.locateMemo.set(memoKey, promise);
}
return promise;
}
// pnpm 10+ does not automatically preserve transitive optional platform-specific
// packages (e.g. sass-embedded-linux-x64) across lock file regeneration. Users
// must list them as direct optionalDependencies. Missing ones are emitted as
// PKG_OPTIONAL_PLATFORM_NOT_INSTALLED warnings in the log summary.
async extractProductionDependencyGraph(tree, dependencyId) {
var _a;
if (this.productionGraph[dependencyId]) {
return;
}
this.productionGraph[dependencyId] = { dependencies: [] };
if (((_a = tree.dedupedDependenciesCount) !== null && _a !== void 0 ? _a : 0) > 0) {
const realDep = this.allDependencies.get(dependencyId);
if (realDep) {
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_DUPLICATE_REF].push(dependencyId);
tree = realDep;
}
else {
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_DUPLICATE_REF_UNRESOLVED].push(dependencyId);
return;
}
}
const packageName = tree.name || tree.from;
const { packageJson } = (await this.locateFromDepOrRoot(packageName, tree.path, 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.locateFromDepOrRoot(packageName, tree.path, dependency.version);
if (!pkg) {
this.logMissingDependency(`${packageName}@${dependency.version}`);
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, _appPackageName) {
for (const root of this.allWorkspacePackages) {
await this.collectDepsRecursively(root);
}
}
async collectDepsRecursively(tree) {
const visit = async (key, value) => {
var _a, _b;
if (((_a = value === null || value === void 0 ? void 0 : value.dedupedDependenciesCount) !== null && _a !== void 0 ? _a : 0) > 0) {
return;
}
const id = `${key}@${value.version}`;
// The pnpm list output can include the same `name@version` thousands of times across a
// deep workspace; without this guard we re-resolve and re-recurse each occurrence.
if (this.collectedDeps.has(id)) {
return;
}
this.collectedDeps.add(id);
const pkg = await this.locateFromDepOrRoot(key, value.path, value.version);
this.allDependencies.set(id, { ...value, path: (_b = pkg === null || pkg === void 0 ? void 0 : pkg.packageDir) !== null && _b !== void 0 ? _b : value.path });
await this.collectDepsRecursively(value);
};
for (const [key, value] of Object.entries(tree.dependencies || {})) {
await visit(key, value);
}
for (const [key, value] of Object.entries(tree.optionalDependencies || {})) {
await visit(key, value);
}
}
getTreeFromWorkspaces(tree, packageName) {
// pnpm v10 workspace: app is nested as a dependency of root — handled by base class
const result = super.getTreeFromWorkspaces(tree, packageName);
if (result !== tree) {
return result;
}
// pnpm v11 workspace: each workspace package is a separate top-level array entry;
// non-workspace (single-tree): find returns the one entry or undefined → falls back to tree
const match = this.allWorkspacePackages.find(pkg => pkg.name === packageName || pkg.from === packageName);
return match !== null && match !== void 0 ? match : tree;
}
async parseDependenciesTree(jsonBlob) {
const dependencyTree = this.extractJsonFromPollutedOutput(jsonBlob);
this._allWorkspacePackages = dependencyTree;
this._pnpmMajorVersion = await this.pnpmVersion.value;
return dependencyTree[0];
}
}
exports.PnpmNodeModulesCollector = PnpmNodeModulesCollector;
//# sourceMappingURL=pnpmNodeModulesCollector.js.map

File diff suppressed because one or more lines are too long

View file

@ -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;
}

View file

@ -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

View file

@ -0,0 +1,52 @@
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;
readonly dedupedDependenciesCount?: number;
}
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[];
};
}

View file

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=types.js.map

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 // Present in pnpm 10.29.3+ `pnpm list --json` output for deduped references.\n // Context: https://github.com/pnpm/pnpm/issues/10601\n readonly dedupedDependenciesCount?: number\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"]}

View file

@ -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;
}

View file

@ -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

View file

@ -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>;
}

View file

@ -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

View file

@ -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"]}