diff --git a/.changeset/flat-socks-dance.md b/.changeset/flat-socks-dance.md new file mode 100644 index 0000000000..4117ca20a1 --- /dev/null +++ b/.changeset/flat-socks-dance.md @@ -0,0 +1,5 @@ +--- +"@pnpm/local-resolver": minor +--- + +Support relative path to workspace directory. diff --git a/.changeset/mighty-kangaroos-brake.md b/.changeset/mighty-kangaroos-brake.md new file mode 100644 index 0000000000..d1f163f122 --- /dev/null +++ b/.changeset/mighty-kangaroos-brake.md @@ -0,0 +1,5 @@ +--- +"@pnpm/exportable-manifest": minor +--- + +Convert relative workspace paths to version specs. diff --git a/.changeset/quiet-papayas-tie.md b/.changeset/quiet-papayas-tie.md new file mode 100644 index 0000000000..9a31a147b6 --- /dev/null +++ b/.changeset/quiet-papayas-tie.md @@ -0,0 +1,5 @@ +--- +"@pnpm/npm-resolver": minor +--- + +Skip workspace protocol specs that use relative path. diff --git a/packages/exportable-manifest/src/index.ts b/packages/exportable-manifest/src/index.ts index 00eaaa2600..ec6a40470d 100644 --- a/packages/exportable-manifest/src/index.ts +++ b/packages/exportable-manifest/src/index.ts @@ -76,6 +76,18 @@ async function makePublishDependency (depName: string, depSpec: string, dir: str } return manifest.version } + if (depSpec.startsWith('workspace:./') || depSpec.startsWith('workspace:../')) { + const { manifest } = await tryReadProjectManifest(path.join(dir, depSpec.substr(10))) + if (!manifest || !manifest.name || !manifest.version) { + throw new PnpmError( + 'CANNOT_RESOLVE_WORKSPACE_PROTOCOL', + `Cannot resolve workspace protocol of dependency "${depName}" ` + + 'because this dependency is not installed. Try running "pnpm install".' + ) + } + if (manifest.name === depName) return `${manifest.version}` + return `npm:${manifest.name}@${manifest.version}` + } depSpec = depSpec.substr(10) if (depSpec.includes('@')) { return `npm:${depSpec}` diff --git a/packages/local-resolver/src/parsePref.ts b/packages/local-resolver/src/parsePref.ts index 5035754604..0b711e004b 100644 --- a/packages/local-resolver/src/parsePref.ts +++ b/packages/local-resolver/src/parsePref.ts @@ -22,7 +22,7 @@ export default function parsePref ( projectDir: string, lockfileDir: string ): LocalPackageSpec | null { - if (pref.startsWith('link:')) { + if (pref.startsWith('link:') || pref.startsWith('workspace:')) { return fromLocal(pref, projectDir, lockfileDir, 'directory') } if (pref.endsWith('.tgz') || @@ -54,8 +54,8 @@ function fromLocal ( type: 'file' | 'directory' ): LocalPackageSpec { const spec = pref.replace(/\\/g, '/') - .replace(/^(file|link):[/]*([A-Za-z]:)/, '$2') // drive name paths on windows - .replace(/^(file|link):(?:[/]*([~./]))?/, '$2') + .replace(/^(file|link|workspace):[/]*([A-Za-z]:)/, '$2') // drive name paths on windows + .replace(/^(file|link|workspace):(?:[/]*([~./]))?/, '$2') const protocol = type === 'directory' ? 'link:' : 'file:' let fetchSpec!: string diff --git a/packages/local-resolver/test/index.ts b/packages/local-resolver/test/index.ts index ecb2ead8de..5344c698c9 100644 --- a/packages/local-resolver/test/index.ts +++ b/packages/local-resolver/test/index.ts @@ -11,6 +11,15 @@ test('resolve directory', async () => { expect(resolveResult!.resolution['type']).toEqual('directory') }) +test('resolve workspace directory', async () => { + const resolveResult = await resolveFromLocal({ pref: 'workspace:..' }, { projectDir: __dirname }) + expect(resolveResult!.id).toEqual('link:..') + expect(resolveResult!.normalizedPref).toEqual('link:..') + expect(resolveResult!['manifest']!.name).toEqual('@pnpm/local-resolver') + expect(resolveResult!.resolution['directory']).toEqual('..') + expect(resolveResult!.resolution['type']).toEqual('directory') +}) + test('resolve directory specified using the file: protocol', async () => { const resolveResult = await resolveFromLocal({ pref: 'file:..' }, { projectDir: __dirname }) expect(resolveResult!.id).toEqual('link:..') diff --git a/packages/npm-resolver/src/index.ts b/packages/npm-resolver/src/index.ts index f4c9d037d6..5169df3920 100644 --- a/packages/npm-resolver/src/index.ts +++ b/packages/npm-resolver/src/index.ts @@ -114,14 +114,17 @@ async function resolveNpm ( opts: ResolveFromNpmOptions ): Promise { const defaultTag = opts.defaultTag ?? 'latest' - const resolvedFromWorkspace = tryResolveFromWorkspace(wantedDependency, { - defaultTag, - projectDir: opts.projectDir, - registry: opts.registry, - workspacePackages: opts.workspacePackages, - }) - if (resolvedFromWorkspace) { - return resolvedFromWorkspace + if (wantedDependency.pref?.startsWith('workspace:')) { + if (wantedDependency.pref.startsWith('workspace:.')) return null + const resolvedFromWorkspace = tryResolveFromWorkspace(wantedDependency, { + defaultTag, + projectDir: opts.projectDir, + registry: opts.registry, + workspacePackages: opts.workspacePackages, + }) + if (resolvedFromWorkspace) { + return resolvedFromWorkspace + } } const workspacePackages = opts.alwaysTryWorkspacePackages !== false ? opts.workspacePackages : undefined const spec = wantedDependency.pref diff --git a/packages/npm-resolver/test/index.ts b/packages/npm-resolver/test/index.ts index 01e7750b96..fa75ece18a 100644 --- a/packages/npm-resolver/test/index.ts +++ b/packages/npm-resolver/test/index.ts @@ -73,6 +73,19 @@ test('resolveFromNpm()', async () => { expect(meta['dist-tags']).toBeTruthy() }) +test('relative workspace protocol is skipped', async () => { + const storeDir = tempy.directory() + const resolve = createResolveFromNpm({ + storeDir, + }) + const resolveResult = await resolve({ pref: 'workspace:../is-positive' }, { + projectDir: '/home/istvan/src', + registry, + }) + + expect(resolveResult).toBe(null) +}) + test('dry run', async (done) => { nock(registry) .get('/is-positive') diff --git a/packages/plugin-commands-publishing/test/publish.ts b/packages/plugin-commands-publishing/test/publish.ts index 34d8fdbcf2..6244ae4e44 100644 --- a/packages/plugin-commands-publishing/test/publish.ts +++ b/packages/plugin-commands-publishing/test/publish.ts @@ -444,6 +444,92 @@ because this dependency is not installed. Try running "pnpm install".' }) }) +test('convert specs with relative workspace protocols to regular version ranges', async () => { + preparePackages(undefined, [ + { + name: 'relative-workspace-protocol-package', + version: '1.0.0', + + dependencies: { + 'file-type': 'workspace:../file-type', + 'is-neg': 'workspace:../is-negative', + 'is-positive': '1.0.0', + 'lodash.delay': '~4.1.0', + }, + devDependencies: { + 'random-package': 'workspace:../random-package', + }, + optionalDependencies: { + 'lodash.deburr': 'workspace:../lodash.deburr', + }, + peerDependencies: { + 'random-package': 'workspace:../random-package', + }, + }, + { + name: 'is-negative', + version: '1.0.0', + }, + { + name: 'file-type', + version: '12.0.1', + }, + { + name: 'lodash.deburr', + version: '4.1.0', + }, + { + name: 'lodash.delay', + version: '4.1.0', + }, + { + name: 'random-package', + version: '1.2.3', + }, + { + name: 'target', + version: '1.0.0', + }, + ]) + + await writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] }) + + process.chdir('relative-workspace-protocol-package') + + await publish.handler({ + ...DEFAULT_OPTS, + argv: { original: ['publish', ...CREDENTIALS] }, + dir: process.cwd(), + }, []) + + process.chdir('../target') + + crossSpawn.sync(pnpmBin, [ + 'add', + '--store-dir=../store', + 'relative-workspace-protocol-package', + '--no-link-workspace-packages', + `--registry=http://localhost:${REGISTRY_MOCK_PORT}`, + ]) + + const { default: publishedManifest } = await import(path.resolve('node_modules/relative-workspace-protocol-package/package.json')) + expect(publishedManifest.dependencies).toStrictEqual({ + 'file-type': '12.0.1', + 'is-neg': 'npm:is-negative@1.0.0', + 'is-positive': '1.0.0', + 'lodash.delay': '~4.1.0', + }) + expect(publishedManifest.devDependencies).toStrictEqual({ + 'random-package': '1.2.3', + }) + expect(publishedManifest.optionalDependencies).toStrictEqual({ + 'lodash.deburr': '4.1.0', + }) + expect(publishedManifest.peerDependencies).toStrictEqual({ + 'random-package': '1.2.3', + }) +}) + test('publish: runs all the lifecycle scripts', async () => { prepare(undefined, { name: 'test-publish-with-scripts',