Skip to content

Commit

Permalink
fix: recursive update with workspace alias (#7999)
Browse files Browse the repository at this point in the history
close #7975
  • Loading branch information
KSXGitHub committed Apr 29, 2024
1 parent 7e69321 commit cb0f459
Show file tree
Hide file tree
Showing 18 changed files with 224 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .changeset/lucky-moons-drum.md
@@ -0,0 +1,5 @@
---
"@pnpm/workspace.spec-parser": major
---

Initial release.
7 changes: 7 additions & 0 deletions .changeset/wicked-queens-bow.md
@@ -0,0 +1,7 @@
---
"@pnpm/resolve-dependencies": patch
"@pnpm/npm-resolver": patch
"pnpm": patch
---

`pnpm update` should not fail when there's an aliased local workspace dependency [#7975](https://github.com/pnpm/pnpm/issues/7975).
31 changes: 31 additions & 0 deletions pkg-manager/plugin-commands-installation/test/update/recursive.ts
Expand Up @@ -4,6 +4,7 @@ import { type Lockfile } from '@pnpm/lockfile-types'
import { readModulesManifest } from '@pnpm/modules-yaml'
import { install, update } from '@pnpm/plugin-commands-installation'
import { preparePackages } from '@pnpm/prepare'
import { readProjectManifestOnly } from '@pnpm/read-project-manifest'
import { addDistTag } from '@pnpm/registry-mock'
import { sync as readYamlFile } from 'read-yaml-file'
import { DEFAULT_OPTS } from '../utils'
Expand Down Expand Up @@ -415,3 +416,33 @@ test('recursive update in workspace should not add new dependencies', async () =
projects['project-1'].hasNot('is-positive')
projects['project-2'].hasNot('is-positive')
})

test('recursive update with aliased workspace dependency (#7975)', async () => {
const projects = preparePackages([
{
name: 'project-1',
version: '1.0.0',
dependencies: {
pkg: 'workspace:project-2@^',
},
},
{
name: 'project-2',
version: '1.0.0',
},
])

await update.handler({
...DEFAULT_OPTS,
...await readProjects(process.cwd(), []),
depth: 0,
dir: process.cwd(),
recursive: true,
workspaceDir: process.cwd(),
})

projects['project-1'].has('pkg')

const manifest = await readProjectManifestOnly('project-1')
expect(manifest).toHaveProperty(['dependencies', 'pkg'], 'workspace:project-2@^')
})
1 change: 1 addition & 0 deletions pkg-manager/resolve-dependencies/package.json
Expand Up @@ -46,6 +46,7 @@
"@pnpm/store-controller-types": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/which-version-is-pinned": "workspace:*",
"@pnpm/workspace.spec-parser": "workspace:*",
"@yarnpkg/core": "4.0.3",
"filenamify": "^4.3.0",
"get-npm-tarball-url": "^2.1.0",
Expand Down
Expand Up @@ -6,6 +6,7 @@ import {
type ProjectManifest,
} from '@pnpm/types'
import { whichVersionIsPinned } from '@pnpm/which-version-is-pinned'
import { WorkspaceSpec } from '@pnpm/workspace.spec-parser'

export type PinnedVersion = 'major' | 'minor' | 'patch' | 'none'

Expand Down Expand Up @@ -54,7 +55,10 @@ export function getWantedDependencies (
}

function updateWorkspacePref (pref: string): string {
return pref.startsWith('workspace:') ? 'workspace:*' : pref
const spec = WorkspaceSpec.parse(pref)
if (!spec) return pref
spec.version = '*'
return spec.toString()
}

