diff --git a/.changeset/giant-carrots-heal.md b/.changeset/giant-carrots-heal.md new file mode 100644 index 0000000000..77b710270d --- /dev/null +++ b/.changeset/giant-carrots-heal.md @@ -0,0 +1,8 @@ +--- +"@pnpm/exportable-manifest": minor +"@pnpm/npm-resolver": minor +"@pnpm/plugin-commands-publishing": minor +"@pnpm/resolve-workspace-range": minor +--- + +Add support for workspace:^ and workspace:~ aliases diff --git a/packages/exportable-manifest/src/index.ts b/packages/exportable-manifest/src/index.ts index 9c04bc9591..bf0bdcef02 100644 --- a/packages/exportable-manifest/src/index.ts +++ b/packages/exportable-manifest/src/index.ts @@ -72,7 +72,10 @@ async function makePublishDependency (depName: string, depSpec: string, dir: str if (!depSpec.startsWith('workspace:')) { return depSpec } - if (depSpec === 'workspace:*' || depSpec.endsWith('@*')) { + + // Dependencies with bare "*", "^" and "~" versions + const versionAliasSpecParts = /^workspace:([^@]+@)?([\^~*])$/.exec(depSpec) + if (versionAliasSpecParts != null) { const { manifest } = await tryReadProjectManifest(path.join(dir, 'node_modules', depName)) if ((manifest == null) || !manifest.version) { throw new PnpmError( @@ -81,10 +84,12 @@ async function makePublishDependency (depName: string, depSpec: string, dir: str 'because this dependency is not installed. Try running "pnpm install".' ) } + + const semverRangeToken = versionAliasSpecParts[2] !== '*' ? versionAliasSpecParts[2] : '' if (depName !== manifest.name) { - return `npm:${manifest.name!}@${manifest.version}` + return `npm:${manifest.name!}@${semverRangeToken}${manifest.version}` } - return manifest.version + return `${semverRangeToken}${manifest.version}` } if (depSpec.startsWith('workspace:./') || depSpec.startsWith('workspace:../')) { const { manifest } = await tryReadProjectManifest(path.join(dir, depSpec.substr(10))) diff --git a/packages/npm-resolver/src/index.ts b/packages/npm-resolver/src/index.ts index c7339b3750..9b7c4628b0 100644 --- a/packages/npm-resolver/src/index.ts +++ b/packages/npm-resolver/src/index.ts @@ -191,6 +191,23 @@ async function resolveNpm ( } } +function workspacePrefToNpm (workspacePref: string): string { + const prefParts = /^workspace:([^@]+@)?(.*)$/.exec(workspacePref) + if (prefParts == null) { + throw new Error(`Invalid workspace spec: ${workspacePref}`) + } + const [workspacePkgAlias, workspaceVersion] = prefParts.slice(1) + + const pkgAliasPart = workspacePkgAlias != null && workspacePkgAlias + ? `npm:${workspacePkgAlias}` + : '' + const versionPart = workspaceVersion === '^' || workspaceVersion === '~' + ? '*' + : workspaceVersion + + return `${pkgAliasPart}${versionPart}` +} + function tryResolveFromWorkspace ( wantedDependency: WantedDependency, opts: { @@ -203,10 +220,8 @@ function tryResolveFromWorkspace ( if (!wantedDependency.pref?.startsWith('workspace:')) { return null } - let pref = wantedDependency.pref.substr(10) - if (pref.includes('@', 1)) { - pref = `npm:${pref}` - } + const pref = workspacePrefToNpm(wantedDependency.pref) + const spec = parsePref(pref, wantedDependency.alias, opts.defaultTag, opts.registry) if (spec == null) throw new Error(`Invalid workspace: spec (${wantedDependency.pref})`) if (opts.workspacePackages == null) { diff --git a/packages/npm-resolver/test/index.ts b/packages/npm-resolver/test/index.ts index da6fd514d1..3288719e4c 100644 --- a/packages/npm-resolver/test/index.ts +++ b/packages/npm-resolver/test/index.ts @@ -1547,3 +1547,67 @@ test('request to a package with malformed metadata', async () => { new PnpmError('MALFORMED_METADATA', 'Received malformed metadata for "code-snippet"') ) }) + +test('resolve workspace:^', async () => { + const storeDir = tempy.directory() + const resolve = createResolveFromNpm({ + storeDir, + }) + const resolveResult = await resolve({ alias: 'is-positive', pref: 'workspace:^' }, { + projectDir: '/home/istvan/src', + registry, + workspacePackages: { + 'is-positive': { + '1.0.0': { + dir: '/home/istvan/src/is-positive', + manifest: { + name: 'is-positive', + version: '1.0.0', + }, + }, + }, + }, + }) + + expect(resolveResult!.resolvedVia).toBe('local-filesystem') + expect(resolveResult!.id).toBe('link:is-positive') + expect(resolveResult!.resolution).toStrictEqual({ + directory: '/home/istvan/src/is-positive', + type: 'directory', + }) + expect(resolveResult!.manifest).toBeTruthy() + expect(resolveResult!.manifest!.name).toBe('is-positive') + expect(resolveResult!.manifest!.version).toBe('1.0.0') +}) + +test('resolve workspace:~', async () => { + const storeDir = tempy.directory() + const resolve = createResolveFromNpm({ + storeDir, + }) + const resolveResult = await resolve({ alias: 'is-positive', pref: 'workspace:~' }, { + projectDir: '/home/istvan/src', + registry, + workspacePackages: { + 'is-positive': { + '1.0.0': { + dir: '/home/istvan/src/is-positive', + manifest: { + name: 'is-positive', + version: '1.0.0', + }, + }, + }, + }, + }) + + expect(resolveResult!.resolvedVia).toBe('local-filesystem') + expect(resolveResult!.id).toBe('link:is-positive') + expect(resolveResult!.resolution).toStrictEqual({ + directory: '/home/istvan/src/is-positive', + type: 'directory', + }) + expect(resolveResult!.manifest).toBeTruthy() + expect(resolveResult!.manifest!.name).toBe('is-positive') + expect(resolveResult!.manifest!.version).toBe('1.0.0') +}) diff --git a/packages/plugin-commands-publishing/test/publish.ts b/packages/plugin-commands-publishing/test/publish.ts index 2dd047b638..8e21230cc4 100644 --- a/packages/plugin-commands-publishing/test/publish.ts +++ b/packages/plugin-commands-publishing/test/publish.ts @@ -351,12 +351,16 @@ test('convert specs with workspace protocols to regular version ranges', async ( 'is-positive': '1.0.0', 'lodash.delay': '~4.1.0', odd: 'workspace:is-odd@*', + rd: 'workspace:ramda@^', + 'word-wrap': 'workspace:~', }, devDependencies: { 'random-package': 'workspace:^1.2.3', + through: 'workspace:^', }, optionalDependencies: { 'lodash.deburr': 'workspace:^4.1.0', + ww: 'workspace:wordwrap@~', }, peerDependencies: { 'random-package': 'workspace:*', @@ -394,6 +398,22 @@ test('convert specs with workspace protocols to regular version ranges', async ( name: 'target', version: '1.0.0', }, + { + name: 'ramda', + version: '0.1.0', + }, + { + name: 'word-wrap', + version: '0.1.0', + }, + { + name: 'through', + version: '0.0.1', + }, + { + name: 'wordwrap', + version: '0.0.1', + }, ]) await writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] }) @@ -417,6 +437,7 @@ test('convert specs with workspace protocols to regular version ranges', async ( crossSpawn.sync(pnpmBin, ['multi', 'install', '--store-dir=store', `--registry=http://localhost:${REGISTRY_MOCK_PORT}`]) process.chdir('workspace-protocol-package') + await publish.handler({ ...DEFAULT_OPTS, argv: { original: ['publish', ...CREDENTIALS] }, @@ -435,12 +456,16 @@ test('convert specs with workspace protocols to regular version ranges', async ( 'lodash.delay': '~4.1.0', even: 'npm:is-even@^1.0.0', odd: 'npm:is-odd@1.0.0', + rd: 'npm:ramda@^0.1.0', + 'word-wrap': '~0.1.0', }) expect(publishedManifest.devDependencies).toStrictEqual({ 'random-package': '^1.2.3', + through: '^0.0.1', }) expect(publishedManifest.optionalDependencies).toStrictEqual({ 'lodash.deburr': '^4.1.0', + ww: 'npm:wordwrap@~0.0.1', }) expect(publishedManifest.peerDependencies).toStrictEqual({ 'random-package': '1.2.3', diff --git a/packages/resolve-workspace-range/src/index.ts b/packages/resolve-workspace-range/src/index.ts index ad16ca13f1..e2bb4df40e 100644 --- a/packages/resolve-workspace-range/src/index.ts +++ b/packages/resolve-workspace-range/src/index.ts @@ -1,7 +1,7 @@ import semver from 'semver' export default function (range: string, versions: string[]) { - if (range === '*') { + if (range === '*' || range === '^' || range === '~') { return semver.maxSatisfying(versions, '*', { includePrerelease: true, })