feat: use workspace spec alias by default in pnpm add (#4947)

This commit is contained in:
javier-garcia-meteologica
2022-07-02 13:12:08 +02:00
committed by GitHub
parent e5610a579b
commit f5621a42c2
11 changed files with 305 additions and 22 deletions

View File

@@ -0,0 +1,12 @@
---
"@pnpm/manifest-utils": minor
"@pnpm/resolve-dependencies": minor
"@pnpm/which-version-is-pinned": major
"pnpm": minor
---
A new value `rolling` for option `save-workspace-protocol`. When selected, pnpm will save workspace versions using a rolling alias (e.g. `"foo": "workspace:^"`) instead of pinning the current version number (e.g. `"foo": "workspace:^1.0.0"`). Usage example:
```
pnpm --save-workspace-protocol=rolling add foo
```

View File

@@ -34,7 +34,7 @@ export interface Config {
saveDev?: boolean
saveOptional?: boolean
savePeer?: boolean
saveWorkspaceProtocol?: boolean
saveWorkspaceProtocol?: boolean | 'rolling'
scriptShell?: string
stream?: boolean
pnpmExecPath: string

View File

@@ -32,7 +32,7 @@ export interface StrictInstallOptions {
fixLockfile: boolean
ignorePackageManifest: boolean
preferFrozenLockfile: boolean
saveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean | 'rolling'
preferWorkspacePackages: boolean
preserveWorkspaceProtocol: boolean
scriptsPrependNodePath: boolean | 'warn-only'

View File

@@ -801,6 +801,38 @@ test('adding a new dependency with the workspace: protocol', async () => {
expect(manifest.dependencies).toStrictEqual({ foo: 'workspace:^1.0.0' })
})
test('adding a new dependency with the workspace: protocol and save-workspace-protocol is "rolling"', async () => {
await addDistTag({ package: 'foo', version: '1.0.0', distTag: 'latest' })
prepareEmpty()
const [{ manifest }] = await mutateModules([
{
dependencySelectors: ['foo'],
manifest: {
name: 'project-1',
version: '1.0.0',
},
mutation: 'installSome',
rootDir: path.resolve('project-1'),
},
], await testDefaults({
saveWorkspaceProtocol: 'rolling',
workspacePackages: {
foo: {
'1.0.0': {
dir: '',
manifest: {
name: 'foo',
version: '1.0.0',
},
},
},
},
}))
expect(manifest.dependencies).toStrictEqual({ foo: 'workspace:^' })
})
test('update workspace range', async () => {
prepareEmpty()
@@ -907,6 +939,119 @@ test('update workspace range', async () => {
dep3: 'workspace:^2.0.0',
dep4: 'workspace:^2.0.0',
dep5: 'workspace:~2.0.0',
dep6: 'workspace:2.0.0',
}
expect(updatedImporters[0].manifest.dependencies).toStrictEqual(expected)
expect(updatedImporters[1].manifest.dependencies).toStrictEqual(expected)
})
test('update workspace range when save-workspace-protocol is "rolling"', async () => {
prepareEmpty()
const updatedImporters = await mutateModules([
{
dependencySelectors: ['dep1', 'dep2', 'dep3', 'dep4', 'dep5', 'dep6'],
manifest: {
name: 'project-1',
version: '1.0.0',
dependencies: {
dep1: 'workspace:1.0.0',
dep2: 'workspace:~1.0.0',
dep3: 'workspace:^1.0.0',
dep4: 'workspace:1',
dep5: 'workspace:1.0',
dep6: 'workspace:*',
},
},
mutation: 'installSome',
rootDir: path.resolve('project-1'),
},
{
buildIndex: 0,
manifest: {
name: 'project-2',
version: '1.0.0',
dependencies: {
dep1: 'workspace:1.0.0',
dep2: 'workspace:~1.0.0',
dep3: 'workspace:^1.0.0',
dep4: 'workspace:1',
dep5: 'workspace:1.0',
dep6: 'workspace:*',
},
},
mutation: 'install',
rootDir: path.resolve('project-2'),
},
], await testDefaults({
saveWorkspaceProtocol: 'rolling',
update: true,
workspacePackages: {
dep1: {
'2.0.0': {
dir: '',
manifest: {
name: 'dep1',
version: '2.0.0',
},
},
},
dep2: {
'2.0.0': {
dir: '',
manifest: {
name: 'dep2',
version: '2.0.0',
},
},
},
dep3: {
'2.0.0': {
dir: '',
manifest: {
name: 'dep3',
version: '2.0.0',
},
},
},
dep4: {
'2.0.0': {
dir: '',
manifest: {
name: 'dep4',
version: '2.0.0',
},
},
},
dep5: {
'2.0.0': {
dir: '',
manifest: {
name: 'dep5',
version: '2.0.0',
},
},
},
dep6: {
'2.0.0': {
dir: '',
manifest: {
name: 'dep6',
version: '2.0.0',
},
},
},
},
}))
const expected = {
dep1: 'workspace:*',
dep2: 'workspace:~',
dep3: 'workspace:^',
dep4: 'workspace:^',
dep5: 'workspace:~',
dep6: 'workspace:*',
}
expect(updatedImporters[0].manifest.dependencies).toStrictEqual(expected)

View File

@@ -13,21 +13,23 @@ export function getPref (
}
) {
const prefix = getPrefix(alias, name)
return `${prefix}${createVersionSpec(version, opts.pinnedVersion)}`
return `${prefix}${createVersionSpec(version, { pinnedVersion: opts.pinnedVersion })}`
}
export function createVersionSpec (version: string | undefined, pinnedVersion?: PinnedVersion) {
if (!version) return '*'
switch (pinnedVersion ?? 'major') {
export function createVersionSpec (version: string | undefined, opts: { pinnedVersion?: PinnedVersion, rolling?: boolean }) {
switch (opts.pinnedVersion ?? 'major') {
case 'none':
return '*'
case 'major':
return `^${version}`
if (opts.rolling) return '^'
return !version ? '*' : `^${version}`
case 'minor':
return `~${version}`
if (opts.rolling) return '~'
return !version ? '*' : `~${version}`
case 'patch':
return `${version}`
if (opts.rolling) return '*'
return !version ? '*' : `${version}`
default:
throw new PnpmError('BAD_PINNED_VERSION', `Cannot pin '${pinnedVersion ?? 'undefined'}'`)
throw new PnpmError('BAD_PINNED_VERSION', `Cannot pin '${opts.pinnedVersion ?? 'undefined'}'`)
}
}

View File

@@ -63,6 +63,33 @@ test('installing with "workspace:" should work even if link-workspace-packages i
await projects['project-1'].has('project-2')
})
test('installing with "workspace:" should work even if link-workspace-packages is off and save-workspace-protocol is "rolling"', async () => {
const projects = preparePackages([
{
name: 'project-1',
version: '1.0.0',
},
{
name: 'project-2',
version: '2.0.0',
},
])
await add.handler({
...DEFAULT_OPTIONS,
dir: path.resolve('project-1'),
linkWorkspacePackages: false,
saveWorkspaceProtocol: 'rolling',
workspaceDir: process.cwd(),
}, ['project-2@workspace:*'])
const pkg = await import(path.resolve('project-1/package.json'))
expect(pkg?.dependencies).toStrictEqual({ 'project-2': 'workspace:^' })
await projects['project-1'].has('project-2')
})
test('installing with "workspace=true" should work even if link-workspace-packages is off and save-workspace-protocol is false', async () => {
const projects = preparePackages([
{

View File

@@ -535,6 +535,47 @@ test('installing with "workspace=true" should work even if link-workspace-packag
await projects['project-1'].has('project-2')
})
test('installing with "workspace=true" should work even if link-workspace-packages is off and save-workspace-protocol is "rolling"', async () => {
const projects = preparePackages([
{
name: 'project-1',
version: '1.0.0',
dependencies: {
'project-2': '0.0.0',
},
},
{
name: 'project-2',
version: '2.0.0',
},
])
await update.handler({
...DEFAULT_OPTS,
...await readProjects(process.cwd(), []),
dir: process.cwd(),
linkWorkspacePackages: false,
lockfileDir: process.cwd(),
recursive: true,
saveWorkspaceProtocol: 'rolling',
sharedWorkspaceLockfile: true,
workspace: true,
workspaceDir: process.cwd(),
}, ['project-2'])
{
const pkg = await import(path.resolve('project-1/package.json'))
expect(pkg?.dependencies).toStrictEqual({ 'project-2': 'workspace:*' })
}
{
const pkg = await import(path.resolve('project-2/package.json'))
expect(pkg.dependencies).toBeFalsy()
}
await projects['project-1'].has('project-2')
})
test('recursive install on workspace with custom lockfile-dir', async () => {
preparePackages([
{

View File

@@ -150,6 +150,54 @@ test('linking a package inside a monorepo with --link-workspace-packages when in
await projects['project-1'].has('project-4')
})
test('linking a package inside a monorepo with --link-workspace-packages when installing new dependencies and save-workspace-protocol is "rolling"', async () => {
const projects = preparePackages([
{
name: 'project-1',
version: '1.0.0',
},
{
name: 'project-2',
version: '2.0.0',
},
{
name: 'project-3',
version: '3.0.0',
},
{
name: 'project-4',
version: '4.0.0',
},
])
await fs.writeFile(
'.npmrc',
[
'link-workspace-packages = true',
'save-workspace-protocol = "rolling"',
].join('\n'),
'utf8')
await writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
process.chdir('project-1')
await execPnpm(['add', 'project-2'])
await execPnpm(['add', 'project-3', '--save-dev'])
await execPnpm(['add', 'project-4', '--save-optional', '--no-save-workspace-protocol'])
const { default: pkg } = await import(path.resolve('package.json'))
expect(pkg?.dependencies).toStrictEqual({ 'project-2': 'workspace:^' }) // spec of linked package added to dependencies
expect(pkg?.devDependencies).toStrictEqual({ 'project-3': 'workspace:^' }) // spec of linked package added to devDependencies
expect(pkg?.optionalDependencies).toStrictEqual({ 'project-4': '^4.0.0' }) // spec of linked package added to optionalDependencies
await projects['project-1'].has('project-2')
await projects['project-1'].has('project-3')
await projects['project-1'].has('project-4')
})
test('linking a package inside a monorepo with --link-workspace-packages', async () => {
const projects = preparePackages([
{

View File

@@ -81,7 +81,7 @@ export default async function (
opts: ResolveDependenciesOptions & {
defaultUpdateDepth: number
preserveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean
saveWorkspaceProtocol: 'rolling' | boolean
}
) {
const _toResolveImporter = toResolveImporter.bind(null, {

View File

@@ -15,7 +15,7 @@ export default async function updateProjectManifest (
opts: {
directDependencies: ResolvedDirectDependency[]
preserveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean | 'rolling'
}
) {
if (!importer.manifest) {
@@ -72,13 +72,20 @@ function resolvedDirectDepToSpecObject (
nodeExecPath?: string
pinnedVersion: PinnedVersion
preserveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean | 'rolling'
}
): PackageSpecObject {
let pref!: string
if (normalizedPref) {
pref = normalizedPref
} else {
const shouldUseWorkspaceProtocol = resolution.type === 'directory' &&
(
Boolean(opts.saveWorkspaceProtocol) ||
(opts.preserveWorkspaceProtocol && specRaw.includes('@workspace:'))
) &&
opts.pinnedVersion !== 'none'
if (isNew === true) {
pref = getPrefPreferSpecifiedSpec({
alias,
@@ -86,6 +93,7 @@ function resolvedDirectDepToSpecObject (
pinnedVersion: opts.pinnedVersion,
specRaw,
version,
rolling: shouldUseWorkspaceProtocol && opts.saveWorkspaceProtocol === 'rolling',
})
} else {
pref = getPrefPreferSpecifiedExoticSpec({
@@ -94,14 +102,11 @@ function resolvedDirectDepToSpecObject (
pinnedVersion: opts.pinnedVersion,
specRaw,
version,
rolling: shouldUseWorkspaceProtocol && opts.saveWorkspaceProtocol === 'rolling',
})
}
if (
resolution.type === 'directory' &&
(
opts.saveWorkspaceProtocol ||
(opts.preserveWorkspaceProtocol && specRaw.includes('@workspace:'))
) &&
shouldUseWorkspaceProtocol &&
!pref.startsWith('workspace:')
) {
pref = `workspace:${pref}`
@@ -123,6 +128,7 @@ function getPrefPreferSpecifiedSpec (
version: string
specRaw: string
pinnedVersion?: PinnedVersion
rolling: boolean
}
) {
const prefix = getPrefix(opts.alias, opts.name)
@@ -139,7 +145,7 @@ function getPrefPreferSpecifiedSpec (
if (semver.parse(opts.version)?.prerelease.length) {
return `${prefix}${opts.version}`
}
return `${prefix}${createVersionSpec(opts.version, opts.pinnedVersion)}`
return `${prefix}${createVersionSpec(opts.version, { pinnedVersion: opts.pinnedVersion, rolling: opts.rolling })}`
}
function getPrefPreferSpecifiedExoticSpec (
@@ -149,6 +155,7 @@ function getPrefPreferSpecifiedExoticSpec (
version: string
specRaw: string
pinnedVersion: PinnedVersion
rolling: boolean
}
) {
const prefix = getPrefix(opts.alias, opts.name)
@@ -159,5 +166,5 @@ function getPrefPreferSpecifiedExoticSpec (
return opts.specRaw.slice(opts.alias.length + 1)
}
}
return `${prefix}${createVersionSpec(opts.version, opts.pinnedVersion)}`
return `${prefix}${createVersionSpec(opts.version, { pinnedVersion: opts.pinnedVersion, rolling: opts.rolling })}`
}

View File

@@ -1,8 +1,9 @@
import { parseRange } from 'semver-utils'
export default function whichVersionIsPinned (spec: string) {
if (spec.startsWith('workspace:')) spec = spec.slice('workspace:'.length)
if (spec === '*') return 'none'
const isWorkspaceProtocol = spec.startsWith('workspace:')
if (isWorkspaceProtocol) spec = spec.slice('workspace:'.length)
if (spec === '*') return isWorkspaceProtocol ? 'patch' : 'none'
const parsedRange = parseRange(spec)
if (parsedRange.length !== 1) return undefined
const versionObject = parsedRange[0]