Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion workspaces/arborist/lib/arborist/build-ideal-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -1292,7 +1292,31 @@ This is a one-time fix-up, please be patient...
// spec isn't a directory, and either isn't a workspace or the workspace we have
// doesn't satisfy the edge. try to fetch a manifest and build a node from that.
return this.#fetchManifest(spec, parent)
.then(pkg => new Node({ name, pkg, parent, installLinks, legacyPeerDeps }), error => {
.then(pkg => {
// When a proxy/upstream registry returns an incomplete manifest
// (e.g. missing version field for platform-specific packages it
// hasn't cached), treat it as a load failure so that optional deps
// are properly pruned instead of written to the lockfile without
// version metadata. Only apply to registry specs — file: deps
// legitimately omit version.
if (!pkg.version && spec.registry) {
const error = Object.assign(
new Error(`incomplete manifest for ${name}, missing version`),
{ code: 'EINCOMPLETEMANIFEST' }
)
error.requiredBy = edge.from.location || '.'
const n = new Node({
name,
parent,
error,
installLinks,
legacyPeerDeps,
})
this.#loadFailures.add(n)
return n
}
return new Node({ name, pkg, parent, installLinks, legacyPeerDeps })
}, error => {
error.requiredBy = edge.from.location || '.'

// failed to load the spec, either because of enotarget or
Expand Down
12 changes: 11 additions & 1 deletion workspaces/arborist/lib/shrinkwrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -909,10 +909,20 @@ class Shrinkwrap {
if (node.extraneous && !/(^|\/)node_modules\//.test(loc) && loc !== 'node_modules') {
continue
}
this.data.packages[loc] = Shrinkwrap.metaFromNode(
const meta = Shrinkwrap.metaFromNode(
node,
this.path,
this.resolveOptions)
// Skip optional deps that have no version — these are typically
// platform-specific packages where the registry returned an
// incomplete manifest (e.g. proxy registries that haven't cached
// the package for non-current platforms).
// Writing them without version produces invalid lockfile entries
// like {"optional": true} that cause "Invalid Version:" errors.
if (meta.optional && !meta.version && !node.isTop) {
continue
}
this.data.packages[loc] = meta
}
} else if (this.#awaitingUpdate.size > 0) {
for (const loc of this.#awaitingUpdate.keys()) {
Expand Down
93 changes: 93 additions & 0 deletions workspaces/arborist/test/shrinkwrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,99 @@ t.test('load a hidden lockfile', async t => {
t.equal(data.dependencies, undefined, 'deleted legacy metadata')
})

t.test('skip optional deps without version in commit', async t => {
// Simulate a scenario where a proxy registry returns incomplete manifests
// for platform-specific optional deps (e.g. @esbuild/aix-ppc64 on a
// macOS machine fetching from Azure Artifacts upstream proxy).
// These nodes end up with no version in their package metadata.
const path = t.testdir({
'package.json': JSON.stringify({
name: 'proxy-registry-repro',
version: '1.0.0',
devDependencies: { esbuild: '^0.27.0' },
}),
})
const meta = new Shrinkwrap({ path })
meta.data = {
lockfileVersion: 3,
packages: {},
}

const root = new Node({
pkg: {
name: 'proxy-registry-repro',
version: '1.0.0',
devDependencies: { esbuild: '^0.27.0' },
},
path,
realpath: path,
})

// esbuild with full metadata (valid)
const esbuild = new Node({
pkg: {
name: 'esbuild',
version: '0.27.7',
optionalDependencies: {
'@esbuild/linux-x64': '0.27.7',
'@esbuild/aix-ppc64': '0.27.7',
},
},
name: 'esbuild',
parent: root,
})
esbuild.dev = true

// platform dep with full metadata (current platform — valid)
const validDep = new Node({
pkg: {
name: '@esbuild/linux-x64',
version: '0.27.7',
os: ['linux'],
cpu: ['x64'],
},
name: '@esbuild/linux-x64',
parent: root,
})
validDep.optional = true
validDep.dev = true

// platform dep WITHOUT version (proxy registry returned incomplete manifest)
const brokenDep = new Node({
pkg: {
name: '@esbuild/aix-ppc64',
// no version! This is the bug scenario
},
name: '@esbuild/aix-ppc64',
parent: esbuild,
})
brokenDep.optional = true
brokenDep.dev = true
brokenDep.extraneous = false

meta.tree = root
const committed = meta.commit()

// The valid platform dep should be in the lockfile
const validLoc = 'node_modules/@esbuild/linux-x64'
t.ok(
committed.packages[validLoc],
'valid optional dep is included'
)
t.equal(
committed.packages[validLoc].version,
'0.27.7',
'valid optional dep has version'
)

// The incomplete platform dep should NOT be in the lockfile
const brokenLoc = 'node_modules/esbuild/node_modules/@esbuild/aix-ppc64'
t.notOk(
committed.packages[brokenLoc],
'optional dep without version is excluded from lockfile'
)
})

t.test('load a fresh hidden lockfile', async t => {
const sw = await Shrinkwrap.reset({
path: hiddenLockfileFixture,
Expand Down