function getWantedDependenciesFromGivenSet (
Expand Down
Expand Up @@ -123,6 +123,7 @@ function resolvedDirectDepToSpecObject (
shouldUseWorkspaceProtocol &&
!pref.startsWith('workspace:')
) {
pref = pref.replace(/^npm:/, '')
pref = `workspace:${pref}`
}
}
Expand Down
3 changes: 3 additions & 0 deletions pkg-manager/resolve-dependencies/tsconfig.json
Expand Up @@ -59,6 +59,9 @@
},
{
"path": "../../store/store-controller-types"
},
{
"path": "../../workspace/spec-parser"
}
],
"composite": true
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions resolving/npm-resolver/package.json
Expand Up @@ -41,6 +41,7 @@
"@pnpm/resolve-workspace-range": "workspace:*",
"@pnpm/resolver-base": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/workspace.spec-parser": "workspace:*",
"@zkochan/retry": "^0.2.0",
"encode-registry": "^3.0.1",
"load-json-file": "^6.2.0",
Expand Down
21 changes: 9 additions & 12 deletions resolving/npm-resolver/src/workspacePrefToNpm.ts
@@ -1,17 +1,14 @@
export function workspacePrefToNpm (workspacePref: string): string {
const prefParts = /^workspace:([^._/][^@]*@)?(.*)$/.exec(workspacePref)
import { WorkspaceSpec } from '@pnpm/workspace.spec-parser'

if (prefParts == null) {
export function workspacePrefToNpm (workspacePref: string): string {
const parseResult = WorkspaceSpec.parse(workspacePref)
if (parseResult == 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}`
const { alias, version } = parseResult
const versionPart = version === '^' || version === '~' ? '*' : version
return alias
? `npm:${alias}@${versionPart}`
: versionPart
}
3 changes: 3 additions & 0 deletions resolving/npm-resolver/tsconfig.json
Expand Up @@ -33,6 +33,9 @@
{
"path": "../../workspace/resolve-workspace-range"
},
{
"path": "../../workspace/spec-parser"
},
{
"path": "../resolver-base"
}
Expand Down
15 changes: 15 additions & 0 deletions workspace/spec-parser/README.md
@@ -0,0 +1,15 @@
# @pnpm/workspace.spec-parser

> Parse and stringify workspace specs
[![npm version](https://img.shields.io/npm/v/@pnpm/workspace.spec-parser.svg)](https://www.npmjs.com/package/@pnpm/workspace.spec-parser)

## Installation

```sh
pnpm add @pnpm/workspace.spec-parser
```

## License

MIT
3 changes: 3 additions & 0 deletions workspace/spec-parser/jest.config.js
@@ -0,0 +1,3 @@
const config = require('../../jest.config.js');

module.exports = config
38 changes: 38 additions & 0 deletions workspace/spec-parser/package.json
@@ -0,0 +1,38 @@
{
"name": "@pnpm/workspace.spec-parser",
"version": "0.0.0",
"description": "Parse and stringify workspace pref",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"engines": {
"node": ">=18.12"
},
"scripts": {
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"repository": "https://github.com/pnpm/pnpm/blob/main/workspace/spec-parser",
"keywords": [
"pnpm9",
"pnpm"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/workspace/spec-parser#readme",
"funding": "https://opencollective.com/pnpm",
"devDependencies": {
"@pnpm/workspace.spec-parser": "workspace:*"
},
"exports": {
".": "./lib/index.js"
}
}
22 changes: 22 additions & 0 deletions workspace/spec-parser/src/index.ts
@@ -0,0 +1,22 @@
const WORKSPACE_PREF_REGEX = /^workspace:((?<alias>[^._/][^@]*)@)?(?<version>.*)$/

export class WorkspaceSpec {
alias?: string
version: string

constructor (version: string, alias?: string) {
this.version = version
this.alias = alias
}

static parse (pref: string): WorkspaceSpec | null {
const parts = WORKSPACE_PREF_REGEX.exec(pref)
if (!parts?.groups) return null
return new WorkspaceSpec(parts.groups.version, parts.groups.alias)
}

toString (): `workspace:${string}` {
const { alias, version } = this
return alias ? `workspace:${alias}@${version}` : `workspace:${version}`
}
}
47 changes: 47 additions & 0 deletions workspace/spec-parser/test/workspace-spec.test.ts
@@ -0,0 +1,47 @@
import { WorkspaceSpec } from '../src/index'

test('parse valid workspace spec', () => {
expect(WorkspaceSpec.parse('workspace:*')).toStrictEqual(new WorkspaceSpec('*'))
expect(WorkspaceSpec.parse('workspace:^')).toStrictEqual(new WorkspaceSpec('^'))
expect(WorkspaceSpec.parse('workspace:~')).toStrictEqual(new WorkspaceSpec('~'))
expect(WorkspaceSpec.parse('workspace:0.1.2')).toStrictEqual(new WorkspaceSpec('0.1.2'))
expect(WorkspaceSpec.parse('workspace:foo@*')).toStrictEqual(new WorkspaceSpec('*', 'foo'))
expect(WorkspaceSpec.parse('workspace:foo@^')).toStrictEqual(new WorkspaceSpec('^', 'foo'))
expect(WorkspaceSpec.parse('workspace:foo@~')).toStrictEqual(new WorkspaceSpec('~', 'foo'))
expect(WorkspaceSpec.parse('workspace:[email protected]')).toStrictEqual(new WorkspaceSpec('0.1.2', 'foo'))
expect(WorkspaceSpec.parse('workspace:@foo/bar@*')).toStrictEqual(new WorkspaceSpec('*', '@foo/bar'))
expect(WorkspaceSpec.parse('workspace:@foo/bar@^')).toStrictEqual(new WorkspaceSpec('^', '@foo/bar'))
expect(WorkspaceSpec.parse('workspace:@foo/bar@~')).toStrictEqual(new WorkspaceSpec('~', '@foo/bar'))
expect(WorkspaceSpec.parse('workspace:@foo/[email protected]')).toStrictEqual(new WorkspaceSpec('0.1.2', '@foo/bar'))
})

test('parse invalid workspace spec', () => {
expect(WorkspaceSpec.parse('npm:[email protected]')).toBe(null)
expect(WorkspaceSpec.parse('*')).toBe(null)
})

test('to string', () => {
expect(new WorkspaceSpec('*').toString()).toBe('workspace:*')
expect(new WorkspaceSpec('^').toString()).toBe('workspace:^')
expect(new WorkspaceSpec('~').toString()).toBe('workspace:~')
expect(new WorkspaceSpec('0.1.2').toString()).toBe('workspace:0.1.2')
expect(new WorkspaceSpec('*', 'foo').toString()).toBe('workspace:foo@*')
expect(new WorkspaceSpec('^', 'foo').toString()).toBe('workspace:foo@^')
expect(new WorkspaceSpec('~', 'foo').toString()).toBe('workspace:foo@~')
expect(new WorkspaceSpec('0.1.2', 'foo').toString()).toBe('workspace:[email protected]')
expect(new WorkspaceSpec('*', '@foo/bar').toString()).toBe('workspace:@foo/bar@*')
expect(new WorkspaceSpec('^', '@foo/bar').toString()).toBe('workspace:@foo/bar@^')
expect(new WorkspaceSpec('~', '@foo/bar').toString()).toBe('workspace:@foo/bar@~')
expect(new WorkspaceSpec('0.1.2', '@foo/bar').toString()).toBe('workspace:@foo/[email protected]')
})

test('mutate alias and version', () => {
const spec = WorkspaceSpec.parse('workspace:*')!
expect(spec.toString()).toBe('workspace:*')
spec.version = '^'
expect(spec.toString()).toBe('workspace:^')
spec.alias = 'foo'
expect(spec.toString()).toBe('workspace:foo@^')
delete spec.alias
expect(spec.toString()).toBe('workspace:^')
})
13 changes: 13 additions & 0 deletions workspace/spec-parser/tsconfig.json
@@ -0,0 +1,13 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [],
"composite": true
}
8 changes: 8 additions & 0 deletions workspace/spec-parser/tsconfig.lint.json
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

0 comments on commit cb0f459

Please sign in to comment.