diff --git a/internal/file/finder_test.go b/internal/file/finder_test.go index c0a2ef4f..991caa90 100644 --- a/internal/file/finder_test.go +++ b/internal/file/finder_test.go @@ -110,7 +110,7 @@ func TestGetGroups(t *testing.T) { path := "" excludedFiles := []string{"testdata/go/go.mod", "testdata/misc/requirements.txt", "testdata/misc/Cargo.lock"} - const nbrOfGroups = 9 + const nbrOfGroups = 12 fileGroups, err := finder.GetGroups( DebrickedOptions{ @@ -349,7 +349,7 @@ func TestGetGroupsWithStrictFlag(t *testing.T) { name: "StrictnessSetTo0", strictness: StrictAll, testedGroupIndex: 3, - expectedNumberOfGroups: 14, + expectedNumberOfGroups: 11, expectedManifestFile: "composer.json", expectedLockFiles: []string{"composer.lock", "go.mod", "Cargo.lock", "requirements.txt.pip.debricked"}, }, @@ -357,7 +357,7 @@ func TestGetGroupsWithStrictFlag(t *testing.T) { name: "StrictnessSetTo1", strictness: StrictLockAndPairs, testedGroupIndex: 1, - expectedNumberOfGroups: 9, + expectedNumberOfGroups: 6, expectedManifestFile: "", expectedLockFiles: []string{ "composer.lock", "composer.lock", "go.mod", "Cargo.lock", "requirements.txt.pip.debricked", "requirements-dev.txt.pip.debricked", @@ -367,7 +367,7 @@ func TestGetGroupsWithStrictFlag(t *testing.T) { name: "StrictnessSetTo2", strictness: StrictPairs, testedGroupIndex: 0, - expectedNumberOfGroups: 7, + expectedNumberOfGroups: 4, expectedManifestFile: "composer.json", expectedLockFiles: []string{ "composer.lock", "requirements-dev.txt.pip.debricked.lock", "requirements.txt.pip.debricked.lock", @@ -381,7 +381,7 @@ func TestGetGroupsWithStrictFlag(t *testing.T) { fileGroups, err := finder.GetGroups( DebrickedOptions{ RootPath: filePath, - Exclusions: []string{"**/node_modules/**"}, + Exclusions: []string{"**/node_modules/**", "**/workspace/**"}, Inclusions: []string{}, LockFileOnly: false, Strictness: c.strictness, diff --git a/internal/file/groups.go b/internal/file/groups.go index 51519925..b732a8b3 100644 --- a/internal/file/groups.go +++ b/internal/file/groups.go @@ -130,12 +130,12 @@ func (gs *Groups) matchWorkspace(workspaceManifest WorkspaceManifest) { func (gs *Groups) AddWorkspaceLockFiles() { for _, group := range gs.groups { - workspaces, err := getWorkspaces(group.ManifestFile) + workspaces, err := getPackageJSONWorkspaces(group.ManifestFile) if err == nil && group.HasLockFiles() { workspaceManifest := WorkspaceManifest{ - LockFiles: group.LockFiles, - RootManifest: group.ManifestFile, - Workspaces: workspaces, + LockFiles: group.LockFiles, + RootManifest: group.ManifestFile, + WorkspacePatterns: workspaces, } gs.matchWorkspace(workspaceManifest) } diff --git a/internal/file/groups_test.go b/internal/file/groups_test.go index a91d61ea..edf97534 100644 --- a/internal/file/groups_test.go +++ b/internal/file/groups_test.go @@ -198,14 +198,18 @@ func TestGroupsMatchWorkspaces(t *testing.T) { gs.Add(*g4) workspaceManifest := WorkspaceManifest{ - LockFiles: []string{"package-lock.json"}, - RootManifest: "package.json", - Workspaces: []string{"package/*"}, + LockFiles: []string{"package-lock.json"}, + RootManifest: "package.json", + WorkspacePatterns: []string{"package/*", "package\\*"}, } gs.matchWorkspace(workspaceManifest) for _, g := range gs.groups { if g.ManifestFile == "package/file3" { - assert.Equal(t, g.LockFiles[0], g1.LockFiles[0]) + if g.HasLockFiles() { + assert.Equal(t, g.LockFiles[0], g1.LockFiles[0]) + } else { + assert.True(t, false) + } } if g.ManifestFile == "pack/file3" { assert.Equal(t, len(g3.LockFiles), 0) @@ -215,9 +219,13 @@ func TestGroupsMatchWorkspaces(t *testing.T) { } func TestAddWorkspaceLockFiles(t *testing.T) { - g1 := NewGroup("testdata/workspace/package.json", nil, []string{"testdata/workspace/package-lock.json"}) - g2 := NewGroup("testdata/workspace/packages/package_one/package.json", nil, []string{}) - g3 := NewGroup("testdata/workspace/packages/package_two/package.json", nil, []string{}) + g1 := NewGroup( + "testdata/workspace/common/package.json", + nil, + []string{"testdata/workspace/common/package-lock.json"}, + ) + g2 := NewGroup("testdata/workspace/common/packages/package_one/package.json", nil, []string{}) + g3 := NewGroup("testdata/workspace/common/packages/package_two/package.json", nil, []string{}) gs := Groups{} gs.Add(*g1) @@ -225,7 +233,7 @@ func TestAddWorkspaceLockFiles(t *testing.T) { gs.Add(*g3) gs.AddWorkspaceLockFiles() for _, g := range gs.groups { - assert.Equal(t, len(g.LockFiles), 1) + assert.Equal(t, 1, len(g.LockFiles)) if len(g.LockFiles) == 1 { assert.Equal(t, g.LockFiles[0], g1.LockFiles[0]) } diff --git a/internal/file/testdata/workspace/package-lock.json b/internal/file/testdata/workspace/common/package-lock.json similarity index 100% rename from internal/file/testdata/workspace/package-lock.json rename to internal/file/testdata/workspace/common/package-lock.json diff --git a/internal/file/testdata/workspace/package.json b/internal/file/testdata/workspace/common/package.json similarity index 71% rename from internal/file/testdata/workspace/package.json rename to internal/file/testdata/workspace/common/package.json index b9e6664a..df51b109 100644 --- a/internal/file/testdata/workspace/package.json +++ b/internal/file/testdata/workspace/common/package.json @@ -7,6 +7,6 @@ "typescript": "5.5.4" }, "workspaces": [ - "testdata/workspace/packages/*", "testdata\\workspace\\packages\\*" + "packages/*" ] } diff --git a/internal/file/testdata/workspace/packages/package_one/package.json b/internal/file/testdata/workspace/common/packages/package_one/package.json similarity index 100% rename from internal/file/testdata/workspace/packages/package_one/package.json rename to internal/file/testdata/workspace/common/packages/package_one/package.json diff --git a/internal/file/testdata/workspace/packages/package_two/package.json b/internal/file/testdata/workspace/common/packages/package_two/package.json similarity index 100% rename from internal/file/testdata/workspace/packages/package_two/package.json rename to internal/file/testdata/workspace/common/packages/package_two/package.json diff --git a/internal/file/testdata/workspace/rare/package-lock.json b/internal/file/testdata/workspace/rare/package-lock.json new file mode 100644 index 00000000..e625a7c4 --- /dev/null +++ b/internal/file/testdata/workspace/rare/package-lock.json @@ -0,0 +1,650 @@ +{ + "name": "npm-workspaces-test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "npm-workspaces-test", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "prettier": "3.3.3", + "rimraf": "6.0.1", + "typescript": "5.5.4" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "20.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", + "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/package_one": { + "resolved": "packages/package_one", + "link": true + }, + "node_modules/package_two": { + "resolved": "packages/package_two", + "link": true + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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==", + "dev": true, + "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" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "packages/package_one": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/node": "^20.0.0" + }, + "devDependencies": { + "typescript": "5.5.4" + } + }, + "packages/package_two": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@types/node": "16.11.40", + "minimist": "^1.2.5", + "package_one": "^1.0.0" + }, + "devDependencies": { + "typescript": "5.5.4" + } + }, + "packages/package_two/node_modules/@types/node": { + "version": "16.11.40", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.40.tgz", + "integrity": "sha512-7bOWglXUO6f21NG3YDI7hIpeMX3M59GG+DzZuzX2EkFKYUnRoxq3EOg4R0KNv2hxryY9M3UUqG5akwwsifrukw==", + "license": "MIT" + } + } +} diff --git a/internal/file/testdata/workspace/rare/package.json b/internal/file/testdata/workspace/rare/package.json new file mode 100644 index 00000000..9a106f14 --- /dev/null +++ b/internal/file/testdata/workspace/rare/package.json @@ -0,0 +1,14 @@ +{ + "name": "npm-workspaces-test", + "private": true, + "devDependencies": { + "prettier": "3.3.3", + "rimraf": "6.0.1", + "typescript": "5.5.4" + }, + "workspaces": { + "packages": [ + "packages/*" + ] + } +} diff --git a/internal/file/testdata/workspace/rare/packages/package_one/package.json b/internal/file/testdata/workspace/rare/packages/package_one/package.json new file mode 100644 index 00000000..d74a7c3a --- /dev/null +++ b/internal/file/testdata/workspace/rare/packages/package_one/package.json @@ -0,0 +1,15 @@ +{ + "name": "package_one", + "version": "1.0.0", + "description": "testing workspaces", + "main": "lib/index.js", + "keywords": [], + "author": "test", + "license": "MIT", + "dependencies": { + "@types/node": "^20.0.0" + }, + "devDependencies": { + "typescript": "5.5.4" + } +} diff --git a/internal/file/testdata/workspace/rare/packages/package_two/package.json b/internal/file/testdata/workspace/rare/packages/package_two/package.json new file mode 100644 index 00000000..140b2f19 --- /dev/null +++ b/internal/file/testdata/workspace/rare/packages/package_two/package.json @@ -0,0 +1,17 @@ +{ + "name": "package_two", + "version": "1.0.0", + "description": "test", + "main": "lib/main.js", + "keywords": [], + "author": "test", + "license": "MIT", + "dependencies": { + "package_one": "^1.0.0", + "@types/node": "16.11.40", + "minimist": "^1.2.5" + }, + "devDependencies": { + "typescript": "5.5.4" + } +} diff --git a/internal/file/workspace.go b/internal/file/workspace.go index 6ae6f57a..eef29ed9 100644 --- a/internal/file/workspace.go +++ b/internal/file/workspace.go @@ -3,45 +3,71 @@ package file import ( "encoding/json" "os" + "path/filepath" "github.com/becheran/wildmatch-go" ) type WorkspaceManifest struct { - RootManifest string - LockFiles []string - Workspaces []string + RootManifest string + LockFiles []string + WorkspacePatterns []string } -type NPMPackageJson struct { +// NPM Workspaces docs: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#workspaces +// Yarn Workspaces docs: https://yarnpkg.com/features/workspaces +type PackageJSON struct { Name string `json:"name"` Workspaces []string `json:"workspaces"` } +type NestledPackageJSON struct { + Workspaces struct { + Packages []string `json:"packages"` + } `json:"workspaces"` +} // Rare format + func (workspaceManifest *WorkspaceManifest) matchManifest(manifestPath string) bool { - for _, workspacePattern := range workspaceManifest.Workspaces { + manifestPath = filepath.ToSlash(manifestPath) // Normalize + relativeManifestPath, err := filepath.Rel( + filepath.ToSlash(filepath.Dir(workspaceManifest.RootManifest)), + manifestPath, + ) + if err != nil { + relativeManifestPath = manifestPath + } + relativeManifestPath = filepath.ToSlash(relativeManifestPath) + for _, workspacePattern := range workspaceManifest.WorkspacePatterns { + workspacePattern = filepath.ToSlash(workspacePattern) pattern := wildmatch.NewWildMatch(workspacePattern) - if pattern.IsMatch(manifestPath) { + if pattern.IsMatch(relativeManifestPath) { return true } + if workspacePattern == filepath.ToSlash(filepath.Dir(relativeManifestPath)) { + return true + } // Check if specific directory match } return false } -func getWorkspaces(rootManifest string) ([]string, error) { - var npmPackageJson NPMPackageJson +func getPackageJSONWorkspaces(rootManifest string) ([]string, error) { + var packageJson PackageJSON - jsonFile, err := os.Open(rootManifest) + jsonData, err := os.ReadFile(rootManifest) if err != nil { return nil, err } - defer jsonFile.Close() - decoder := json.NewDecoder(jsonFile) - err = decoder.Decode(&npmPackageJson) - if err != nil { + err = json.Unmarshal(jsonData, &packageJson) + if err != nil { // Try rare format instead + var nestledPackageJSON NestledPackageJSON + err = json.Unmarshal(jsonData, &nestledPackageJSON) + if err == nil { + return nestledPackageJSON.Workspaces.Packages, nil + } + return nil, err } - return npmPackageJson.Workspaces, nil + return packageJson.Workspaces, nil } diff --git a/internal/file/workspace_test.go b/internal/file/workspace_test.go index 6be0f133..9a2f574c 100644 --- a/internal/file/workspace_test.go +++ b/internal/file/workspace_test.go @@ -11,7 +11,12 @@ func TestMatchManifest(t *testing.T) { wm := WorkspaceManifest{ RootManifest: "package.json", LockFiles: []string{"package-lock.json"}, - Workspaces: []string{"package/*", "pkg/internal/*", "pack/internal/package.json"}, + WorkspacePatterns: []string{ + "package/*", + "pkg/internal/*", + "pack/internal/package.json", + "packages/package_two", + }, } cases := []struct { manifestFile string @@ -33,6 +38,52 @@ func TestMatchManifest(t *testing.T) { manifestFile: "pkg/internal/package.json", expected: true, }, + { + manifestFile: "packages/package_two/package.json", + expected: true, + }, + { + manifestFile: "packages/package_two/internal/package.json", + expected: false, + }, + } + + for _, c := range cases { + t.Run(c.manifestFile, func(t *testing.T) { + match := (&wm).matchManifest(c.manifestFile) + fmt.Println(c.manifestFile) + assert.Equal(t, c.expected, match) + }) + } +} + +func TestDeeperMatchManifest(t *testing.T) { + wm := WorkspaceManifest{ + RootManifest: "Src/app/package.json", + LockFiles: []string{"Src/app/package-lock.json"}, + WorkspacePatterns: []string{ + "package/*", + "pkg/internal/*", + "pack/internal/package.json", + "packages/package_two", + }, + } + cases := []struct { + manifestFile string + expected bool + }{ + { + manifestFile: "Src/app/package/test/package.json", + expected: true, + }, + { + manifestFile: "Src/app/packages/package_two/package.json", + expected: true, + }, + { + manifestFile: "Src/app/packages/package_two/internal/package.json", + expected: false, + }, } for _, c := range cases { @@ -42,16 +93,24 @@ func TestMatchManifest(t *testing.T) { assert.Equal(t, c.expected, match) }) } + } func TestGetWorkspaces(t *testing.T) { - workspaces, err := getWorkspaces("testdata/workspace/package.json") + workspaces, err := getPackageJSONWorkspaces("testdata/workspace/common/package.json") + assert.NoError(t, err) + assert.Equal(t, 1, len(workspaces)) + assert.Equal(t, "packages/*", workspaces[0]) +} + +func TestGetWorkspacesRare(t *testing.T) { + workspaces, err := getPackageJSONWorkspaces("testdata/workspace/rare/package.json") assert.NoError(t, err) - assert.Equal(t, 2, len(workspaces)) - assert.Equal(t, "testdata/workspace/packages/*", workspaces[0]) + assert.Equal(t, 1, len(workspaces)) + assert.Equal(t, "packages/*", workspaces[0]) } func TestGetWorkspacesNoFile(t *testing.T) { - _, err := getWorkspaces("testdata/non_existing_folder/package.json") + _, err := getPackageJSONWorkspaces("testdata/non_existing_folder/package.json") assert.Error(t, err) }