diff --git a/internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js b/internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js index 4e9f835f5ef..a29ebdb5562 100644 --- a/internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js +++ b/internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js @@ -48,7 +48,8 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t path: workspaceRootDir, }); const tree = await arb.loadVirtual(); - const cliNode = Array.from(tree.tops).find((node) => node.packageName === targetPackageName); + const tops = Array.from(tree.tops.values()); + const cliNode = tops.find((node) => node.packageName === targetPackageName); if (!cliNode) { throw new Error(`Target package "${targetPackageName}" not found in workspace`); } @@ -59,7 +60,12 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t // Using the keys, extract relevant package-entries from package-lock.json const extractedPackages = Object.create(null); + const resolvedConflicts = new Set(); for (let [packageLoc, node] of relevantPackageLocations) { + if (resolvedConflicts.has(packageLoc)) { + // This package location was already moved due to a conflict + continue; + } let pkg = packageLockJson.packages[packageLoc]; if (pkg.link) { pkg = packageLockJson.packages[pkg.resolved]; @@ -70,13 +76,52 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t if (extractedPackages[packageLoc]) { throw new Error(`Duplicate root package entry for "${targetPackageName}"`); } - } else if (!pkg.resolved) { - // For all but the root package, ensure that "resolved" and "integrity" fields are present - // These are always missing for locally linked packages, but sometimes also for others (e.g. if installed - // from local cache) - const {resolved, integrity} = await fetchPackageMetadata(node.packageName, node.version, workspaceRootDir); - pkg.resolved = resolved; - pkg.integrity = integrity; + } else { + if (!pkg.resolved) { + // For all but the root package, ensure that "resolved" and "integrity" fields are present + // These are always missing for locally linked packages, but sometimes also for others + // (e.g. if installed from local cache) + const {resolved, integrity} = await fetchPackageMetadata( + node.packageName, node.version, workspaceRootDir); + pkg.resolved = resolved; + pkg.integrity = integrity; + } + // Align package locations with new target + const newPackageLoc = normalizePackageLocation(packageLoc, node, targetPackageName, tree.packageName); + // Detect conflicts with dependencies hoisted to root level for packages other than the target + const existingPackageAtNewLocation = relevantPackageLocations.get(newPackageLoc); + if (newPackageLoc !== packageLoc && existingPackageAtNewLocation) { + if (pkg.version !== existingPackageAtNewLocation.version) { + console.log( + `Hoisting conflict: Package "${node.packageName}" (from "${packageLoc}") already exists at ` + + `new location ${newPackageLoc} in version ${existingPackageAtNewLocation.version}`); + resolvedConflicts.add(newPackageLoc); + const conflictPkg = packageLockJson.packages[newPackageLoc]; + if (!conflictPkg.resolved) { + // For all but the root package, ensure that "resolved" and "integrity" fields are present + // These are always missing for locally linked packages, but sometimes also for others + // (e.g. if installed from local cache) + const {resolved, integrity} = await fetchPackageMetadata( + existingPackageAtNewLocation.packageName, existingPackageAtNewLocation.version, + workspaceRootDir); + conflictPkg.resolved = resolved; + conflictPkg.integrity = integrity; + } + // Move existing package to a package-specific subdirectories to avoid conflict + for (const edge of existingPackageAtNewLocation.edgesIn) { + const parentPackage = edge.from.top.packageName; + if (parentPackage === tree.packageName) { + // Skip dependencies of the workspace package + continue; + } + console.log(`Moving conflicting package "${node.packageName}" under ` + + `"node_modules/${parentPackage}/node_modules/"`); + const subPath = `node_modules/${parentPackage}/node_modules/${node.packageName}`; + extractedPackages[subPath] = structuredClone(conflictPkg); + } + } + } + packageLoc = newPackageLoc; } extractedPackages[packageLoc] = pkg; } @@ -100,6 +145,31 @@ export default async function convertPackageLockToShrinkwrap(workspaceRootDir, t return shrinkwrap; } +/** + * Normalize package locations from workspace-specific paths to standard npm paths. + * Examples (assuming @ui5/cli is the targetPackageName): + * - packages/cli/node_modules/foo -> node_modules/foo + * - packages/fs/node_modules/bar -> node_modules/@ui5/fs/node_modules/bar + * + * @param {string} location - Package location from arborist + * @param {object} node - Package node from arborist + * @param {string} targetPackageName - Target package name for shrinkwrap file + * @param {string} rootPackageName - Root / workspace package name + * @returns {string} - Normalized location for npm-shrinkwrap.json + */ +function normalizePackageLocation(location, node, targetPackageName, rootPackageName) { + const topPackageName = node.top.packageName; + if (topPackageName === targetPackageName) { + // Remove location for packages within target package (e.g. @ui5/cli) + return location.substring(node.top.location.length + 1); + } else if (topPackageName !== rootPackageName) { + // Add package within node_modules of actual package name (e.g. @ui5/fs) + return `node_modules/${topPackageName}/${location.substring(node.top.location.length + 1)}`; + } + // If it's already within the root workspace package, keep as-is + return location; +} + function collectDependencies(node, relevantPackageLocations) { if (relevantPackageLocations.has(node.location)) { // Already processed diff --git a/internal/shrinkwrap-extractor/test/expected/package.b/npm-shrinkwrap.json b/internal/shrinkwrap-extractor/test/expected/package.b/npm-shrinkwrap.json index cf2dabfe879..3e91f55dab1 100644 --- a/internal/shrinkwrap-extractor/test/expected/package.b/npm-shrinkwrap.json +++ b/internal/shrinkwrap-extractor/test/expected/package.b/npm-shrinkwrap.json @@ -1076,6 +1076,59 @@ "resolved": "https://registry.npmjs.org/package/version.tgz", "integrity": "sha512-mock-integrity-hash" }, + "node_modules/@ui5/fs/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "resolved": "https://registry.npmjs.org/package/version.tgz", + "integrity": "sha512-mock-integrity-hash" + }, + "node_modules/@ui5/fs/node_modules/globby": { + "version": "15.0.0", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "resolved": "https://registry.npmjs.org/package/version.tgz", + "integrity": "sha512-mock-integrity-hash" + }, + "node_modules/@ui5/fs/node_modules/ignore": { + "version": "7.0.5", + "license": "MIT", + "engines": { + "node": ">= 4" + }, + "resolved": "https://registry.npmjs.org/package/version.tgz", + "integrity": "sha512-mock-integrity-hash" + }, + "node_modules/@ui5/fs/node_modules/path-type": { + "version": "6.0.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "resolved": "https://registry.npmjs.org/package/version.tgz", + "integrity": "sha512-mock-integrity-hash" + }, "node_modules/@ui5/logger": { "name": "@ui5/logger", "version": "4.0.2", @@ -1293,28 +1346,28 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.2", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - }, - "resolved": "https://registry.npmjs.org/package/version.tgz", - "integrity": "sha512-mock-integrity-hash" + "node": ">=8" + } }, "node_modules/ansi-styles": { - "version": "6.2.3", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" - }, - "resolved": "https://registry.npmjs.org/package/version.tgz", - "integrity": "sha512-mock-integrity-hash" + } }, "node_modules/argparse": { "version": "2.0.1", @@ -1670,6 +1723,32 @@ "resolved": "https://registry.npmjs.org/package/version.tgz", "integrity": "sha512-mock-integrity-hash" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone": { "version": "2.1.2", "license": "MIT", @@ -1946,6 +2025,18 @@ "resolved": "https://registry.npmjs.org/package/version.tgz", "integrity": "sha512-mock-integrity-hash" }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/depd": { "version": "2.0.0", "license": "MIT", @@ -4043,6 +4134,24 @@ "resolved": "https://registry.npmjs.org/package/version.tgz", "integrity": "sha512-mock-integrity-hash" }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "7.0.3", "license": "MIT", @@ -5935,21 +6044,21 @@ "integrity": "sha512-mock-integrity-hash" }, "node_modules/wrap-ansi": { - "version": "8.1.0", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - }, - "resolved": "https://registry.npmjs.org/package/version.tgz", - "integrity": "sha512-mock-integrity-hash" + } }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", @@ -6028,6 +6137,18 @@ "resolved": "https://registry.npmjs.org/package/version.tgz", "integrity": "sha512-mock-integrity-hash" }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wsl-utils": { "version": "0.1.0", "license": "MIT", @@ -6122,122 +6243,7 @@ "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", "license": "Apache-2.0" }, - "node_modules/yesno": { - "version": "0.4.0", - "license": "BSD", - "resolved": "https://registry.npmjs.org/package/version.tgz", - "integrity": "sha512-mock-integrity-hash" - }, - "packages/cli/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "packages/cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "packages/cli/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "packages/cli/node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "packages/cli/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/cli/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "packages/cli/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "packages/cli/node_modules/yargs": { + "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", @@ -6255,7 +6261,7 @@ "node": ">=12" } }, - "packages/cli/node_modules/yargs-parser": { + "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", @@ -6264,56 +6270,9 @@ "node": ">=12" } }, - "packages/fs/node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "resolved": "https://registry.npmjs.org/package/version.tgz", - "integrity": "sha512-mock-integrity-hash" - }, - "packages/fs/node_modules/globby": { - "version": "15.0.0", - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.5", - "path-type": "^6.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "resolved": "https://registry.npmjs.org/package/version.tgz", - "integrity": "sha512-mock-integrity-hash" - }, - "packages/fs/node_modules/ignore": { - "version": "7.0.5", - "license": "MIT", - "engines": { - "node": ">= 4" - }, - "resolved": "https://registry.npmjs.org/package/version.tgz", - "integrity": "sha512-mock-integrity-hash" - }, - "packages/fs/node_modules/path-type": { - "version": "6.0.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, + "node_modules/yesno": { + "version": "0.4.0", + "license": "BSD", "resolved": "https://registry.npmjs.org/package/version.tgz", "integrity": "sha512-mock-integrity-hash" } diff --git a/internal/shrinkwrap-extractor/test/expected/package.c/npm-shrinkwrap.json b/internal/shrinkwrap-extractor/test/expected/package.c/npm-shrinkwrap.json new file mode 100644 index 00000000000..85ff3e55e92 --- /dev/null +++ b/internal/shrinkwrap-extractor/test/expected/package.c/npm-shrinkwrap.json @@ -0,0 +1,57 @@ +{ + "name": "@ui5/target", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ui5/target", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@sapui5/some-thirdparty": "^2.0.0", + "@ui5/module-a": "^1.0.0", + "@ui5/module-b": "^1.0.0" + }, + "devDependencies": {} + }, + "node_modules/@sapui5/some-thirdparty": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/package/version.tgz", + "integrity": "sha512-mock-integrity-hash" + }, + "node_modules/@ui5/module-a/node_modules/@sapui5/some-thirdparty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package/version.tgz", + "integrity": "sha512-mock-integrity-hash" + }, + "node_modules/@ui5/module-b/node_modules/@sapui5/some-thirdparty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package/version.tgz", + "integrity": "sha512-mock-integrity-hash" + }, + "node_modules/@ui5/module-a": { + "name": "@ui5/module-a", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@ui5/module-b": "^1.0.0", + "@sapui5/some-thirdparty": "^1.0.0" + }, + "devDependencies": {}, + "resolved": "https://registry.npmjs.org/package/version.tgz", + "integrity": "sha512-mock-integrity-hash" + }, + "node_modules/@ui5/module-b": { + "name": "@ui5/module-b", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@sapui5/some-thirdparty": "^1.0.0" + }, + "devDependencies": {}, + "resolved": "https://registry.npmjs.org/package/version.tgz", + "integrity": "sha512-mock-integrity-hash" + } + } +} \ No newline at end of file diff --git a/internal/shrinkwrap-extractor/test/fixture/project.c/package-lock.fixture.json b/internal/shrinkwrap-extractor/test/fixture/project.c/package-lock.fixture.json new file mode 100644 index 00000000000..240b184baef --- /dev/null +++ b/internal/shrinkwrap-extractor/test/fixture/project.c/package-lock.fixture.json @@ -0,0 +1,68 @@ +{ + "name": "@ui5/cli-monorepo", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "monorepo-root", + "version": "0.0.1", + "license": "Apache-2.0", + "workspaces": [ + "packages/target", + "packages/module-a", + "packages/module-b" + ], + "dependencies": {}, + "devDependencies": {} + }, + "node_modules/@ui5/target": { + "resolved": "packages/target", + "link": true + }, + "node_modules/@ui5/module-a": { + "resolved": "packages/module-a", + "link": true + }, + "node_modules/@ui5/module-b": { + "resolved": "packages/module-b", + "link": true + }, + "packages/target": { + "name": "@ui5/target", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@sapui5/some-thirdparty": "^2.0.0", + "@ui5/module-a": "^1.0.0", + "@ui5/module-b": "^1.0.0" + }, + "devDependencies": {} + }, + "packages/module-a": { + "name": "@ui5/module-a", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@ui5/module-b": "^1.0.0", + "@sapui5/some-thirdparty": "^1.0.0" + }, + "devDependencies": {} + }, + "packages/module-b": { + "name": "@ui5/module-b", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@sapui5/some-thirdparty": "^1.0.0" + }, + "devDependencies": {} + }, + "node_modules/@sapui5/some-thirdparty": { + "version": "1.0.0" + }, + "packages/target/node_modules/@sapui5/some-thirdparty": { + "version": "2.0.0" + } + } +} diff --git a/internal/shrinkwrap-extractor/test/lib/convertToShrinkwrap.js b/internal/shrinkwrap-extractor/test/lib/convertToShrinkwrap.js index 018776b6121..783085a1d9a 100644 --- a/internal/shrinkwrap-extractor/test/lib/convertToShrinkwrap.js +++ b/internal/shrinkwrap-extractor/test/lib/convertToShrinkwrap.js @@ -84,7 +84,42 @@ test("Convert package-lock.json to shrinkwrap", async (t) => { console.log(`Generated shrinkwrap with ${packagePaths.length - 1} dependencies`); }); -test("Compare generated shrinkwrap with expected result", async (t) => { +test("Workspace paths should be normalized to node_modules format", async (t) => { + const __dirname = import.meta.dirname; + + const cwd = path.join(__dirname, "..", "fixture", "project.a"); + const symlinkPath = await setupFixtureSymlink(cwd); + t.after(async () => await unlink(symlinkPath).catch(() => {})); + + const targetPackageName = "@ui5/cli"; + const shrinkwrapJson = await convertPackageLockToShrinkwrap(cwd, targetPackageName); + + // Verify that no package paths contain workspace prefixes like "packages/cli/node_modules/..." + const packagePaths = Object.keys(shrinkwrapJson.packages); + + for (const packagePath of packagePaths) { + // Skip root package (empty string) + if (packagePath === "") continue; + + // Assert that no path starts with "packages/" + assert.ok(!packagePath.startsWith("packages/"), + `Package path "${packagePath}" should not start with "packages/" prefix`); + + // Assert that non-root paths start with "node_modules/" + assert.ok(packagePath.startsWith("node_modules/"), + `Package path "${packagePath}" should start with "node_modules/" prefix`); + } + + // Specifically check a package that would have been under packages/cli/node_modules in the monorepo + // The "@npmcli/config" package is a direct dependency that exists in the CLI's node_modules + const npmCliConfigPackage = shrinkwrapJson.packages["node_modules/@npmcli/config"]; + assert.ok(npmCliConfigPackage, "The '@npmcli/config' package should be present at normalized path"); + assert.equal(npmCliConfigPackage.version, "9.0.0", "@npmcli/config package should have correct version"); + + console.log(`✓ All ${packagePaths.length - 1} package paths correctly normalized`); +}); + +test("Compare generated shrinkwrap with expected result: package.a", async (t) => { // Setup mock to prevent actual npm registry requests const mockRestore = setupPacoteMock(); t.after(() => mockRestore()); @@ -137,8 +172,7 @@ test("Compare generated shrinkwrap with expected result", async (t) => { "Generated shrinkwrap packages should match expected"); }); - -test("Compare generated shrinkwrap with expected result", async (t) => { +test("Compare generated shrinkwrap with expected result: package.b", async (t) => { // Setup mock to prevent actual npm registry requests const mockRestore = setupPacoteMock(); t.after(() => mockRestore()); @@ -169,6 +203,37 @@ test("Compare generated shrinkwrap with expected result", async (t) => { "Generated shrinkwrap packages should match expected"); }); +test("Compare generated shrinkwrap with expected result: package.c", async (t) => { + // Setup mock to prevent actual npm registry requests + const mockRestore = setupPacoteMock(); + t.after(() => mockRestore()); + + const __dirname = import.meta.dirname; + const generatedShrinkwrapPath = path.join(__dirname, "..", "tmp", "package.c", "npm-shrinkwrap.generated.json"); + // Clean any existing generated file + await mkdir(path.dirname(generatedShrinkwrapPath), {recursive: true}); + await unlink(generatedShrinkwrapPath).catch(() => {}); + + // Generate shrinkwrap from fixture + const cwd = path.join(__dirname, "..", "fixture", "project.c"); + const symlinkPath = await setupFixtureSymlink(cwd); + t.after(async () => await unlink(symlinkPath).catch(() => {})); + + const targetPackageName = "@ui5/target"; + + const generatedShrinkwrap = await convertPackageLockToShrinkwrap(cwd, targetPackageName); + + // Load expected shrinkwrap + const expectedShrinkwrapPath = path.join(__dirname, "..", "expected", "package.c", "npm-shrinkwrap.json"); + const expectedShrinkwrap = await readJson(expectedShrinkwrapPath); + + // Write generated shrinkwrap to tmp dir for debugging purposes + await writeFile(generatedShrinkwrapPath, JSON.stringify(generatedShrinkwrap, null, "\t"), "utf-8"); + + assert.deepEqual(generatedShrinkwrap.packages, expectedShrinkwrap.packages, + "Generated shrinkwrap packages should match expected"); +}); + // Error handling tests test("Error handling - invalid target package name", async (t) => { const __dirname = import.meta.dirname;