From ed87c99359b06d4e6bafd7d00067525eec67461c Mon Sep 17 00:00:00 2001 From: Shunta Takemoto <58549977+shuntatakemoto@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:06:01 +0900 Subject: [PATCH] feat: treat bare `workspace:` protocol as `workspace:*` (#10436) * feat: treat bare `workspace:` protocol as `workspace:*` * chore: add chageset * test(exportable-manifest): add test for `workspace` with explicit versions * test: add tests and update changesets --------- Co-authored-by: Zoltan Kochan --- .changeset/nine-areas-glow.md | 8 +++++ cspell.json | 4 +++ pkg-manifest/exportable-manifest/src/index.ts | 7 +++-- .../exportable-manifest/test/index.test.ts | 24 ++++++++++++++ .../npm-resolver/src/workspacePrefToNpm.ts | 2 +- .../test/workspacePrefToNpm.test.ts | 6 ++-- .../resolve-workspace-range/package.json | 5 +-- .../resolve-workspace-range/src/index.ts | 2 +- .../test/index.test.ts | 31 +++++++++++++++++++ .../test/tsconfig.json | 18 +++++++++++ 10 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 .changeset/nine-areas-glow.md create mode 100644 workspace/resolve-workspace-range/test/index.test.ts create mode 100644 workspace/resolve-workspace-range/test/tsconfig.json diff --git a/.changeset/nine-areas-glow.md b/.changeset/nine-areas-glow.md new file mode 100644 index 0000000000..4fdea315f1 --- /dev/null +++ b/.changeset/nine-areas-glow.md @@ -0,0 +1,8 @@ +--- +"@pnpm/resolve-workspace-range": minor +"@pnpm/exportable-manifest": minor +"@pnpm/npm-resolver": minor +pnpm: minor +--- + +Support bare `workspace:` protocol without version specifier. It is now treated as `workspace:*` and resolves to the concrete version during publish [#10436](https://github.com/pnpm/pnpm/pull/10436). diff --git a/cspell.json b/cspell.json index 06fbddae3a..39e255e7e9 100644 --- a/cspell.json +++ b/cspell.json @@ -36,6 +36,7 @@ "copyfiles", "corejs", "corepack", + "corge", "cowsay", "cves", "cwsay", @@ -83,6 +84,7 @@ "forgejo", "fsevents", "gabor", + "garply", "gcttmf", "getattr", "ghsa", @@ -90,6 +92,7 @@ "gitea", "globalconfig", "globstar", + "grault", "gruntfile", "gwhitney", "haptics", @@ -181,6 +184,7 @@ "pkgname", "pkgs", "plotly", + "plugh", "pnpmfile", "pnpmfiles", "pnpmjs", diff --git a/pkg-manifest/exportable-manifest/src/index.ts b/pkg-manifest/exportable-manifest/src/index.ts index 0044a9446d..3ef0dd0737 100644 --- a/pkg-manifest/exportable-manifest/src/index.ts +++ b/pkg-manifest/exportable-manifest/src/index.ts @@ -136,13 +136,14 @@ async function replaceWorkspaceProtocol (depName: string, depSpec: string, dir: return depSpec } - // Dependencies with bare "*", "^" and "~" versions - const versionAliasSpecParts = /^workspace:(.*?)@?([\^~*])$/.exec(depSpec) + // Dependencies with bare "*", "^", "~" versions, or no version (workspace:) + const versionAliasSpecParts = /^workspace:(?:(.+)@)?([\^~*])?$/.exec(depSpec) if (versionAliasSpecParts != null) { modulesDir = modulesDir ?? path.join(dir, 'node_modules') const manifest = await readAndCheckManifest(depName, path.join(modulesDir, depName)) - const semverRangeToken = versionAliasSpecParts[2] !== '*' ? versionAliasSpecParts[2] : '' + const specifierSuffix: string | undefined = versionAliasSpecParts[2] + const semverRangeToken = specifierSuffix === '^' || specifierSuffix === '~' ? specifierSuffix : '' if (depName !== manifest.name) { return `npm:${manifest.name!}@${semverRangeToken}${manifest.version}` } diff --git a/pkg-manifest/exportable-manifest/test/index.test.ts b/pkg-manifest/exportable-manifest/test/index.test.ts index 42ac9eb17f..4fe2b93339 100644 --- a/pkg-manifest/exportable-manifest/test/index.test.ts +++ b/pkg-manifest/exportable-manifest/test/index.test.ts @@ -96,9 +96,13 @@ test('workspace deps are replaced', async () => { baz: 'workspace:baz@^', foo: 'workspace:*', qux: 'workspace:^', + quux: 'workspace:', waldo: 'workspace:^', xerox: 'workspace:../xerox', xeroxAlias: 'workspace:../xerox', + corge: 'workspace:1.0.0', + grault: 'workspace:^1.0.0', + garply: 'workspace:plugh@2.0.0', }, peerDependencies: { foo: 'workspace:>= || ^3.9.0', @@ -127,6 +131,10 @@ test('workspace deps are replaced', async () => { name: 'qux', version: '1.0.0-alpha-a.b-c-something+build.1-aef.1-its-okay', }, + { + name: 'quux', + version: '7.8.9', + }, { name: 'waldo', version: '1.9.0', @@ -135,6 +143,18 @@ test('workspace deps are replaced', async () => { name: 'xerox', version: '4.5.6', }, + { + name: 'corge', + version: '1.0.0', + }, + { + name: 'grault', + version: '1.0.0', + }, + { + name: 'plugh', + version: '2.0.0', + }, ]) writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] }) @@ -151,9 +171,13 @@ test('workspace deps are replaced', async () => { baz: '^1.2.3', foo: '4.5.6', qux: '^1.0.0-alpha-a.b-c-something+build.1-aef.1-its-okay', + quux: '7.8.9', waldo: '^1.9.0', xerox: '4.5.6', xeroxAlias: 'npm:xerox@4.5.6', + corge: '1.0.0', + grault: '^1.0.0', + garply: 'npm:plugh@2.0.0', }, peerDependencies: { baz: '^1.0.0 || >1.2.3', diff --git a/resolving/npm-resolver/src/workspacePrefToNpm.ts b/resolving/npm-resolver/src/workspacePrefToNpm.ts index 08c8c12658..176948cdc5 100644 --- a/resolving/npm-resolver/src/workspacePrefToNpm.ts +++ b/resolving/npm-resolver/src/workspacePrefToNpm.ts @@ -7,7 +7,7 @@ export function workspacePrefToNpm (workspaceBareSpecifier: string): string { } const { alias, version } = parseResult - const versionPart = version === '^' || version === '~' ? '*' : version + const versionPart = version === '^' || version === '~' || version === '' ? '*' : version return alias ? `npm:${alias}@${versionPart}` : versionPart diff --git a/resolving/npm-resolver/test/workspacePrefToNpm.test.ts b/resolving/npm-resolver/test/workspacePrefToNpm.test.ts index c0e8b8850e..964d02f95e 100644 --- a/resolving/npm-resolver/test/workspacePrefToNpm.test.ts +++ b/resolving/npm-resolver/test/workspacePrefToNpm.test.ts @@ -2,8 +2,10 @@ import { workspacePrefToNpm } from '../lib/workspacePrefToNpm.js' describe('workspacePrefToNpm', () => { test('resolve workspace only version aliases', async () => { - expect(workspacePrefToNpm('workspace:^')).toStrictEqual('*') - expect(workspacePrefToNpm('workspace:~')).toStrictEqual('*') + expect(workspacePrefToNpm('workspace:')).toBe('*') + expect(workspacePrefToNpm('workspace:*')).toBe('*') + expect(workspacePrefToNpm('workspace:^')).toBe('*') + expect(workspacePrefToNpm('workspace:~')).toBe('*') }) test('resolve package name aliases', async () => { diff --git a/workspace/resolve-workspace-range/package.json b/workspace/resolve-workspace-range/package.json index b7198eebc0..abc58491e6 100644 --- a/workspace/resolve-workspace-range/package.json +++ b/workspace/resolve-workspace-range/package.json @@ -25,8 +25,9 @@ ], "scripts": { "start": "tsc --watch", - "test": "pnpm run compile", - "lint": "eslint \"src/**/*.ts\"", + "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"", + "_test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest", + "test": "pnpm run compile && pnpm run _test", "prepublishOnly": "pnpm run compile", "compile": "tsc --build && pnpm run lint --fix" }, diff --git a/workspace/resolve-workspace-range/src/index.ts b/workspace/resolve-workspace-range/src/index.ts index fafd9e2dad..87f6c85b7a 100644 --- a/workspace/resolve-workspace-range/src/index.ts +++ b/workspace/resolve-workspace-range/src/index.ts @@ -1,7 +1,7 @@ import semver from 'semver' export function resolveWorkspaceRange (range: string, versions: string[]): string | null { - if (range === '*' || range === '^' || range === '~') { + if (range === '*' || range === '^' || range === '~' || range === '') { return semver.maxSatisfying(versions, '*', { includePrerelease: true, }) diff --git a/workspace/resolve-workspace-range/test/index.test.ts b/workspace/resolve-workspace-range/test/index.test.ts new file mode 100644 index 0000000000..1fad054776 --- /dev/null +++ b/workspace/resolve-workspace-range/test/index.test.ts @@ -0,0 +1,31 @@ +import { resolveWorkspaceRange } from '@pnpm/resolve-workspace-range' + +describe('resolveWorkspaceRange', () => { + const versions = ['1.0.0', '2.0.0', '3.0.0-beta.1'] + + test('resolves * to max version including prereleases', () => { + expect(resolveWorkspaceRange('*', versions)).toBe('3.0.0-beta.1') + }) + + test('resolves ^ to max version including prereleases', () => { + expect(resolveWorkspaceRange('^', versions)).toBe('3.0.0-beta.1') + }) + + test('resolves ~ to max version including prereleases', () => { + expect(resolveWorkspaceRange('~', versions)).toBe('3.0.0-beta.1') + }) + + test('resolves empty string (bare workspace:) to max version including prereleases', () => { + expect(resolveWorkspaceRange('', versions)).toBe('3.0.0-beta.1') + }) + + test('resolves semver range', () => { + expect(resolveWorkspaceRange('^1.0.0', versions)).toBe('1.0.0') + expect(resolveWorkspaceRange('^2.0.0', versions)).toBe('2.0.0') + expect(resolveWorkspaceRange('>=1.0.0', versions)).toBe('2.0.0') + }) + + test('returns null when no version satisfies range', () => { + expect(resolveWorkspaceRange('^4.0.0', versions)).toBeNull() + }) +}) diff --git a/workspace/resolve-workspace-range/test/tsconfig.json b/workspace/resolve-workspace-range/test/tsconfig.json new file mode 100644 index 0000000000..67ce5e1d0e --- /dev/null +++ b/workspace/resolve-workspace-range/test/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "../node_modules/.test.lib", + "rootDir": "..", + "isolatedModules": true + }, + "include": [ + "**/*.ts", + "../../../__typings__/**/*.d.ts" + ], + "references": [ + { + "path": ".." + } + ] +}