From cb0f459c544bebc14e1a62fd040fd6cec8e7ca92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kh=E1=BA=A3i?= Date: Tue, 30 Apr 2024 06:17:12 +0700 Subject: [PATCH] fix: recursive update with workspace alias (#7999) close #7975 --- .changeset/lucky-moons-drum.md | 5 ++ .changeset/wicked-queens-bow.md | 7 +++ .../test/update/recursive.ts | 31 ++++++++++++ pkg-manager/resolve-dependencies/package.json | 1 + .../src/getWantedDependencies.ts | 6 ++- .../src/updateProjectManifest.ts | 1 + .../resolve-dependencies/tsconfig.json | 3 ++ pnpm-lock.yaml | 12 +++++ resolving/npm-resolver/package.json | 1 + .../npm-resolver/src/workspacePrefToNpm.ts | 21 ++++----- resolving/npm-resolver/tsconfig.json | 3 ++ workspace/spec-parser/README.md | 15 ++++++ workspace/spec-parser/jest.config.js | 3 ++ workspace/spec-parser/package.json | 38 +++++++++++++++ workspace/spec-parser/src/index.ts | 22 +++++++++ .../spec-parser/test/workspace-spec.test.ts | 47 +++++++++++++++++++ workspace/spec-parser/tsconfig.json | 13 +++++ workspace/spec-parser/tsconfig.lint.json | 8 ++++ 18 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 .changeset/lucky-moons-drum.md create mode 100644 .changeset/wicked-queens-bow.md create mode 100644 workspace/spec-parser/README.md create mode 100644 workspace/spec-parser/jest.config.js create mode 100644 workspace/spec-parser/package.json create mode 100644 workspace/spec-parser/src/index.ts create mode 100644 workspace/spec-parser/test/workspace-spec.test.ts create mode 100644 workspace/spec-parser/tsconfig.json create mode 100644 workspace/spec-parser/tsconfig.lint.json diff --git a/.changeset/lucky-moons-drum.md b/.changeset/lucky-moons-drum.md new file mode 100644 index 0000000000..0bf16866fc --- /dev/null +++ b/.changeset/lucky-moons-drum.md @@ -0,0 +1,5 @@ +--- +"@pnpm/workspace.spec-parser": major +--- + +Initial release. diff --git a/.changeset/wicked-queens-bow.md b/.changeset/wicked-queens-bow.md new file mode 100644 index 0000000000..9e62d7c397 --- /dev/null +++ b/.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). diff --git a/pkg-manager/plugin-commands-installation/test/update/recursive.ts b/pkg-manager/plugin-commands-installation/test/update/recursive.ts index 6a014b4b0b..7a0a09b41a 100644 --- a/pkg-manager/plugin-commands-installation/test/update/recursive.ts +++ b/pkg-manager/plugin-commands-installation/test/update/recursive.ts @@ -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' @@ -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@^') +}) diff --git a/pkg-manager/resolve-dependencies/package.json b/pkg-manager/resolve-dependencies/package.json index 45a885f83a..419e8eeca7 100644 --- a/pkg-manager/resolve-dependencies/package.json +++ b/pkg-manager/resolve-dependencies/package.json @@ -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", diff --git a/pkg-manager/resolve-dependencies/src/getWantedDependencies.ts b/pkg-manager/resolve-dependencies/src/getWantedDependencies.ts index 6ccbe9c2cc..bf182f52d3 100644 --- a/pkg-manager/resolve-dependencies/src/getWantedDependencies.ts +++ b/pkg-manager/resolve-dependencies/src/getWantedDependencies.ts @@ -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' @@ -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 ( diff --git a/pkg-manager/resolve-dependencies/src/updateProjectManifest.ts b/pkg-manager/resolve-dependencies/src/updateProjectManifest.ts index 96e2d90049..3a4efc7de0 100644 --- a/pkg-manager/resolve-dependencies/src/updateProjectManifest.ts +++ b/pkg-manager/resolve-dependencies/src/updateProjectManifest.ts @@ -123,6 +123,7 @@ function resolvedDirectDepToSpecObject ( shouldUseWorkspaceProtocol && !pref.startsWith('workspace:') ) { + pref = pref.replace(/^npm:/, '') pref = `workspace:${pref}` } } diff --git a/pkg-manager/resolve-dependencies/tsconfig.json b/pkg-manager/resolve-dependencies/tsconfig.json index 318173ae17..9f507a0c32 100644 --- a/pkg-manager/resolve-dependencies/tsconfig.json +++ b/pkg-manager/resolve-dependencies/tsconfig.json @@ -59,6 +59,9 @@ }, { "path": "../../store/store-controller-types" + }, + { + "path": "../../workspace/spec-parser" } ], "composite": true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f09d8bce5..7e8d47721c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4289,6 +4289,9 @@ importers: '@pnpm/which-version-is-pinned': specifier: workspace:* version: link:../../packages/which-version-is-pinned + '@pnpm/workspace.spec-parser': + specifier: workspace:* + version: link:../../workspace/spec-parser '@yarnpkg/core': specifier: 4.0.3 version: 4.0.3(typanion@3.14.0) @@ -5191,6 +5194,9 @@ importers: '@pnpm/types': specifier: workspace:* version: link:../../packages/types + '@pnpm/workspace.spec-parser': + specifier: workspace:* + version: link:../../workspace/spec-parser '@zkochan/retry': specifier: ^0.2.0 version: 0.2.0 @@ -6556,6 +6562,12 @@ importers: specifier: workspace:* version: 'link:' + workspace/spec-parser: + devDependencies: + '@pnpm/workspace.spec-parser': + specifier: workspace:* + version: 'link:' + packages: '@aashutoshrathi/word-wrap@1.2.6': diff --git a/resolving/npm-resolver/package.json b/resolving/npm-resolver/package.json index c8b9169d49..514a90489e 100644 --- a/resolving/npm-resolver/package.json +++ b/resolving/npm-resolver/package.json @@ -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", diff --git a/resolving/npm-resolver/src/workspacePrefToNpm.ts b/resolving/npm-resolver/src/workspacePrefToNpm.ts index fb1d4e3e63..29b5e9beec 100644 --- a/resolving/npm-resolver/src/workspacePrefToNpm.ts +++ b/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 } diff --git a/resolving/npm-resolver/tsconfig.json b/resolving/npm-resolver/tsconfig.json index 2b49edaef6..b1725d10a6 100644 --- a/resolving/npm-resolver/tsconfig.json +++ b/resolving/npm-resolver/tsconfig.json @@ -33,6 +33,9 @@ { "path": "../../workspace/resolve-workspace-range" }, + { + "path": "../../workspace/spec-parser" + }, { "path": "../resolver-base" } diff --git a/workspace/spec-parser/README.md b/workspace/spec-parser/README.md new file mode 100644 index 0000000000..3d9149c3a4 --- /dev/null +++ b/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 diff --git a/workspace/spec-parser/jest.config.js b/workspace/spec-parser/jest.config.js new file mode 100644 index 0000000000..56786ef060 --- /dev/null +++ b/workspace/spec-parser/jest.config.js @@ -0,0 +1,3 @@ +const config = require('../../jest.config.js'); + +module.exports = config diff --git a/workspace/spec-parser/package.json b/workspace/spec-parser/package.json new file mode 100644 index 0000000000..5172c03004 --- /dev/null +++ b/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" + } +} diff --git a/workspace/spec-parser/src/index.ts b/workspace/spec-parser/src/index.ts new file mode 100644 index 0000000000..26fd94a033 --- /dev/null +++ b/workspace/spec-parser/src/index.ts @@ -0,0 +1,22 @@ +const WORKSPACE_PREF_REGEX = /^workspace:((?[^._/][^@]*)@)?(?.*)$/ + +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}` + } +} diff --git a/workspace/spec-parser/test/workspace-spec.test.ts b/workspace/spec-parser/test/workspace-spec.test.ts new file mode 100644 index 0000000000..8352419032 --- /dev/null +++ b/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:foo@0.1.2')).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/bar@0.1.2')).toStrictEqual(new WorkspaceSpec('0.1.2', '@foo/bar')) +}) + +test('parse invalid workspace spec', () => { + expect(WorkspaceSpec.parse('npm:foo@0.1.2')).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:foo@0.1.2') + 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/bar@0.1.2') +}) + +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:^') +}) diff --git a/workspace/spec-parser/tsconfig.json b/workspace/spec-parser/tsconfig.json new file mode 100644 index 0000000000..0c16b31af5 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/workspace/spec-parser/tsconfig.lint.json b/workspace/spec-parser/tsconfig.lint.json new file mode 100644 index 0000000000..1bbe711971 --- /dev/null +++ b/workspace/spec-parser/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*.ts", + "test/**/*.ts", + "../../__typings__/**/*.d.ts" + ] +}