Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
Vendored
+10
@@ -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>;
|
||||
}
|
||||
Generated
Vendored
+18
@@ -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
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"bunNodeModulesCollector.js","sourceRoot":"","sources":["../../src/node-module-collector/bunNodeModulesCollector.ts"],"names":[],"mappings":";;;AAAA,+CAAkC;AAClC,qDAAqC;AAErC,mFAA+E;AAE/E,MAAa,uBAAwB,SAAQ,6DAA6B;IAA1E;;QACkB,mBAAc,GAAG,EAAE,OAAO,EAAE,mBAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAA;IAM5E,CAAC;IAJW,KAAK,CAAC,mBAAmB,CAAC,EAAM;QACxC,kBAAG,CAAC,IAAI,CAAC,IAAI,EAAE,+GAA+G,CAAC,CAAA;QAC/H,OAAO,KAAK,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAA;IACtC,CAAC;CACF;AAPD,0DAOC","sourcesContent":["import { log } from \"builder-util\"\nimport { PM } from \"./packageManager\"\nimport { TraversedDependency } from \"./types\"\nimport { TraversalNodeModulesCollector } from \"./traversalNodeModulesCollector\"\n\nexport class BunNodeModulesCollector extends TraversalNodeModulesCollector {\n public readonly installOptions = { manager: PM.BUN, lockfile: \"bun.lock\" }\n\n protected async getDependenciesTree(pm: PM): Promise<TraversedDependency> {\n log.info(null, \"note: bun does not support any CLI for dependency tree extraction, utilizing file traversal collector instead\")\n return super.getDependenciesTree(pm)\n }\n}\n"]}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2016-present, Yarn Contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
/**
|
||||
* High-level node_modules hoisting algorithm recipe
|
||||
*
|
||||
* 1. Take input dependency graph and start traversing it,
|
||||
* as you visit new node in the graph - clone it if there can be multiple paths
|
||||
* to access the node from the graph root to the node, e.g. essentially represent
|
||||
* the graph with a tree as you go, to make hoisting possible.
|
||||
* 2. You want to hoist every node possible to the top root node first,
|
||||
* then to each of its children etc, so you need to keep track what is your current
|
||||
* root node into which you are hoisting
|
||||
* 3. Traverse the dependency graph from the current root node and for each package name
|
||||
* that can be potentially hoisted to the current root node build a list of idents
|
||||
* in descending hoisting preference. You will check in next steps whether most preferred ident
|
||||
* for the given package name can be hoisted first, and if not, then you check the
|
||||
* less preferred ident, etc, until either some ident will be hoisted
|
||||
* or you run out of idents to check
|
||||
* (no need to convert the graph to the tree when you build this preference map).
|
||||
* 4. The children of the root node are already "hoisted", so you need to start
|
||||
* from the dependencies of these children. You take some child and
|
||||
* sort its dependencies so that regular dependencies without peer dependencies
|
||||
* will come first and then those dependencies that peer depend on them.
|
||||
* This is needed to make algorithm more efficient and hoist nodes which are easier
|
||||
* to hoist first and then handle peer dependent nodes.
|
||||
* 5. You take this sorted list of dependencies and check if each of them can be
|
||||
* hoisted to the current root node. To answer is the node can be hoisted you check
|
||||
* your constraints - require promise and peer dependency promise.
|
||||
* The possible answers can be: YES - the node is hoistable to the current root,
|
||||
* NO - the node is not hoistable to the current root
|
||||
* and DEPENDS - the node is hoistable to the root if nodes X, Y, Z are hoistable
|
||||
* to the root. The case DEPENDS happens when all the require and other
|
||||
* constraints are met, except peer dependency constraints. Note, that the nodes
|
||||
* that are not package idents currently at the top of preference list are considered
|
||||
* to have the answer NO right away, before doing any other constraint checks.
|
||||
* 6. When you have hoistable answer for each dependency of a node you then build
|
||||
* a list of nodes that are NOT hoistable. These are the nodes that have answer NO
|
||||
* and the nodes that DEPENDS on these nodes. All the other nodes are hoistable,
|
||||
* those that have answer YES and those that have answer DEPENDS,
|
||||
* because they are cyclically dependent on each another
|
||||
* 7. You hoist all the hoistable nodes to the current root and continue traversing
|
||||
* the tree. Note, you need to track newly added nodes to the current root,
|
||||
* because after you finished tree traversal you want to come back to these new nodes
|
||||
* first thing and hoist everything from each of them to the current tree root.
|
||||
* 8. After you have finished traversing newly hoisted current root nodes
|
||||
* it means you cannot hoist anything to the current tree root and you need to pick
|
||||
* the next node as current tree root and run the algorithm again
|
||||
* until you run out of candidates for current tree root.
|
||||
*/
|
||||
type PackageName = string;
|
||||
export declare enum HoisterDependencyKind {
|
||||
REGULAR = 0,
|
||||
WORKSPACE = 1,
|
||||
EXTERNAL_SOFT_LINK = 2
|
||||
}
|
||||
export type HoisterTree = {
|
||||
name: PackageName;
|
||||
identName: PackageName;
|
||||
reference: string;
|
||||
dependencies: Set<HoisterTree>;
|
||||
peerNames: Set<PackageName>;
|
||||
hoistPriority?: number;
|
||||
dependencyKind?: HoisterDependencyKind;
|
||||
};
|
||||
export type HoisterResult = {
|
||||
name: PackageName;
|
||||
identName: PackageName;
|
||||
references: Set<string>;
|
||||
dependencies: Set<HoisterResult>;
|
||||
};
|
||||
type Locator = string;
|
||||
declare enum DebugLevel {
|
||||
NONE = -1,
|
||||
PERF = 0,
|
||||
CHECK = 1,
|
||||
REASONS = 2,
|
||||
INTENSIVE_CHECK = 9
|
||||
}
|
||||
export type HoistOptions = {
|
||||
/** Runs self-checks after hoisting is finished */
|
||||
check?: boolean;
|
||||
/** Debug level */
|
||||
debugLevel?: DebugLevel;
|
||||
/** Hoist borders are defined by parent node locator and its dependency name. The dependency is considered a border, nothing can be hoisted past this dependency, but dependency can be hoisted */
|
||||
hoistingLimits?: Map<Locator, Set<PackageName>>;
|
||||
};
|
||||
/**
|
||||
* Hoists package tree.
|
||||
*
|
||||
* The root node of a tree must has id: '.'.
|
||||
* This function does not mutate its arguments, it hoists and returns tree copy.
|
||||
*
|
||||
* @param tree package tree (cycles in the tree are allowed)
|
||||
*
|
||||
* @returns hoisted tree copy
|
||||
*/
|
||||
export declare const hoist: (tree: HoisterTree, opts?: HoistOptions) => HoisterResult;
|
||||
export {};
|
||||
+884
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+18
@@ -0,0 +1,18 @@
|
||||
import { Nullish } from "builder-util-runtime";
|
||||
import { TmpDir } from "temp-file";
|
||||
import { NpmNodeModulesCollector } from "./npmNodeModulesCollector";
|
||||
import { getPackageManagerCommand, PM } from "./packageManager";
|
||||
import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector";
|
||||
import { BunNodeModulesCollector } from "./bunNodeModulesCollector";
|
||||
import { Lazy } from "lazy-val";
|
||||
import { TraversalNodeModulesCollector } from "./traversalNodeModulesCollector";
|
||||
export { getPackageManagerCommand, PM };
|
||||
export declare function getCollectorByPackageManager(pm: PM, rootDir: string, tempDirManager: TmpDir): NpmNodeModulesCollector | PnpmNodeModulesCollector | TraversalNodeModulesCollector | BunNodeModulesCollector;
|
||||
export declare const determinePackageManagerEnv: ({ projectDir, appDir, workspaceRoot }: {
|
||||
projectDir: string;
|
||||
appDir: string;
|
||||
workspaceRoot: string | Nullish;
|
||||
}) => Lazy<{
|
||||
pm: PM;
|
||||
workspaceRoot: Promise<string | undefined>;
|
||||
}>;
|
||||
+130
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+67
@@ -0,0 +1,67 @@
|
||||
import { LogLevel } from "builder-util";
|
||||
import { PackageJson } from "./types";
|
||||
import * as fs from "fs-extra";
|
||||
export declare enum LogMessageByKey {
|
||||
PKG_DUPLICATE_REF = "duplicate dependency references",
|
||||
PKG_NOT_FOUND = "cannot find path for dependency",
|
||||
PKG_NOT_ON_DISK = "dependency not found on disk",
|
||||
PKG_SELF_REF = "self-referential dependencies",
|
||||
PKG_OPTIONAL_NOT_INSTALLED = "missing optional dependencies",
|
||||
PKG_COLLECTOR_OUTPUT = "collector stderr output"
|
||||
}
|
||||
export declare const logMessageLevelByKey: Record<LogMessageByKey, LogLevel>;
|
||||
export type Package = {
|
||||
packageDir: string;
|
||||
packageJson: PackageJson;
|
||||
};
|
||||
type JsonCache = Record<string, Promise<PackageJson | null>>;
|
||||
type RealPathCache = Record<string, Promise<string>>;
|
||||
type ExistsCache = Record<string, Promise<boolean>>;
|
||||
type LstatCache = Record<string, Promise<fs.Stats | null>>;
|
||||
type PackageCache = Record<string, Promise<Package | null>>;
|
||||
type LogSummaryCache = Record<LogMessageByKey, string[]>;
|
||||
export declare class ModuleManager {
|
||||
/** Cache for package.json contents (readJson) */
|
||||
readonly json: JsonCache;
|
||||
/** Cache for resolved real paths (if symlink, realpath; otherwise resolve) */
|
||||
readonly realPath: RealPathCache;
|
||||
/** Cache for file/directory existence checks */
|
||||
readonly exists: ExistsCache;
|
||||
/** Cache for lstat results */
|
||||
readonly lstat: LstatCache;
|
||||
/** Cache for package lookups (key: "packageName||fromDir||semverRange"). Use helper function `versionedCacheKey` */
|
||||
readonly packageData: PackageCache;
|
||||
/** For logging purposes, just track all dependencies for each key */
|
||||
readonly logSummary: LogSummaryCache;
|
||||
private readonly jsonMap;
|
||||
private readonly realPathMap;
|
||||
private readonly existsMap;
|
||||
private readonly lstatMap;
|
||||
private readonly packageDataMap;
|
||||
private readonly logSummaryMap;
|
||||
constructor();
|
||||
private createLogSummarySyncProxy;
|
||||
private createAsyncProxy;
|
||||
versionedCacheKey(pkg: {
|
||||
name: string;
|
||||
path: string;
|
||||
semver?: string;
|
||||
}): string;
|
||||
protected locatePackageVersionFromCacheKey(key: string): Promise<Package | null>;
|
||||
locatePackageVersion({ parentDir, pkgName, requiredRange }: {
|
||||
parentDir: string;
|
||||
pkgName: string;
|
||||
requiredRange?: string;
|
||||
}): Promise<Package | null>;
|
||||
private semverSatisfies;
|
||||
/**
|
||||
* Upward search (hoisted)
|
||||
*/
|
||||
private upwardSearch;
|
||||
/**
|
||||
* Breadth-first downward search from parentDir/node_modules
|
||||
* Looks for node_modules/\*\/node_modules/pkgName (and deeper)
|
||||
*/
|
||||
private downwardSearch;
|
||||
}
|
||||
export {};
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ModuleManager = exports.logMessageLevelByKey = exports.LogMessageByKey = void 0;
|
||||
const builder_util_1 = require("builder-util");
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const semver = require("semver");
|
||||
var LogMessageByKey;
|
||||
(function (LogMessageByKey) {
|
||||
LogMessageByKey["PKG_DUPLICATE_REF"] = "duplicate dependency references";
|
||||
LogMessageByKey["PKG_NOT_FOUND"] = "cannot find path for dependency";
|
||||
LogMessageByKey["PKG_NOT_ON_DISK"] = "dependency not found on disk";
|
||||
LogMessageByKey["PKG_SELF_REF"] = "self-referential dependencies";
|
||||
LogMessageByKey["PKG_OPTIONAL_NOT_INSTALLED"] = "missing optional dependencies";
|
||||
LogMessageByKey["PKG_COLLECTOR_OUTPUT"] = "collector stderr output";
|
||||
})(LogMessageByKey || (exports.LogMessageByKey = LogMessageByKey = {}));
|
||||
exports.logMessageLevelByKey = {
|
||||
[LogMessageByKey.PKG_DUPLICATE_REF]: "info",
|
||||
[LogMessageByKey.PKG_NOT_FOUND]: "warn",
|
||||
[LogMessageByKey.PKG_NOT_ON_DISK]: "warn",
|
||||
[LogMessageByKey.PKG_SELF_REF]: "debug",
|
||||
[LogMessageByKey.PKG_OPTIONAL_NOT_INSTALLED]: "info",
|
||||
[LogMessageByKey.PKG_COLLECTOR_OUTPUT]: "warn",
|
||||
};
|
||||
class ModuleManager {
|
||||
constructor() {
|
||||
this.jsonMap = new Map();
|
||||
this.realPathMap = new Map();
|
||||
this.existsMap = new Map();
|
||||
this.lstatMap = new Map();
|
||||
this.packageDataMap = new Map();
|
||||
this.logSummaryMap = new Map();
|
||||
this.logSummary = this.createLogSummarySyncProxy();
|
||||
this.exists = this.createAsyncProxy(this.existsMap, (p) => (0, builder_util_1.exists)(p));
|
||||
this.json = this.createAsyncProxy(this.jsonMap, (p) => fs.readJson(p).catch(() => null));
|
||||
this.lstat = this.createAsyncProxy(this.lstatMap, (p) => fs.lstat(p).catch(() => null));
|
||||
this.packageData = this.createAsyncProxy(this.packageDataMap, (p) => this.locatePackageVersionFromCacheKey(p).catch(() => null));
|
||||
this.realPath = this.createAsyncProxy(this.realPathMap, async (p) => {
|
||||
const filePath = path.resolve(p);
|
||||
const stat = await this.lstat[filePath];
|
||||
return (stat === null || stat === void 0 ? void 0 : stat.isSymbolicLink()) ? fs.realpath(filePath) : filePath;
|
||||
});
|
||||
}
|
||||
createLogSummarySyncProxy() {
|
||||
return new Proxy({}, {
|
||||
get: (_, key) => {
|
||||
if (!this.logSummaryMap.has(key)) {
|
||||
this.logSummaryMap.set(key, []);
|
||||
}
|
||||
return this.logSummaryMap.get(key);
|
||||
},
|
||||
set: (_, key, value) => {
|
||||
this.logSummaryMap.set(key, value);
|
||||
return true;
|
||||
},
|
||||
has: (_, key) => {
|
||||
return this.logSummaryMap.has(key);
|
||||
},
|
||||
// Add these to make Object.entries() work
|
||||
ownKeys: _ => {
|
||||
return Array.from(this.logSummaryMap.keys());
|
||||
},
|
||||
getOwnPropertyDescriptor: (_, key) => {
|
||||
if (this.logSummaryMap.has(key)) {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
}
|
||||
// this allows dot-notation access while still supporting async retrieval
|
||||
// e.g., cache.packageJson[somePath] returns Promise<PackageJson>
|
||||
createAsyncProxy(map, compute) {
|
||||
return new Proxy({}, {
|
||||
async get(_, key) {
|
||||
if (map.has(key)) {
|
||||
return Promise.resolve(map.get(key));
|
||||
}
|
||||
return await Promise.resolve(compute(key)).then(value => {
|
||||
map.set(key, value);
|
||||
return value;
|
||||
});
|
||||
},
|
||||
set(_, key, value) {
|
||||
map.set(key, value);
|
||||
return true;
|
||||
},
|
||||
has(_, key) {
|
||||
return map.has(key);
|
||||
},
|
||||
});
|
||||
}
|
||||
versionedCacheKey(pkg) {
|
||||
return [pkg.name, pkg.path, pkg.semver || ""].join("||");
|
||||
}
|
||||
async locatePackageVersionFromCacheKey(key) {
|
||||
const [name, fromDir, semverRange] = key.split("||");
|
||||
const result = await this.locatePackageVersion({ parentDir: fromDir, pkgName: name, requiredRange: semverRange });
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
return { ...result, packageDir: await this.realPath[result.packageDir] };
|
||||
}
|
||||
async locatePackageVersion({ parentDir, pkgName, requiredRange }) {
|
||||
// 1) check direct parent node_modules/pkgName first
|
||||
const direct = path.join(path.resolve(parentDir), "node_modules", pkgName, "package.json");
|
||||
if (await this.exists[direct]) {
|
||||
const json = await this.json[direct];
|
||||
if (json && this.semverSatisfies(json.version, requiredRange)) {
|
||||
return { packageDir: path.dirname(direct), packageJson: json };
|
||||
}
|
||||
}
|
||||
// 2) upward hoisted search, then 3) downward non-hoisted search
|
||||
return (await this.upwardSearch(parentDir, pkgName, requiredRange)) || (await this.downwardSearch(parentDir, pkgName, requiredRange)) || null;
|
||||
}
|
||||
semverSatisfies(found, range) {
|
||||
if ((0, builder_util_1.isEmptyOrSpaces)(range) || range === "*") {
|
||||
return true;
|
||||
}
|
||||
if (range === found) {
|
||||
return true;
|
||||
}
|
||||
if (semver.validRange(range) == null) {
|
||||
// ignore, we can't verify non-semver ranges
|
||||
// e.g. git urls, file:, patch:, etc. Example:
|
||||
// "@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.43#~/.yarn/patches/@ai-sdk-google-npm-2.0.43-689ed559b3.patch"
|
||||
builder_util_1.log.debug({ found, range }, "unable to validate semver version range, assuming match");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return semver.satisfies(found, range);
|
||||
}
|
||||
catch {
|
||||
// fallback: simple equality or basic prefix handling (^, ~)
|
||||
if (range.startsWith("^") || range.startsWith("~")) {
|
||||
const r = range.slice(1);
|
||||
return r === found;
|
||||
}
|
||||
// if range is like "8.x" or "8.*" match major
|
||||
const m = range.match(/^(\d+)[.(*|x)]*/);
|
||||
const fm = found.match(/^(\d+)\./);
|
||||
if (m && fm) {
|
||||
return m[1] === fm[1];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Upward search (hoisted)
|
||||
*/
|
||||
async upwardSearch(parentDir, pkgName, requiredRange) {
|
||||
let current = path.resolve(parentDir);
|
||||
const root = path.parse(current).root;
|
||||
while (true) {
|
||||
const candidate = path.join(current, "node_modules", pkgName, "package.json");
|
||||
if (await this.exists[candidate]) {
|
||||
const json = await this.json[candidate];
|
||||
if (json && this.semverSatisfies(json.version, requiredRange)) {
|
||||
return { packageDir: path.dirname(candidate), packageJson: json };
|
||||
}
|
||||
// otherwise keep searching upward (we may find a different hoisted version)
|
||||
}
|
||||
if (current === root) {
|
||||
break;
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Breadth-first downward search from parentDir/node_modules
|
||||
* Looks for node_modules/\*\/node_modules/pkgName (and deeper)
|
||||
*/
|
||||
async downwardSearch(parentDir, pkgName, requiredRange, maxExplored = 2000, maxDepth = 6) {
|
||||
var _a, _b, _c, _d;
|
||||
const start = path.join(path.resolve(parentDir), "node_modules");
|
||||
if (!(await this.exists[start]) || !((_a = (await this.lstat[start])) === null || _a === void 0 ? void 0 : _a.isDirectory())) {
|
||||
return null;
|
||||
}
|
||||
const visited = new Set();
|
||||
const queue = [{ dir: start, depth: 0 }];
|
||||
let explored = 0;
|
||||
while (queue.length > 0) {
|
||||
const { dir, depth } = queue.shift();
|
||||
if (explored++ > maxExplored) {
|
||||
break;
|
||||
}
|
||||
if (depth > maxDepth) {
|
||||
continue;
|
||||
}
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(dir);
|
||||
}
|
||||
catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
const entryPath = path.join(dir, entry);
|
||||
// handle scoped packages @scope/name
|
||||
if (entry.startsWith("@")) {
|
||||
// queue the scope directory itself to explore its children
|
||||
if ((await this.exists[entryPath]) && ((_b = (await this.lstat[entryPath])) === null || _b === void 0 ? void 0 : _b.isDirectory())) {
|
||||
const scopeEntries = await fs.readdir(entryPath);
|
||||
for (const sc of scopeEntries) {
|
||||
const scPath = path.join(entryPath, sc);
|
||||
// check scPath/node_modules/pkgName
|
||||
const candidatePkgJson = path.join(scPath, "node_modules", pkgName, "package.json");
|
||||
if (await this.exists[candidatePkgJson]) {
|
||||
const json = await this.json[candidatePkgJson];
|
||||
if (json && this.semverSatisfies(json.version, requiredRange)) {
|
||||
return { packageDir: path.dirname(candidatePkgJson), packageJson: json };
|
||||
}
|
||||
}
|
||||
// enqueue scPath/node_modules to explore further
|
||||
const scNodeModules = path.join(scPath, "node_modules");
|
||||
if ((await this.exists[scNodeModules]) && ((_c = (await this.lstat[scNodeModules])) === null || _c === void 0 ? void 0 : _c.isDirectory())) {
|
||||
if (!visited.has(scNodeModules)) {
|
||||
visited.add(scNodeModules);
|
||||
queue.push({ dir: scNodeModules, depth: depth + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// check for direct candidate: entry/node_modules/pkgName
|
||||
try {
|
||||
const stat = await this.lstat[entryPath];
|
||||
if (!(stat === null || stat === void 0 ? void 0 : stat.isDirectory())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
continue;
|
||||
}
|
||||
const candidatePkgJson = path.join(entryPath, "node_modules", pkgName, "package.json");
|
||||
if (await this.exists[candidatePkgJson]) {
|
||||
const json = await this.json[candidatePkgJson];
|
||||
if (json && this.semverSatisfies(json.version, requiredRange)) {
|
||||
return { packageDir: path.dirname(candidatePkgJson), packageJson: json };
|
||||
}
|
||||
}
|
||||
// also check entry/node_modules directly for pkgName (some layouts)
|
||||
const candidateDirect = path.join(entryPath, pkgName, "package.json");
|
||||
if (await this.exists[candidateDirect]) {
|
||||
const json = await this.json[candidateDirect];
|
||||
if (json && this.semverSatisfies(json.version, requiredRange)) {
|
||||
return { packageDir: path.dirname(candidateDirect), packageJson: json };
|
||||
}
|
||||
}
|
||||
// enqueue entry/node_modules for deeper traversal
|
||||
const nextNodeModules = path.join(entryPath, "node_modules");
|
||||
if ((await this.exists[nextNodeModules]) && ((_d = (await this.lstat[nextNodeModules])) === null || _d === void 0 ? void 0 : _d.isDirectory())) {
|
||||
if (!visited.has(nextNodeModules)) {
|
||||
visited.add(nextNodeModules);
|
||||
queue.push({ dir: nextNodeModules, depth: depth + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
exports.ModuleManager = ModuleManager;
|
||||
//# sourceMappingURL=moduleManager.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+121
@@ -0,0 +1,121 @@
|
||||
import { TmpDir } from "builder-util";
|
||||
import { Lazy } from "lazy-val";
|
||||
import { ModuleManager } from "./moduleManager";
|
||||
import { PM } from "./packageManager";
|
||||
import type { Dependency, DependencyGraph, NodeModuleInfo, PackageJson } from "./types";
|
||||
export declare abstract class NodeModulesCollector<ProdDepType extends Dependency<ProdDepType, OptionalDepType>, OptionalDepType> {
|
||||
protected readonly rootDir: string;
|
||||
private readonly tempDirManager;
|
||||
private readonly nodeModules;
|
||||
protected readonly allDependencies: Map<string, ProdDepType>;
|
||||
protected readonly productionGraph: DependencyGraph;
|
||||
protected readonly cache: ModuleManager;
|
||||
protected isHoisted: Lazy<boolean>;
|
||||
constructor(rootDir: string, tempDirManager: TmpDir);
|
||||
/**
|
||||
* Retrieves and collects all Node.js modules for a given package.
|
||||
*
|
||||
* This method orchestrates the entire module collection process by:
|
||||
* 1. Fetching the dependency tree from the package manager
|
||||
* 2. Collecting all dependencies recursively
|
||||
* 3. Extracting workspace references if applicable
|
||||
* 4. Building a production dependency graph
|
||||
* 5. Hoisting the dependencies to their final locations
|
||||
* 6. Resolving and returning module information
|
||||
*/
|
||||
getNodeModules({ packageName }: {
|
||||
packageName: string;
|
||||
}): Promise<{
|
||||
nodeModules: NodeModuleInfo[];
|
||||
logSummary: ModuleManager["logSummary"];
|
||||
}>;
|
||||
abstract readonly installOptions: {
|
||||
manager: PM;
|
||||
lockfile: string;
|
||||
};
|
||||
protected abstract getArgs(): string[];
|
||||
protected abstract extractProductionDependencyGraph(tree: Dependency<ProdDepType, OptionalDepType>, dependencyId: string): Promise<void>;
|
||||
protected abstract collectAllDependencies(tree: Dependency<ProdDepType, OptionalDepType>, appPackageName: string): Promise<void>;
|
||||
/**
|
||||
* Retrieves the dependency tree from the package manager.
|
||||
*
|
||||
* Executes the appropriate package manager command to fetch the dependency tree and writes
|
||||
* the output to a temporary file. Includes retry logic to handle transient failures such as
|
||||
* incomplete JSON output or missing files. Will retry up to 1 time with exponential backoff.
|
||||
*/
|
||||
protected getDependenciesTree(pm: PM): Promise<ProdDepType>;
|
||||
/**
|
||||
* Parses the dependencies tree from shell command output.
|
||||
*
|
||||
**/
|
||||
protected parseDependenciesTree(shellOutput: string): ProdDepType | Promise<ProdDepType>;
|
||||
protected extractJsonFromPollutedOutput<T>(shellOutput: string): T;
|
||||
protected cacheKey(pkg: Pick<ProdDepType, "name" | "version" | "path">): string;
|
||||
protected normalizePackageVersion(key: string, pkg: ProdDepType): {
|
||||
id: string;
|
||||
pkgOverride: ProdDepType & {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Determines if a given dependency is a production dependency of a package.
|
||||
*
|
||||
* Checks both the dependencies and optionalDependencies of a package to see if
|
||||
* the specified dependency name is listed.
|
||||
*
|
||||
* @param depName - The name of the dependency to check
|
||||
* @param pkg - The package to search for the dependency in
|
||||
* @returns True if the dependency is found in either dependencies or optionalDependencies, false otherwise
|
||||
*/
|
||||
protected isProdDependency(depName: string, pkg: ProdDepType): boolean;
|
||||
protected locatePackageWithVersion(depTree: Pick<ProdDepType, "name" | "version" | "path">): Promise<{
|
||||
packageDir: string;
|
||||
packageJson: PackageJson;
|
||||
} | null>;
|
||||
/**
|
||||
* Parses a dependency identifier string into name and version components.
|
||||
*
|
||||
* Handles both scoped packages (e.g., "@scope/pkg@1.2.3") and regular packages (e.g., "pkg@1.2.3").
|
||||
* If the identifier is malformed or cannot be parsed, defaults to treating the entire string as
|
||||
* the package name with an "unknown" version.
|
||||
*/
|
||||
protected parseNameVersion(identifier: string): {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
/**
|
||||
* Retrieves the dependency tree and handles workspace package self-references.
|
||||
*
|
||||
* If the project is a workspace project, this method removes the root package's self-reference
|
||||
* from the dependency tree to avoid circular dependencies. It promotes the root package's
|
||||
* direct dependencies to the top level of the tree.
|
||||
*
|
||||
* @param tree - The original dependency tree
|
||||
* @param packageName - The name of the package to check for and remove from the tree
|
||||
* @returns The extracted dependency subtree
|
||||
*/
|
||||
protected getTreeFromWorkspaces(tree: ProdDepType, packageName: string): ProdDepType;
|
||||
private transformToHoisterTree;
|
||||
private _getNodeModules;
|
||||
asyncExec(command: string, args: string[], cwd?: string): Promise<{
|
||||
stdout: string | undefined;
|
||||
stderr: string | undefined;
|
||||
}>;
|
||||
/**
|
||||
* Executes a command and streams its output to a file.
|
||||
*
|
||||
* Spawns a child process to execute the specified command with arguments, capturing stdout
|
||||
* to a file. Handles Windows-specific quirks by wrapping .cmd files in a temporary .bat file
|
||||
* when necessary. Enables corepack strict mode by default but allows process.env overrides.
|
||||
*
|
||||
* Special handling for `npm list` exit code 1, which is expected in certain scenarios.
|
||||
*
|
||||
* @param command - The command to execute
|
||||
* @param args - Array of command-line arguments
|
||||
* @param cwd - The working directory to execute the command in
|
||||
* @param tempOutputFile - The path to the temporary file where stdout will be written
|
||||
* @returns Promise that resolves when the command completes successfully or rejects if it fails
|
||||
* @throws {Error} If the child process spawn fails or exits with a non-zero code
|
||||
*/
|
||||
streamCollectorCommandToFile(command: string, args: string[], cwd: string, tempOutputFile: string): Promise<void>;
|
||||
}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.NodeModulesCollector = void 0;
|
||||
const builder_util_1 = require("builder-util");
|
||||
const childProcess = require("child_process");
|
||||
const fs = require("fs-extra");
|
||||
const fs_extra_1 = require("fs-extra");
|
||||
const lazy_val_1 = require("lazy-val");
|
||||
const path = require("path");
|
||||
const hoist_1 = require("./hoist");
|
||||
const moduleManager_1 = require("./moduleManager");
|
||||
const packageManager_1 = require("./packageManager");
|
||||
class NodeModulesCollector {
|
||||
constructor(rootDir, tempDirManager) {
|
||||
this.rootDir = rootDir;
|
||||
this.tempDirManager = tempDirManager;
|
||||
this.nodeModules = [];
|
||||
this.allDependencies = new Map();
|
||||
this.productionGraph = {};
|
||||
this.cache = new moduleManager_1.ModuleManager();
|
||||
this.isHoisted = new lazy_val_1.Lazy(async () => {
|
||||
const { manager } = this.installOptions;
|
||||
const command = (0, packageManager_1.getPackageManagerCommand)(manager);
|
||||
const config = (await this.asyncExec(command, ["config", "list"])).stdout;
|
||||
if (config == null) {
|
||||
builder_util_1.log.debug({ manager }, "unable to determine if node_modules are hoisted: no config output. falling back to hoisted mode");
|
||||
return false;
|
||||
}
|
||||
const lines = Object.fromEntries(config.split("\n").map(line => line.split("=").map(s => s.trim())));
|
||||
if (lines["node-linker"] === "hoisted") {
|
||||
builder_util_1.log.debug({ manager }, "node_modules are hoisted");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Retrieves and collects all Node.js modules for a given package.
|
||||
*
|
||||
* This method orchestrates the entire module collection process by:
|
||||
* 1. Fetching the dependency tree from the package manager
|
||||
* 2. Collecting all dependencies recursively
|
||||
* 3. Extracting workspace references if applicable
|
||||
* 4. Building a production dependency graph
|
||||
* 5. Hoisting the dependencies to their final locations
|
||||
* 6. Resolving and returning module information
|
||||
*/
|
||||
async getNodeModules({ packageName }) {
|
||||
const tree = await this.getDependenciesTree(this.installOptions.manager);
|
||||
await this.collectAllDependencies(tree, packageName);
|
||||
const realTree = this.getTreeFromWorkspaces(tree, packageName);
|
||||
await this.extractProductionDependencyGraph(realTree, packageName);
|
||||
const hoisterResult = (0, hoist_1.hoist)(this.transformToHoisterTree(this.productionGraph, packageName), {
|
||||
check: builder_util_1.log.isDebugEnabled,
|
||||
});
|
||||
await this._getNodeModules(hoisterResult.dependencies, this.nodeModules);
|
||||
builder_util_1.log.debug({ packageName, depCount: this.nodeModules.length }, "node modules collection complete");
|
||||
return { nodeModules: this.nodeModules, logSummary: this.cache.logSummary };
|
||||
}
|
||||
/**
|
||||
* Retrieves the dependency tree from the package manager.
|
||||
*
|
||||
* Executes the appropriate package manager command to fetch the dependency tree and writes
|
||||
* the output to a temporary file. Includes retry logic to handle transient failures such as
|
||||
* incomplete JSON output or missing files. Will retry up to 1 time with exponential backoff.
|
||||
*/
|
||||
async getDependenciesTree(pm) {
|
||||
const command = (0, packageManager_1.getPackageManagerCommand)(pm);
|
||||
const args = this.getArgs();
|
||||
const tempOutputFile = await this.tempDirManager.getTempFile({
|
||||
prefix: path.basename(command, path.extname(command)),
|
||||
suffix: "output.json",
|
||||
});
|
||||
return (0, builder_util_1.retry)(async () => {
|
||||
await this.streamCollectorCommandToFile(command, args, this.rootDir, tempOutputFile);
|
||||
const shellOutput = await fs.readFile(tempOutputFile, { encoding: "utf8" });
|
||||
const result = await Promise.resolve(this.parseDependenciesTree(shellOutput));
|
||||
return result;
|
||||
}, {
|
||||
retries: 1,
|
||||
interval: 2000,
|
||||
backoff: 2000,
|
||||
shouldRetry: async (error) => {
|
||||
var _a;
|
||||
const fields = { error: error.message, tempOutputFile, cwd: this.rootDir, packageManager: pm };
|
||||
if (!(await (0, builder_util_1.exists)(tempOutputFile))) {
|
||||
builder_util_1.log.debug(fields, "dependency tree output file missing, retrying");
|
||||
return true;
|
||||
}
|
||||
const fileContent = await fs.readFile(tempOutputFile, { encoding: "utf8" });
|
||||
fields.fileContentLength = fileContent.length.toString();
|
||||
if (fileContent.trim().length === 0) {
|
||||
builder_util_1.log.debug(fields, "dependency tree output file empty, retrying");
|
||||
return true;
|
||||
}
|
||||
// extract small start/end sample for debugging purposes (e.g. polluted console output)
|
||||
const lines = fileContent.split("\n");
|
||||
const lineSampleSize = Math.min(5, lines.length / 2);
|
||||
if (2 * lineSampleSize > 5) {
|
||||
fields.sampleStart = lines.slice(0, lineSampleSize).join("\n");
|
||||
fields.sampleEnd = lines.slice(-lineSampleSize).join("\n");
|
||||
}
|
||||
else {
|
||||
fields.content = fileContent;
|
||||
}
|
||||
if ((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes("Unexpected end of JSON input")) {
|
||||
builder_util_1.log.debug(fields, "JSON parse error in dependency tree, retrying");
|
||||
return true;
|
||||
}
|
||||
builder_util_1.log.error(fields, "error parsing dependencies tree");
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Parses the dependencies tree from shell command output.
|
||||
*
|
||||
**/
|
||||
parseDependenciesTree(shellOutput) {
|
||||
return this.extractJsonFromPollutedOutput(shellOutput);
|
||||
}
|
||||
extractJsonFromPollutedOutput(shellOutput) {
|
||||
const consoleOutput = shellOutput.trim();
|
||||
try {
|
||||
// Please for the love of all that is holy, this should cover 99% of cases where npm/pnpm/yarn output is clean JSON
|
||||
return JSON.parse(consoleOutput);
|
||||
}
|
||||
catch {
|
||||
// ignore
|
||||
}
|
||||
// DEDICATED FALLBACK FOR POLLUTED OUTPUT, non-trivial to implement correctly, not needed in most cases, and highly inefficient
|
||||
// Find the first index that starts with { or [
|
||||
const bracketOpen = Math.max(consoleOutput.indexOf("{"), 0);
|
||||
const bracketOpenSquare = Math.max(consoleOutput.indexOf("["), 0);
|
||||
const start = Math.min(bracketOpen, bracketOpenSquare); // always non-negative due to Math.max above
|
||||
for (let i = start; i < consoleOutput.length; i++) {
|
||||
const slice = consoleOutput.slice(start, i + 1);
|
||||
try {
|
||||
return JSON.parse(slice);
|
||||
}
|
||||
catch {
|
||||
// ignore, try next
|
||||
}
|
||||
}
|
||||
throw new Error("No JSON content found in output");
|
||||
}
|
||||
cacheKey(pkg) {
|
||||
const rel = path.relative(this.rootDir, pkg.path);
|
||||
return `${pkg.name}::${pkg.version}::${rel !== null && rel !== void 0 ? rel : "."}`;
|
||||
}
|
||||
// We use the key (alias name) instead of value.name for npm aliased packages
|
||||
// e.g., { "foo": { name: "@scope/bar", ... } } should be stored as "foo@version"
|
||||
normalizePackageVersion(key, pkg) {
|
||||
return { id: `${key}@${pkg.version}`, pkgOverride: { ...pkg, name: key } };
|
||||
}
|
||||
/**
|
||||
* Determines if a given dependency is a production dependency of a package.
|
||||
*
|
||||
* Checks both the dependencies and optionalDependencies of a package to see if
|
||||
* the specified dependency name is listed.
|
||||
*
|
||||
* @param depName - The name of the dependency to check
|
||||
* @param pkg - The package to search for the dependency in
|
||||
* @returns True if the dependency is found in either dependencies or optionalDependencies, false otherwise
|
||||
*/
|
||||
isProdDependency(depName, pkg) {
|
||||
const prodDeps = { ...pkg.dependencies, ...pkg.optionalDependencies };
|
||||
return prodDeps[depName] != null;
|
||||
}
|
||||
async locatePackageWithVersion(depTree) {
|
||||
const result = await this.cache.locatePackageVersion({
|
||||
parentDir: depTree.path,
|
||||
pkgName: depTree.name,
|
||||
requiredRange: depTree.version,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Parses a dependency identifier string into name and version components.
|
||||
*
|
||||
* Handles both scoped packages (e.g., "@scope/pkg@1.2.3") and regular packages (e.g., "pkg@1.2.3").
|
||||
* If the identifier is malformed or cannot be parsed, defaults to treating the entire string as
|
||||
* the package name with an "unknown" version.
|
||||
*/
|
||||
parseNameVersion(identifier) {
|
||||
const lastAt = identifier.lastIndexOf("@");
|
||||
if (lastAt <= 0) {
|
||||
// fallback for scoped packages or malformed strings
|
||||
return { name: identifier, version: "unknown" };
|
||||
}
|
||||
const name = identifier.slice(0, lastAt);
|
||||
const version = identifier.slice(lastAt + 1);
|
||||
return { name, version };
|
||||
}
|
||||
/**
|
||||
* Retrieves the dependency tree and handles workspace package self-references.
|
||||
*
|
||||
* If the project is a workspace project, this method removes the root package's self-reference
|
||||
* from the dependency tree to avoid circular dependencies. It promotes the root package's
|
||||
* direct dependencies to the top level of the tree.
|
||||
*
|
||||
* @param tree - The original dependency tree
|
||||
* @param packageName - The name of the package to check for and remove from the tree
|
||||
* @returns The extracted dependency subtree
|
||||
*/
|
||||
getTreeFromWorkspaces(tree, packageName) {
|
||||
if (tree.workspaces && tree.dependencies) {
|
||||
for (const [key, value] of Object.entries(tree.dependencies)) {
|
||||
if (key === packageName) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
transformToHoisterTree(obj, key, nodes = new Map()) {
|
||||
let node = nodes.get(key);
|
||||
const { name, version } = this.parseNameVersion(key);
|
||||
if (!node) {
|
||||
node = {
|
||||
name,
|
||||
identName: name,
|
||||
reference: version,
|
||||
dependencies: new Set(),
|
||||
peerNames: new Set(),
|
||||
};
|
||||
nodes.set(key, node);
|
||||
const deps = (obj[key] || {}).dependencies || [];
|
||||
for (const dep of deps) {
|
||||
const child = this.transformToHoisterTree(obj, dep, nodes);
|
||||
node.dependencies.add(child);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
async _getNodeModules(dependencies, result) {
|
||||
var _a;
|
||||
if (dependencies.size === 0) {
|
||||
return;
|
||||
}
|
||||
for (const d of dependencies.values()) {
|
||||
const reference = [...d.references][0];
|
||||
const key = `${d.name}@${reference}`;
|
||||
const p = (_a = this.allDependencies.get(key)) === null || _a === void 0 ? void 0 : _a.path;
|
||||
if (p === undefined) {
|
||||
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_NOT_FOUND].push(key);
|
||||
continue;
|
||||
}
|
||||
// fix npm list issue
|
||||
// https://github.com/npm/cli/issues/8535
|
||||
if (!(await this.cache.exists[p])) {
|
||||
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_NOT_ON_DISK].push(key);
|
||||
continue;
|
||||
}
|
||||
const node = {
|
||||
name: d.name,
|
||||
version: reference,
|
||||
dir: await this.cache.realPath[p],
|
||||
};
|
||||
result.push(node);
|
||||
if (d.dependencies.size > 0) {
|
||||
node.dependencies = [];
|
||||
await this._getNodeModules(d.dependencies, node.dependencies);
|
||||
}
|
||||
}
|
||||
result.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
async asyncExec(command, args, cwd = this.rootDir) {
|
||||
const file = await this.tempDirManager.getTempFile({ prefix: "exec-", suffix: ".txt" });
|
||||
try {
|
||||
await this.streamCollectorCommandToFile(command, args, cwd, file);
|
||||
const result = await fs.readFile(file, { encoding: "utf8" });
|
||||
return { stdout: result === null || result === void 0 ? void 0 : result.trim(), stderr: undefined };
|
||||
}
|
||||
catch (error) {
|
||||
builder_util_1.log.debug({ error: error.message }, "failed to execute command");
|
||||
return { stdout: undefined, stderr: error.message };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Executes a command and streams its output to a file.
|
||||
*
|
||||
* Spawns a child process to execute the specified command with arguments, capturing stdout
|
||||
* to a file. Handles Windows-specific quirks by wrapping .cmd files in a temporary .bat file
|
||||
* when necessary. Enables corepack strict mode by default but allows process.env overrides.
|
||||
*
|
||||
* Special handling for `npm list` exit code 1, which is expected in certain scenarios.
|
||||
*
|
||||
* @param command - The command to execute
|
||||
* @param args - Array of command-line arguments
|
||||
* @param cwd - The working directory to execute the command in
|
||||
* @param tempOutputFile - The path to the temporary file where stdout will be written
|
||||
* @returns Promise that resolves when the command completes successfully or rejects if it fails
|
||||
* @throws {Error} If the child process spawn fails or exits with a non-zero code
|
||||
*/
|
||||
async streamCollectorCommandToFile(command, args, cwd, tempOutputFile) {
|
||||
const execName = path.basename(command, path.extname(command));
|
||||
const isWindowsScriptFile = process.platform === "win32" && path.extname(command).toLowerCase() === ".cmd";
|
||||
if (isWindowsScriptFile) {
|
||||
// If the command is a Windows script file (.cmd), we need to wrap it in a .bat file to ensure it runs correctly with cmd.exe
|
||||
// This is necessary because .cmd files are not directly executable in the same way as .bat files.
|
||||
// We create a temporary .bat file that calls the .cmd file with the provided arguments. The .bat file will be executed by cmd.exe.
|
||||
// Note: This is a workaround for Windows command execution quirks when using `shell: true`
|
||||
const tempBatFile = await this.tempDirManager.getTempFile({
|
||||
prefix: execName,
|
||||
suffix: ".bat",
|
||||
});
|
||||
const batScript = `@echo off\r\n"${command}" %*\r\n`; // <-- CRLF required for .bat
|
||||
await fs.writeFile(tempBatFile, batScript, { encoding: "utf8" });
|
||||
command = "cmd.exe";
|
||||
args = ["/c", `"${tempBatFile}"`, ...args];
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
const outStream = (0, fs_extra_1.createWriteStream)(tempOutputFile);
|
||||
const child = childProcess.spawn(command, args, {
|
||||
cwd,
|
||||
env: { COREPACK_ENABLE_STRICT: "0", ...process.env }, // allow `process.env` overrides
|
||||
shell: true, // `true`` is now required: https://github.com/electron-userland/electron-builder/issues/9488
|
||||
});
|
||||
let stderr = "";
|
||||
child.stdout.pipe(outStream);
|
||||
child.stderr.on("data", chunk => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("error", err => {
|
||||
reject(new Error(`Node module collector spawn failed: ${err.message}`));
|
||||
});
|
||||
child.on("close", code => {
|
||||
outStream.close();
|
||||
// https://github.com/npm/npm/issues/17624
|
||||
const shouldIgnore = code === 1 && "npm" === execName.toLowerCase() && args.includes("list");
|
||||
if (shouldIgnore) {
|
||||
builder_util_1.log.debug(null, "`npm list` returned non-zero exit code, but it MIGHT be expected (https://github.com/npm/npm/issues/17624). Check stderr for details.");
|
||||
}
|
||||
if (stderr.length > 0) {
|
||||
builder_util_1.log.debug({ stderr }, "note: there was node module collector output on stderr");
|
||||
this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_COLLECTOR_OUTPUT].push(stderr);
|
||||
}
|
||||
const shouldResolve = code === 0 || shouldIgnore;
|
||||
return shouldResolve ? resolve() : reject(new Error(`Node module collector process exited with code ${code}:\n${stderr}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.NodeModulesCollector = NodeModulesCollector;
|
||||
//# sourceMappingURL=nodeModulesCollector.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+14
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+80
@@ -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
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+17
@@ -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 {};
|
||||
+144
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { NodeModulesCollector } from "./nodeModulesCollector";
|
||||
import { PM } from "./packageManager";
|
||||
import { PnpmDependency } from "./types";
|
||||
export declare class PnpmNodeModulesCollector extends NodeModulesCollector<PnpmDependency, PnpmDependency> {
|
||||
readonly installOptions: {
|
||||
manager: PM;
|
||||
lockfile: string;
|
||||
};
|
||||
protected getArgs(): string[];
|
||||
protected extractProductionDependencyGraph(tree: PnpmDependency, dependencyId: string): Promise<void>;
|
||||
protected collectAllDependencies(tree: PnpmDependency): Promise<void>;
|
||||
protected parseDependenciesTree(jsonBlob: string): PnpmDependency;
|
||||
}
|
||||
Generated
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PnpmNodeModulesCollector = void 0;
|
||||
const builder_util_1 = require("builder-util");
|
||||
const nodeModulesCollector_1 = require("./nodeModulesCollector");
|
||||
const packageManager_1 = require("./packageManager");
|
||||
class PnpmNodeModulesCollector extends nodeModulesCollector_1.NodeModulesCollector {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.installOptions = {
|
||||
manager: packageManager_1.PM.PNPM,
|
||||
lockfile: "pnpm-lock.yaml",
|
||||
};
|
||||
}
|
||||
getArgs() {
|
||||
return ["list", "--prod", "--json", "--depth", "Infinity", "--silent", "--loglevel=error"];
|
||||
}
|
||||
async extractProductionDependencyGraph(tree, dependencyId) {
|
||||
if (this.productionGraph[dependencyId]) {
|
||||
return;
|
||||
}
|
||||
this.productionGraph[dependencyId] = { dependencies: [] };
|
||||
const packageName = tree.name || tree.from;
|
||||
const { packageJson } = (await this.cache.locatePackageVersion({ pkgName: packageName, parentDir: this.rootDir, requiredRange: tree.version })) || {};
|
||||
const all = packageJson ? { ...packageJson.dependencies, ...packageJson.optionalDependencies } : { ...tree.dependencies, ...tree.optionalDependencies };
|
||||
const optional = packageJson ? { ...packageJson.optionalDependencies } : {};
|
||||
const deps = { ...(tree.dependencies || {}), ...(tree.optionalDependencies || {}) };
|
||||
this.productionGraph[dependencyId] = { dependencies: [] };
|
||||
const depPromises = Object.entries(deps).map(async ([packageName, dependency]) => {
|
||||
// First check if it's in production dependencies
|
||||
if (!all[packageName]) {
|
||||
return undefined;
|
||||
}
|
||||
// Then check if optional dependency path exists (using actual resolved path)
|
||||
if (optional[packageName]) {
|
||||
const pkg = await this.cache.locatePackageVersion({ pkgName: packageName, parentDir: this.rootDir, requiredRange: dependency.version });
|
||||
if (!pkg) {
|
||||
builder_util_1.log.debug({ name: packageName, version: dependency.version, path: dependency.path }, `optional dependency doesn't exist, skipping - likely not installed`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
const { id: childDependencyId, pkgOverride } = this.normalizePackageVersion(packageName, dependency);
|
||||
await this.extractProductionDependencyGraph(pkgOverride, childDependencyId);
|
||||
return childDependencyId;
|
||||
});
|
||||
const collectedDependencies = [];
|
||||
for (const dep of depPromises) {
|
||||
const result = await dep;
|
||||
if (result !== undefined) {
|
||||
collectedDependencies.push(result);
|
||||
}
|
||||
}
|
||||
this.productionGraph[dependencyId] = { dependencies: collectedDependencies };
|
||||
}
|
||||
async collectAllDependencies(tree) {
|
||||
var _a, _b;
|
||||
// Collect regular dependencies
|
||||
for (const [key, value] of Object.entries(tree.dependencies || {})) {
|
||||
const pkg = await this.cache.locatePackageVersion({ pkgName: key, parentDir: this.rootDir, requiredRange: value.version });
|
||||
this.allDependencies.set(`${key}@${value.version}`, { ...value, path: (_a = pkg === null || pkg === void 0 ? void 0 : pkg.packageDir) !== null && _a !== void 0 ? _a : value.path });
|
||||
await this.collectAllDependencies(value);
|
||||
}
|
||||
// Collect optional dependencies if they exist
|
||||
for (const [key, value] of Object.entries(tree.optionalDependencies || {})) {
|
||||
const pkg = await this.cache.locatePackageVersion({ pkgName: key, parentDir: this.rootDir, requiredRange: value.version });
|
||||
this.allDependencies.set(`${key}@${value.version}`, { ...value, path: (_b = pkg === null || pkg === void 0 ? void 0 : pkg.packageDir) !== null && _b !== void 0 ? _b : value.path });
|
||||
await this.collectAllDependencies(value);
|
||||
}
|
||||
}
|
||||
parseDependenciesTree(jsonBlob) {
|
||||
// pnpm returns an array of dependency trees
|
||||
const dependencyTree = this.extractJsonFromPollutedOutput(jsonBlob);
|
||||
return dependencyTree[0];
|
||||
}
|
||||
}
|
||||
exports.PnpmNodeModulesCollector = PnpmNodeModulesCollector;
|
||||
//# sourceMappingURL=pnpmNodeModulesCollector.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+18
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+122
@@ -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
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
+51
@@ -0,0 +1,51 @@
|
||||
export type PackageJson = {
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
peerDependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
workspaces?: string[] | {
|
||||
packages: string[];
|
||||
};
|
||||
};
|
||||
export interface NodeModuleInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
dir: string;
|
||||
dependencies?: Array<NodeModuleInfo>;
|
||||
}
|
||||
export type ParsedDependencyTree = {
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
readonly path: string;
|
||||
readonly workspaces?: string[] | {
|
||||
packages: string[];
|
||||
};
|
||||
};
|
||||
export interface PnpmDependency extends Dependency<PnpmDependency, PnpmDependency> {
|
||||
readonly from: string;
|
||||
readonly resolved: string;
|
||||
}
|
||||
export interface NpmDependency extends Dependency<NpmDependency, string> {
|
||||
readonly resolved?: string;
|
||||
readonly _dependencies?: {
|
||||
[packageName: string]: string;
|
||||
};
|
||||
}
|
||||
export interface TraversedDependency extends Dependency<TraversedDependency, TraversedDependency> {
|
||||
}
|
||||
export type Dependency<T, V> = Dependencies<T, V> & ParsedDependencyTree;
|
||||
export type Dependencies<T, V> = {
|
||||
readonly dependencies?: {
|
||||
[packageName: string]: T;
|
||||
};
|
||||
readonly optionalDependencies?: {
|
||||
[packageName: string]: V;
|
||||
};
|
||||
};
|
||||
export interface DependencyGraph {
|
||||
[packageNameAndVersion: string]: {
|
||||
readonly dependencies: string[];
|
||||
};
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=types.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/node-module-collector/types.ts"],"names":[],"mappings":"","sourcesContent":["export type PackageJson = {\n name: string\n version: string\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n peerDependencies?: Record<string, string>\n optionalDependencies?: Record<string, string>\n workspaces?: string[] | { packages: string[] }\n}\n\nexport interface NodeModuleInfo {\n name: string\n version: string\n dir: string\n dependencies?: Array<NodeModuleInfo>\n}\n\nexport type ParsedDependencyTree = {\n readonly name: string\n readonly version: string\n readonly path: string\n readonly workspaces?: string[] | { packages: string[] } // we only use this at root level\n}\n\n// Note: `PnpmDependency` and `NpmDependency` include the output of `JSON.parse(...)` of `pnpm list` and `npm list` respectively\n// This object has a TON of info - a majority, if not all, of each dependency's package.json\n// We extract only what we need when constructing DependencyTree in `extractProductionDependencyTree`\nexport interface PnpmDependency extends Dependency<PnpmDependency, PnpmDependency> {\n readonly from: string\n readonly resolved: string\n}\n\nexport interface NpmDependency extends Dependency<NpmDependency, string> {\n readonly resolved?: string\n // implicit dependencies, returned only through `npm list`\n readonly _dependencies?: {\n [packageName: string]: string\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface TraversedDependency extends Dependency<TraversedDependency, TraversedDependency> {}\n\nexport type Dependency<T, V> = Dependencies<T, V> & ParsedDependencyTree\n\nexport type Dependencies<T, V> = {\n readonly dependencies?: {\n [packageName: string]: T\n }\n readonly optionalDependencies?: {\n [packageName: string]: V\n }\n}\n\nexport interface DependencyGraph {\n [packageNameAndVersion: string]: {\n readonly dependencies: string[]\n }\n}\n"]}
|
||||
Generated
Vendored
+15
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+93
@@ -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 2–3: 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 1–3: 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 2–3: 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
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+10
@@ -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>;
|
||||
}
|
||||
Generated
Vendored
+22
@@ -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
|
||||
Generated
Vendored
+1
@@ -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"]}
|
||||
Reference in New Issue
Block a user