Summary
When a .pkgrc (or any auto-discovered config file) is present, buildMarker in lib/index.ts replaces marker.config with the parsed config file instead of merging the pkg field over the entry's package.json. The walker then loses access to the entry's dependencies, main, name, etc., and the ahead-of-time entry-package dependencies traversal in walker.ts is skipped.
Expected
.pkgrc should be a drop-in replacement for the pkg field in package.json — moving config into .pkgrc should not change which files end up in the executable.
Actual
Moving the pkg block from package.json into .pkgrc silently disables the entry-package dependencies derivative pump. Dependencies that aren't statically reachable via require() from the entry script can be missed.
Root cause
lib/index.ts (referencing lib-es5/index.js:49-58 in the published 6.19.0 build):
function buildMarker(configJson, config, inputJson, input) {
const marker = configJson
? { config: configJson, base: path.dirname(config), configPath: config }
: { config: inputJson || {}, base: path.dirname(input), configPath: input };
marker.toplevel = true;
return marker;
}
When configJson is truthy (i.e. a .pkgrc was found), marker.config becomes the parsed config file. For a bare .pkgrc (auto-wrapped at lib/config.ts to { pkg: {...} }), this means marker.config.dependencies, marker.config.main, marker.config.name, etc. are all undefined for the entry package.
Then in lib/walker.ts stepActivate (referencing lib-es5/walker.js:508-526):
const { dependencies } = config;
if (typeof dependencies === 'object') {
for (const dependency in dependencies) {
if (dependencies[dependency]) {
derivatives.push({ alias: dependency, aliasType: ALIAS_AS_RESOLVABLE, fromDependencies: true });
derivatives.push({ alias: `${dependency}/package.json`, aliasType: ALIAS_AS_RESOLVABLE, fromDependencies: true });
}
}
}
This loop is the only place where declared dependencies are pushed as derivatives without a static require() site. With .pkgrc, it's skipped for the toplevel marker.
Same loss applies to config.main (referenced at lib-es5/walker.js:809-810) and config.name (used for dictionary lookup at lib-es5/walker.js:495-506).
Repro
mkdir pkgrc-bug && cd pkgrc-bug
npm init -y
npm i lodash
cat > index.js <<'INNER'
const dep = process.env.DEP || 'lodash';
const m = require(dep);
console.log(typeof m);
INNER
Case A — pkg field in package.json:
{
"name": "pkgrc-bug",
"bin": "index.js",
"dependencies": { "lodash": "*" },
"pkg": { "scripts": ["index.js"] }
}
pkg . -t host -o out-a → produced binary runs (lodash was bundled via the dependencies derivative pump).
Case B — same fields, but the pkg block moved to .pkgrc:
// package.json (no "pkg" field)
{ "name": "pkgrc-bug", "bin": "index.js", "dependencies": { "lodash": "*" } }
// .pkgrc
{ "scripts": ["index.js"] }
pkg . -t host -o out-b → lodash is not bundled (the require(dep) is dynamic, so the walker's static analysis can't find it; the only other path that would have included it — the dependencies traversal — is skipped because marker.config is the .pkgrc, not package.json).
Suggested fix
In buildMarker, keep marker.config rooted in inputJson and only override the pkg field from configJson:
function buildMarker(configJson, config, inputJson, input) {
const baseConfig = inputJson ?? {};
const marker = configJson
? {
config: { ...baseConfig, pkg: configJson.pkg ?? baseConfig.pkg },
base: path.dirname(config),
configPath: config,
}
: { config: baseConfig, base: path.dirname(input), configPath: input };
marker.toplevel = true;
return marker;
}
This preserves dependencies, main, name, etc. from the entry's package.json while letting .pkgrc contribute (and override) only the pkg-specific fields — which matches the documented intent of .pkgrc as a drop-in replacement for the pkg field.
Environment
@yao-pkg/pkg@6.19.0
- Node.js 22.22.2
- Linux x64
Summary
When a
.pkgrc(or any auto-discovered config file) is present,buildMarkerinlib/index.tsreplacesmarker.configwith the parsed config file instead of merging thepkgfield over the entry'spackage.json. The walker then loses access to the entry'sdependencies,main,name, etc., and the ahead-of-time entry-packagedependenciestraversal inwalker.tsis skipped.Expected
.pkgrcshould be a drop-in replacement for thepkgfield inpackage.json— moving config into.pkgrcshould not change which files end up in the executable.Actual
Moving the
pkgblock frompackage.jsoninto.pkgrcsilently disables the entry-packagedependenciesderivative pump. Dependencies that aren't statically reachable viarequire()from the entry script can be missed.Root cause
lib/index.ts(referencinglib-es5/index.js:49-58in the published 6.19.0 build):When
configJsonis truthy (i.e. a.pkgrcwas found),marker.configbecomes the parsed config file. For a bare.pkgrc(auto-wrapped atlib/config.tsto{ pkg: {...} }), this meansmarker.config.dependencies,marker.config.main,marker.config.name, etc. are allundefinedfor the entry package.Then in
lib/walker.tsstepActivate(referencinglib-es5/walker.js:508-526):This loop is the only place where declared
dependenciesare pushed as derivatives without a staticrequire()site. With.pkgrc, it's skipped for the toplevel marker.Same loss applies to
config.main(referenced atlib-es5/walker.js:809-810) andconfig.name(used for dictionary lookup atlib-es5/walker.js:495-506).Repro
Case A —
pkgfield inpackage.json:{ "name": "pkgrc-bug", "bin": "index.js", "dependencies": { "lodash": "*" }, "pkg": { "scripts": ["index.js"] } }pkg . -t host -o out-a→ produced binary runs (lodashwas bundled via thedependenciesderivative pump).Case B — same fields, but the
pkgblock moved to.pkgrc:pkg . -t host -o out-b→lodashis not bundled (therequire(dep)is dynamic, so the walker's static analysis can't find it; the only other path that would have included it — thedependenciestraversal — is skipped becausemarker.configis the.pkgrc, notpackage.json).Suggested fix
In
buildMarker, keepmarker.configrooted ininputJsonand only override thepkgfield fromconfigJson:This preserves
dependencies,main,name, etc. from the entry'spackage.jsonwhile letting.pkgrccontribute (and override) only the pkg-specific fields — which matches the documented intent of.pkgrcas a drop-in replacement for thepkgfield.Environment
@yao-pkg/pkg@6.19.0