Skip to content

bug: .pkgrc usage drops entry's package.json from marker, skipping dependencies traversal #274

@robertsLando

Description

@robertsLando

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions