mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-28 01:28:13 -05:00
feat: use workspace spec alias by default in pnpm add (#4947)
This commit is contained in:
committed by
GitHub
parent
e5610a579b
commit
f5621a42c2
12
.changeset/perfect-cherries-worry.md
Normal file
12
.changeset/perfect-cherries-worry.md
Normal 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
|
||||
```
|
||||
@@ -34,7 +34,7 @@ export interface Config {
|
||||
saveDev?: boolean
|
||||
saveOptional?: boolean
|
||||
savePeer?: boolean
|
||||
saveWorkspaceProtocol?: boolean
|
||||
saveWorkspaceProtocol?: boolean | 'rolling'
|
||||
scriptShell?: string
|
||||
stream?: boolean
|
||||
pnpmExecPath: string
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'}'`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -81,7 +81,7 @@ export default async function (
|
||||
opts: ResolveDependenciesOptions & {
|
||||
defaultUpdateDepth: number
|
||||
preserveWorkspaceProtocol: boolean
|
||||
saveWorkspaceProtocol: boolean
|
||||
saveWorkspaceProtocol: 'rolling' | boolean
|
||||
}
|
||||
) {
|
||||
const _toResolveImporter = toResolveImporter.bind(null, {
|
||||
|
||||
@@ -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 })}`
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user