app-builder-lib
Version:
electron-builder lib
122 lines • 7.16 kB
JavaScript
;
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