From c0bf1a34ddc2e5cc50bbde41fb7bf4b620d820e3 Mon Sep 17 00:00:00 2001 From: "bongseok13.choi" Date: Mon, 20 Jul 2026 11:26:02 +0900 Subject: [PATCH 1/5] NXT-17571: Support npm lockfileVersion 2 and 3 in bootstrap --override Co-Authored-By: Claude --- CHANGELOG.md | 6 +++++ commands/bootstrap.js | 60 ++++++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f485395..8c45d269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## unreleased + +### bootstrap + +* Fixed `--override` to support `package-lock.json` with `lockfileVersion` 2 and 3. + ## 7.3.3 (July 7, 2026) ### transpile diff --git a/commands/bootstrap.js b/commands/bootstrap.js index f7f78725..81a986be 100644 --- a/commands/bootstrap.js +++ b/commands/bootstrap.js @@ -84,31 +84,61 @@ function api ({ ['package.json', 'package-lock.json', 'npm-shrinkwrap.json'] .map(f => path.join(cwd, f)) .filter(f => fs.existsSync(f)) - .forEach((f, i) => { - const lockfile = i > 0; + .forEach(f => { + const lockfile = path.basename(f) !== 'package.json'; // Restore any detected backups if (fs.existsSync(f + '.bak')) { fs.unlinkSync(f); fs.renameSync(f + '.bak', f); } const obj = JSON.parse(fs.readFileSync(f, {encoding: 'utf8'})); + if (lockfile) { + obj.lockfileVersion = obj.lockfileVersion || 1; + obj.requires = true; + } // Update dependency entry for local entries that exist - local - .filter(dep => obj.dependencies && obj.dependencies[dep]) - .forEach(dep => { - const fileDep = 'file:' + path.join(override, dep, 'package.tgz'); - if (lockfile) { - obj.lockfileVersion = obj.lockfileVersion || 1; - obj.requires = true; - obj.dependencies[dep].version = fileDep; + local.forEach(dep => { + const fileDep = 'file:' + path.join(override, dep, 'package.tgz'); + if (!lockfile) { + if (obj.dependencies && obj.dependencies[dep]) { + obj.dependencies[dep] = fileDep; + } + return; + } + // lockfileVersion 1 (and the legacy tree kept in v2): + // the flat "dependencies" map keyed by package name + if (obj.dependencies && obj.dependencies[dep]) { + obj.dependencies[dep].version = fileDep; + // Remove unneeded properties to avoid issues + ['resolved', 'from', 'integrity', 'requires'].forEach( + key => delete obj.dependencies[dep][key] + ); + } + // lockfileVersion 2 & 3: the "packages" map keyed by + // install path (e.g. "node_modules/@enact/core") + if (obj.packages) { + const nodeModulesKey = 'node_modules/' + dep; + if (obj.packages[nodeModulesKey]) { + obj.packages[nodeModulesKey].resolved = fileDep; // Remove unneeded properties to avoid issues - ['resolved', 'from', 'integrity', 'requires'].forEach( - key => delete obj.dependencies[dep][key] + ['from', 'integrity'].forEach( + key => delete obj.packages[nodeModulesKey][key] + ); + } + // The root package ("") mirrors package.json's + // dependency specifiers + const root = obj.packages['']; + if (root) { + ['dependencies', 'devDependencies', 'optionalDependencies'].forEach( + depType => { + if (root[depType] && root[depType][dep]) { + root[depType][dep] = fileDep; + } + } ); - } else { - obj.dependencies[dep] = fileDep; } - }); + } + }); // Backup existing and write the newly modified file fs.renameSync(f, f + '.bak'); fs.writeFileSync(f, JSON.stringify(obj, null, ' '), {encoding: 'utf8'}); From 7727513863c31d4b31f36a54e395fd3f383e920d Mon Sep 17 00:00:00 2001 From: "bongseok13.choi" Date: Mon, 20 Jul 2026 14:10:28 +0900 Subject: [PATCH 2/5] NXT-17571: Align packages map override with lockfileVersion 1 handling Set `version` and `resolved` to the local file dependency and remove `from`, `integrity`, and `requires`, matching the legacy `dependencies` tree logic. --- commands/bootstrap.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/commands/bootstrap.js b/commands/bootstrap.js index 81a986be..ad17867e 100644 --- a/commands/bootstrap.js +++ b/commands/bootstrap.js @@ -119,9 +119,10 @@ function api ({ if (obj.packages) { const nodeModulesKey = 'node_modules/' + dep; if (obj.packages[nodeModulesKey]) { + obj.packages[nodeModulesKey].version = fileDep; obj.packages[nodeModulesKey].resolved = fileDep; // Remove unneeded properties to avoid issues - ['from', 'integrity'].forEach( + ['from', 'integrity', 'requires'].forEach( key => delete obj.packages[nodeModulesKey][key] ); } From 7173c529352343ea8c3cb99d7b626ef663c137ce Mon Sep 17 00:00:00 2001 From: "bongseok13.choi" Date: Mon, 20 Jul 2026 17:27:39 +0900 Subject: [PATCH 3/5] NXT-17571: Handle nested node_modules paths in packages map override A dependency can be installed at the top level or nested under another package (e.g. node_modules/@enact/limestone/node_modules/@enact/core), so rewrite every packages entry whose path resolves to the overridden package instead of only the top-level one. Also drop `resolved` alongside the other stale fields to mirror the lockfileVersion 1 handling. --- commands/bootstrap.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/commands/bootstrap.js b/commands/bootstrap.js index ad17867e..9a9c6968 100644 --- a/commands/bootstrap.js +++ b/commands/bootstrap.js @@ -117,15 +117,20 @@ function api ({ // lockfileVersion 2 & 3: the "packages" map keyed by // install path (e.g. "node_modules/@enact/core") if (obj.packages) { - const nodeModulesKey = 'node_modules/' + dep; - if (obj.packages[nodeModulesKey]) { - obj.packages[nodeModulesKey].version = fileDep; - obj.packages[nodeModulesKey].resolved = fileDep; - // Remove unneeded properties to avoid issues - ['from', 'integrity', 'requires'].forEach( - key => delete obj.packages[nodeModulesKey][key] - ); - } + // A dependency can be installed at the top level + // ("node_modules/@enact/core") or nested + // ("node_modules/@enact/limestone/node_modules/@enact/core"), + // so update every path that resolves to this package. + const suffix = 'node_modules/' + dep; + Object.keys(obj.packages) + .filter(key => key === suffix || key.endsWith('/' + suffix)) + .forEach(key => { + obj.packages[key].version = fileDep; + // Remove unneeded properties to avoid issues + ['resolved', 'from', 'integrity', 'requires'].forEach( + k => delete obj.packages[key][k] + ); + }); // The root package ("") mirrors package.json's // dependency specifiers const root = obj.packages['']; From 122d42150dcb1158fd9a2fc7ebb01b66aa033819 Mon Sep 17 00:00:00 2001 From: "bongseok13.choi" Date: Tue, 21 Jul 2026 08:38:57 +0900 Subject: [PATCH 4/5] NXT-17571: Set resolved to local tgz and normalize path separators For the packages map (lockfileVersion 2 & 3), set `resolved` to the local file dependency and drop `version` (npm derives it from the tarball), matching how npm records local file installs. Also normalize the file dependency path to POSIX separators so generated lockfiles are consistent on Windows. --- commands/bootstrap.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/bootstrap.js b/commands/bootstrap.js index 9a9c6968..e7c7b4ee 100644 --- a/commands/bootstrap.js +++ b/commands/bootstrap.js @@ -98,7 +98,7 @@ function api ({ } // Update dependency entry for local entries that exist local.forEach(dep => { - const fileDep = 'file:' + path.join(override, dep, 'package.tgz'); + const fileDep = 'file:' + path.join(override, dep, 'package.tgz').split(path.sep).join('/'); if (!lockfile) { if (obj.dependencies && obj.dependencies[dep]) { obj.dependencies[dep] = fileDep; @@ -125,9 +125,9 @@ function api ({ Object.keys(obj.packages) .filter(key => key === suffix || key.endsWith('/' + suffix)) .forEach(key => { - obj.packages[key].version = fileDep; + obj.packages[key].resolved = fileDep; // Remove unneeded properties to avoid issues - ['resolved', 'from', 'integrity', 'requires'].forEach( + ['version', 'from', 'integrity', 'requires'].forEach( k => delete obj.packages[key][k] ); }); From 709222c5d0818f471f930c2dff9823a7b7f68138 Mon Sep 17 00:00:00 2001 From: "bongseok13.choi" Date: Tue, 21 Jul 2026 10:38:40 +0900 Subject: [PATCH 5/5] NXT-17571: Drop nested node_modules entries so deps resolve to override Instead of rewriting nested packages entries (node_modules/@enact/limestone/node_modules/@enact/core), remove them so the dependent package resolves to the overridden top-level package (node_modules/@enact/core). Only the top-level entry is repointed at the local tgz via `resolved`, keeping its `version`. --- commands/bootstrap.js | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/commands/bootstrap.js b/commands/bootstrap.js index e7c7b4ee..ea95acfb 100644 --- a/commands/bootstrap.js +++ b/commands/bootstrap.js @@ -117,20 +117,23 @@ function api ({ // lockfileVersion 2 & 3: the "packages" map keyed by // install path (e.g. "node_modules/@enact/core") if (obj.packages) { - // A dependency can be installed at the top level - // ("node_modules/@enact/core") or nested - // ("node_modules/@enact/limestone/node_modules/@enact/core"), - // so update every path that resolves to this package. - const suffix = 'node_modules/' + dep; + // The top-level install ("node_modules/@enact/core") + // gets pointed at the local tgz. + const topKey = 'node_modules/' + dep; + if (obj.packages[topKey]) { + obj.packages[topKey].resolved = fileDep; + // Remove unneeded properties to avoid issues + ['from', 'integrity', 'requires'].forEach( + k => delete obj.packages[topKey][k] + ); + } + // Nested installs + // ("node_modules/@enact/limestone/node_modules/@enact/core") + // are removed so the dependent resolves to the + // overridden top-level package instead. Object.keys(obj.packages) - .filter(key => key === suffix || key.endsWith('/' + suffix)) - .forEach(key => { - obj.packages[key].resolved = fileDep; - // Remove unneeded properties to avoid issues - ['version', 'from', 'integrity', 'requires'].forEach( - k => delete obj.packages[key][k] - ); - }); + .filter(key => key.endsWith('/' + topKey)) + .forEach(key => delete obj.packages[key]); // The root package ("") mirrors package.json's // dependency specifiers const root = obj.packages[''];