feat: workspace:^ and workspace:~ aliases (#3116) (#3368)

* feat: workspace:^ and workspace:~ aliases (#3116)

* fix: apply suggestions and add changeset

* fix(npm-resolver): workspace aliases

* refactor: implement suggestions

* refactor: use only one regex
This commit is contained in:
javier-garcia-meteologica
2021-04-21 17:22:08 +02:00
committed by GitHub
parent 25cea85567
commit 85fb21a837
6 changed files with 125 additions and 8 deletions

View File

@@ -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

View File

@@ -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)))

View File

@@ -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) {

View File

@@ -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')
})

View File

@@ -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',

View File

@@ -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,
})