mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-29 04:21:39 -04:00
feat: catalogs (#8122)
* feat: create new @pnpm/catalogs.types package (#8026) * feat: read catalog configs from workspace manifest (#8123) * refactor: move InvalidWorkspaceManifestError to its own file * feat: read catalogs config from workspace manifest * chore: add changeset for new catalog config parsing * feat: create new `@pnpm/catalogs.protocol-parser` package (#8124) This works around a problem with pnpm's CI setup not compiling packages that are not dependencies of the main pnpm package before running these tests. https://github.com/pnpm/pnpm/pull/8027#issuecomment-2081650304 * refactor: factor out isWantedDepPrefSame to extend in a future commit (#8125) * feat: create new `@pnpm/catalogs.config` package (#8220) * refactor: remove single default catalog check This check will happen in `@pnpm/catalogs.config` instead. * feat: create new @pnpm/catalogs.config package * fix: work around CI setup not compiling orphan packages before testing This works around a problem with pnpm's CI setup not compiling packages that are not dependencies of the main pnpm package before running these tests. https://github.com/pnpm/pnpm/pull/8027#issuecomment-2081650304 * feat: create new `@pnpm/catalogs.resolver` package (#8219) * feat: create new @pnpm/catalogs.resolver package * fix: work around CI setup not compiling orphan packages before testing This works around a problem with pnpm's CI setup not compiling packages that are not dependencies of the main pnpm package before running these tests. https://github.com/pnpm/pnpm/pull/8027#issuecomment-2081650304 * feat: implement catalog protocol for publish (#8225) * feat: implement catalog protocol for install (#8221) * feat: add catalogs to @pnpm/config * refactor: factor out resolveDependenciesOfImporterDependency function * feat: implement catalog resolver and replace prefs * revert: work around CI setup not compiling orphan packages before testing * feat: record catalog lookup snapshots through propagated metadata * feat: update projects when catalogs config changes * test: add catalog protocol install tests * refactor: remove filter-packages-from-dir dependency from core tests (#8244) * refactor: remove filter-packages-from-dir dependency from core tests * test: refactor * test: refactor --------- Co-authored-by: Zoltan Kochan <z@kochan.io> --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
5
.changeset/chilly-eels-study.md
Normal file
5
.changeset/chilly-eels-study.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/exportable-manifest": major
|
||||
---
|
||||
|
||||
Creating an exportable manifest now requires a `Catalog` object to be passed to `createExportableManifest` in order to replace `catalog:` protocol specifiers.
|
||||
5
.changeset/unlucky-chefs-learn.md
Normal file
5
.changeset/unlucky-chefs-learn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/workspace.read-manifest": minor
|
||||
---
|
||||
|
||||
The `readWorkspaceManifest` function now parses and validates [pnpm catalogs](https://github.com/pnpm/rfcs/pull/1) configs if present.
|
||||
5
catalogs/config/CHANGELOG.md
Normal file
5
catalogs/config/CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# @pnpm/catalogs.config
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Initial release
|
||||
3
catalogs/config/README.md
Normal file
3
catalogs/config/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @pnpm/catalogs.config
|
||||
|
||||
> Create a normalized catalogs config from `pnpm-workspace.yaml` contents.
|
||||
1
catalogs/config/jest.config.js
Normal file
1
catalogs/config/jest.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../jest.config.js')
|
||||
44
catalogs/config/package.json
Normal file
44
catalogs/config/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@pnpm/catalogs.config",
|
||||
"version": "0.1.0",
|
||||
"description": "Create a normalized catalogs config from pnpm-workspace.yaml contents.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
],
|
||||
"repository": "https://github.com/pnpm/pnpm/blob/main/catalogs/config",
|
||||
"keywords": [
|
||||
"pnpm9",
|
||||
"pnpm",
|
||||
"types"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/catalogs/config#readme",
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"compile": "tsc --build && pnpm run lint --fix",
|
||||
"prepublishOnly": "pnpm run compile",
|
||||
"test": "pnpm run compile && pnpm run _test",
|
||||
"_test": "jest"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"exports": {
|
||||
".": "./lib/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/error": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/catalogs.config": "workspace:*",
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/workspace.read-manifest": "workspace:*"
|
||||
}
|
||||
}
|
||||
36
catalogs/config/src/getCatalogsFromWorkspaceManifest.ts
Normal file
36
catalogs/config/src/getCatalogsFromWorkspaceManifest.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type WorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
|
||||
export function getCatalogsFromWorkspaceManifest (
|
||||
workspaceManifest: Pick<WorkspaceManifest, 'catalog' | 'catalogs'> | undefined
|
||||
): Catalogs {
|
||||
// If the pnpm-workspace.yaml file doesn't exist, no catalogs are defined.
|
||||
//
|
||||
// In some cases, it makes sense for callers to handle null/undefined checks
|
||||
// of this form. In this case, let's explicitly handle not found
|
||||
// pnpm-workspace.yaml files by returning an empty catalog to make consuming
|
||||
// logic easier.
|
||||
if (workspaceManifest == null) {
|
||||
return {}
|
||||
}
|
||||
|
||||
checkDefaultCatalogIsDefinedOnce(workspaceManifest)
|
||||
|
||||
return {
|
||||
// If workspaceManifest.catalog is undefined, intentionally allow the spread
|
||||
// below to overwrite it. The check above ensures only one or the either is
|
||||
// defined.
|
||||
default: workspaceManifest.catalog,
|
||||
|
||||
...workspaceManifest.catalogs,
|
||||
}
|
||||
}
|
||||
|
||||
export function checkDefaultCatalogIsDefinedOnce (manifest: Pick<WorkspaceManifest, 'catalog' | 'catalogs'>): void {
|
||||
if (manifest.catalog != null && manifest.catalogs?.default != null) {
|
||||
throw new PnpmError(
|
||||
'INVALID_CATALOGS_CONFIGURATION',
|
||||
'The \'default\' catalog was defined multiple times. Use the \'catalog\' field or \'catalogs.default\', but not both.')
|
||||
}
|
||||
}
|
||||
1
catalogs/config/src/index.ts
Normal file
1
catalogs/config/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { getCatalogsFromWorkspaceManifest } from './getCatalogsFromWorkspaceManifest'
|
||||
@@ -0,0 +1,54 @@
|
||||
import { getCatalogsFromWorkspaceManifest } from '@pnpm/catalogs.config'
|
||||
|
||||
test('combines implicit default and named catalogs', () => {
|
||||
expect(getCatalogsFromWorkspaceManifest({
|
||||
catalog: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
catalogs: {
|
||||
bar: {
|
||||
baz: '^2.0.0',
|
||||
},
|
||||
},
|
||||
})).toEqual({
|
||||
default: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
bar: {
|
||||
baz: '^2.0.0',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('combines explicit default and named catalogs', () => {
|
||||
expect(getCatalogsFromWorkspaceManifest({
|
||||
catalogs: {
|
||||
default: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
bar: {
|
||||
baz: '^2.0.0',
|
||||
},
|
||||
},
|
||||
})).toEqual({
|
||||
default: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
bar: {
|
||||
baz: '^2.0.0',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('throws if default catalog is defined multiple times', () => {
|
||||
expect(() => getCatalogsFromWorkspaceManifest({
|
||||
catalog: {
|
||||
bar: '^2.0.0',
|
||||
},
|
||||
catalogs: {
|
||||
default: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
},
|
||||
})).toThrow(/The 'default' catalog was defined multiple times/)
|
||||
})
|
||||
17
catalogs/config/test/tsconfig.json
Normal file
17
catalogs/config/test/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"noEmit": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
23
catalogs/config/tsconfig.json
Normal file
23
catalogs/config/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"composite": true,
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/read-manifest"
|
||||
},
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
catalogs/config/tsconfig.lint.json
Normal file
8
catalogs/config/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
5
catalogs/protocol-parser/CHANGELOG.md
Normal file
5
catalogs/protocol-parser/CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# @pnpm/catalogs.protocol-parser
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Initial release
|
||||
3
catalogs/protocol-parser/README.md
Normal file
3
catalogs/protocol-parser/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @pnpm/catalogs.protocol-parser
|
||||
|
||||
> Parse catalog protocol specifiers and return the catalog name.
|
||||
1
catalogs/protocol-parser/jest.config.js
Normal file
1
catalogs/protocol-parser/jest.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../jest.config.js')
|
||||
39
catalogs/protocol-parser/package.json
Normal file
39
catalogs/protocol-parser/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@pnpm/catalogs.protocol-parser",
|
||||
"version": "0.1.0",
|
||||
"description": "Parse catalog protocol specifiers and return the catalog name.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
],
|
||||
"repository": "https://github.com/pnpm/pnpm/blob/main/catalogs/protocol-parser",
|
||||
"keywords": [
|
||||
"pnpm9",
|
||||
"pnpm",
|
||||
"types"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/catalogs/protocol-parser#readme",
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"compile": "tsc --build && pnpm run lint --fix",
|
||||
"prepublishOnly": "pnpm run compile",
|
||||
"test": "pnpm run compile && pnpm run _test",
|
||||
"_test": "jest"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"exports": {
|
||||
".": "./lib/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/catalogs.protocol-parser": "workspace:*"
|
||||
}
|
||||
}
|
||||
1
catalogs/protocol-parser/src/index.ts
Normal file
1
catalogs/protocol-parser/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { parseCatalogProtocol } from './parseCatalogProtocol'
|
||||
18
catalogs/protocol-parser/src/parseCatalogProtocol.ts
Normal file
18
catalogs/protocol-parser/src/parseCatalogProtocol.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
const CATALOG_PROTOCOL = 'catalog:'
|
||||
|
||||
/**
|
||||
* Parse a package.json dependency specifier using the catalog: protocol.
|
||||
* Returns null if the given specifier does not start with 'catalog:'.
|
||||
*/
|
||||
export function parseCatalogProtocol (pref: string): string | 'default' | null {
|
||||
if (!pref.startsWith(CATALOG_PROTOCOL)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const catalogNameRaw = pref.slice(CATALOG_PROTOCOL.length).trim()
|
||||
|
||||
// Allow a specifier of 'catalog:' to be a short-hand for 'catalog:default'.
|
||||
const catalogNameNormalized = catalogNameRaw === '' ? 'default' : catalogNameRaw
|
||||
|
||||
return catalogNameNormalized
|
||||
}
|
||||
20
catalogs/protocol-parser/test/parseCatalogProtocol.test.ts
Normal file
20
catalogs/protocol-parser/test/parseCatalogProtocol.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { parseCatalogProtocol } from '@pnpm/catalogs.protocol-parser'
|
||||
|
||||
test('parses named catalog', () => {
|
||||
expect(parseCatalogProtocol('catalog:foo')).toBe('foo')
|
||||
expect(parseCatalogProtocol('catalog:bar')).toBe('bar')
|
||||
})
|
||||
|
||||
test('returns null for specifier not using catalog protocol', () => {
|
||||
expect(parseCatalogProtocol('^1.0.0')).toBe(null)
|
||||
})
|
||||
|
||||
describe('default catalog', () => {
|
||||
test('parses explicit default catalog', () => {
|
||||
expect(parseCatalogProtocol('catalog:default')).toBe('default')
|
||||
})
|
||||
|
||||
test('parses implicit catalog', () => {
|
||||
expect(parseCatalogProtocol('catalog:')).toBe('default')
|
||||
})
|
||||
})
|
||||
17
catalogs/protocol-parser/test/tsconfig.json
Normal file
17
catalogs/protocol-parser/test/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"noEmit": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
13
catalogs/protocol-parser/tsconfig.json
Normal file
13
catalogs/protocol-parser/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"composite": true,
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
8
catalogs/protocol-parser/tsconfig.lint.json
Normal file
8
catalogs/protocol-parser/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
5
catalogs/resolver/CHANGELOG.md
Normal file
5
catalogs/resolver/CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# @pnpm/catalogs.resolver
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Initial release
|
||||
3
catalogs/resolver/README.md
Normal file
3
catalogs/resolver/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @pnpm/catalogs.resolver
|
||||
|
||||
> Dereferences `catalog:` protocol specifiers into usable specifiers.
|
||||
1
catalogs/resolver/jest.config.js
Normal file
1
catalogs/resolver/jest.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../jest.config.js')
|
||||
44
catalogs/resolver/package.json
Normal file
44
catalogs/resolver/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@pnpm/catalogs.resolver",
|
||||
"version": "0.1.0",
|
||||
"description": "Dereferences catalog protocol specifiers into usable specifiers.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
],
|
||||
"repository": "https://github.com/pnpm/pnpm/blob/main/catalogs/resolver",
|
||||
"keywords": [
|
||||
"pnpm9",
|
||||
"pnpm",
|
||||
"types"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/catalogs/resolver#readme",
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"compile": "tsc --build && pnpm run lint --fix",
|
||||
"prepublishOnly": "pnpm run compile",
|
||||
"test": "pnpm run compile && pnpm run _test",
|
||||
"_test": "jest"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"exports": {
|
||||
".": "./lib/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/catalogs.protocol-parser": "workspace:^",
|
||||
"@pnpm/error": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/catalogs.resolver": "workspace:*",
|
||||
"@pnpm/catalogs.types": "workspace:*"
|
||||
}
|
||||
}
|
||||
11
catalogs/resolver/src/index.ts
Normal file
11
catalogs/resolver/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export {
|
||||
resolveFromCatalog,
|
||||
type CatalogResolution,
|
||||
type CatalogResolutionFound,
|
||||
type CatalogResolutionMisconfiguration,
|
||||
type CatalogResolutionUnused as CatalogResolutionNotUsed,
|
||||
type CatalogResolutionResult,
|
||||
type CatalogResolver,
|
||||
type WantedDependency,
|
||||
} from './resolveFromCatalog'
|
||||
export { type CatalogResultMatcher, matchCatalogResolveResult } from './matchCatalogResolveResult'
|
||||
18
catalogs/resolver/src/matchCatalogResolveResult.ts
Normal file
18
catalogs/resolver/src/matchCatalogResolveResult.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { type CatalogResolutionUnused, type CatalogResolutionResult, type CatalogResolutionFound, type CatalogResolutionMisconfiguration } from './resolveFromCatalog'
|
||||
|
||||
export interface CatalogResultMatcher<T> {
|
||||
readonly found: (found: CatalogResolutionFound) => T
|
||||
readonly misconfiguration: (misconfiguration: CatalogResolutionMisconfiguration) => T
|
||||
readonly unused: (unused: CatalogResolutionUnused) => T
|
||||
}
|
||||
|
||||
export function matchCatalogResolveResult<T> (
|
||||
result: CatalogResolutionResult,
|
||||
matcher: CatalogResultMatcher<T>
|
||||
): T {
|
||||
switch (result.type) {
|
||||
case 'found': return matcher.found(result)
|
||||
case 'misconfiguration': return matcher.misconfiguration(result)
|
||||
case 'unused': return matcher.unused(result)
|
||||
}
|
||||
}
|
||||
95
catalogs/resolver/src/resolveFromCatalog.ts
Normal file
95
catalogs/resolver/src/resolveFromCatalog.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { parseCatalogProtocol } from '@pnpm/catalogs.protocol-parser'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
|
||||
export interface WantedDependency {
|
||||
readonly pref: string
|
||||
readonly alias: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Dereferences a wanted dependency using the catalog protocol and returns the
|
||||
* configured version.
|
||||
*
|
||||
* Example: catalog:default -> ^1.2.3
|
||||
*/
|
||||
export type CatalogResolver = (wantedDependency: WantedDependency) => CatalogResolutionResult
|
||||
|
||||
export type CatalogResolutionResult = CatalogResolutionFound | CatalogResolutionMisconfiguration | CatalogResolutionUnused
|
||||
|
||||
export interface CatalogResolutionFound {
|
||||
readonly type: 'found'
|
||||
readonly resolution: CatalogResolution
|
||||
}
|
||||
|
||||
export interface CatalogResolution {
|
||||
/**
|
||||
* The name of the catalog the resolved specifier was defined in.
|
||||
*/
|
||||
readonly catalogName: string
|
||||
|
||||
/**
|
||||
* The specifier that should be used for the wanted dependency. This is a
|
||||
* usable version that replaces the catalog protocol with the relevant user
|
||||
* defined specifier.
|
||||
*/
|
||||
readonly specifier: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The user misconfigured a catalog entry. The entry could be missing or
|
||||
* invalid.
|
||||
*/
|
||||
export interface CatalogResolutionMisconfiguration {
|
||||
readonly type: 'misconfiguration'
|
||||
|
||||
/**
|
||||
* Convenience error to rethrow.
|
||||
*/
|
||||
readonly error: PnpmError
|
||||
readonly catalogName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The wanted dependency does not use the catalog protocol.
|
||||
*/
|
||||
export interface CatalogResolutionUnused {
|
||||
readonly type: 'unused'
|
||||
}
|
||||
|
||||
export function resolveFromCatalog (catalogs: Catalogs, wantedDependency: WantedDependency): CatalogResolutionResult {
|
||||
const catalogName = parseCatalogProtocol(wantedDependency.pref)
|
||||
|
||||
if (catalogName == null) {
|
||||
return { type: 'unused' }
|
||||
}
|
||||
|
||||
const catalogLookup = catalogs[catalogName]?.[wantedDependency.alias]
|
||||
if (catalogLookup == null) {
|
||||
return {
|
||||
type: 'misconfiguration',
|
||||
catalogName,
|
||||
error: new PnpmError(
|
||||
'CATALOG_ENTRY_NOT_FOUND_FOR_SPEC',
|
||||
`No catalog entry '${wantedDependency.alias}' was found for catalog '${catalogName}'.`),
|
||||
}
|
||||
}
|
||||
|
||||
if (parseCatalogProtocol(catalogLookup) != null) {
|
||||
return {
|
||||
type: 'misconfiguration',
|
||||
catalogName,
|
||||
error: new PnpmError(
|
||||
'CATALOG_ENTRY_INVALID_RECURSIVE_DEFINITION',
|
||||
`Found invalid catalog entry using the catalog protocol recursively. The entry for '${wantedDependency.alias}' in catalog '${catalogName}' is invalid.`),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'found',
|
||||
resolution: {
|
||||
catalogName,
|
||||
specifier: catalogLookup,
|
||||
},
|
||||
}
|
||||
}
|
||||
77
catalogs/resolver/test/resolveFromCatalog.test.ts
Normal file
77
catalogs/resolver/test/resolveFromCatalog.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { type WantedDependency, resolveFromCatalog } from '@pnpm/catalogs.resolver'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
|
||||
describe('default catalog', () => {
|
||||
const catalogs = {
|
||||
default: {
|
||||
foo: '1.0.0',
|
||||
},
|
||||
}
|
||||
|
||||
test('resolves using implicit name', () => {
|
||||
expect(resolveFromCatalog(catalogs, { alias: 'foo', pref: 'catalog:' }))
|
||||
.toEqual({ type: 'found', resolution: { catalogName: 'default', specifier: '1.0.0' } })
|
||||
})
|
||||
|
||||
test('resolves using explicit name', () => {
|
||||
expect(resolveFromCatalog(catalogs, { alias: 'foo', pref: 'catalog:default' }))
|
||||
.toEqual({ type: 'found', resolution: { catalogName: 'default', specifier: '1.0.0' } })
|
||||
})
|
||||
})
|
||||
|
||||
test('resolves named catalog', () => {
|
||||
const catalogs = {
|
||||
foo: {
|
||||
bar: '1.0.0',
|
||||
},
|
||||
}
|
||||
|
||||
expect(resolveFromCatalog(catalogs, { alias: 'bar', pref: 'catalog:foo' }))
|
||||
.toEqual({ type: 'found', resolution: { catalogName: 'foo', specifier: '1.0.0' } })
|
||||
})
|
||||
|
||||
test('returns unused for specifier not using catalog protocol', () => {
|
||||
const catalogs = {
|
||||
foo: {
|
||||
bar: '1.0.0',
|
||||
},
|
||||
}
|
||||
|
||||
expect(resolveFromCatalog(catalogs, { alias: 'bar', pref: '^2.0.0' })).toEqual({ type: 'unused' })
|
||||
})
|
||||
|
||||
describe('misconfiguration', () => {
|
||||
function resolveFromCatalogOrThrow (catalogs: Catalogs, wantedDependency: WantedDependency) {
|
||||
const result = resolveFromCatalog(catalogs, wantedDependency)
|
||||
if (result.type === 'misconfiguration') {
|
||||
throw result.error
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
test('returns error for missing unresolved catalog', () => {
|
||||
const catalogs = {
|
||||
foo: {
|
||||
bar: '1.0.0',
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => resolveFromCatalogOrThrow(catalogs, { alias: 'bar', pref: 'catalog:' }))
|
||||
.toThrow("No catalog entry 'bar' was found for catalog 'default'.")
|
||||
expect(() => resolveFromCatalogOrThrow(catalogs, { alias: 'bar', pref: 'catalog:baz' }))
|
||||
.toThrow("No catalog entry 'bar' was found for catalog 'baz'.")
|
||||
expect(() => resolveFromCatalogOrThrow(catalogs, { alias: 'foo', pref: 'catalog:foo' }))
|
||||
.toThrow("No catalog entry 'foo' was found for catalog 'foo'.")
|
||||
})
|
||||
|
||||
test('returns error for recursive catalog', () => {
|
||||
const catalogs = {
|
||||
foo: {
|
||||
bar: 'catalog:foo',
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => resolveFromCatalogOrThrow(catalogs, { alias: 'bar', pref: 'catalog:foo' }))
|
||||
.toThrow("Found invalid catalog entry using the catalog protocol recursively. The entry for 'bar' in catalog 'foo' is invalid.")
|
||||
})
|
||||
})
|
||||
17
catalogs/resolver/test/tsconfig.json
Normal file
17
catalogs/resolver/test/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"noEmit": true,
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
23
catalogs/resolver/tsconfig.json
Normal file
23
catalogs/resolver/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"composite": true,
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
{
|
||||
"path": "../protocol-parser"
|
||||
},
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
catalogs/resolver/tsconfig.lint.json
Normal file
8
catalogs/resolver/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
5
catalogs/types/CHANGELOG.md
Normal file
5
catalogs/types/CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# @pnpm/catalogs.types
|
||||
|
||||
## 0.1.0
|
||||
|
||||
Initial release
|
||||
3
catalogs/types/README.md
Normal file
3
catalogs/types/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @pnpm/catalogs.types
|
||||
|
||||
> Types related to the pnpm catalogs feature.
|
||||
38
catalogs/types/package.json
Normal file
38
catalogs/types/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@pnpm/catalogs.types",
|
||||
"version": "0.1.0",
|
||||
"description": "Types related to the pnpm catalogs feature.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
],
|
||||
"repository": "https://github.com/pnpm/pnpm/blob/main/catalogs/types",
|
||||
"keywords": [
|
||||
"pnpm9",
|
||||
"pnpm",
|
||||
"types"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/catalogs/types#readme",
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"compile": "tsc --build && pnpm run lint --fix",
|
||||
"prepublishOnly": "pnpm run compile",
|
||||
"test": "pnpm run compile"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"exports": {
|
||||
".": "./lib/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/catalogs.types": "workspace:*"
|
||||
}
|
||||
}
|
||||
29
catalogs/types/src/index.ts
Normal file
29
catalogs/types/src/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Catalogs parsed from the pnpm-workspace.yaml file.
|
||||
*
|
||||
* https://github.com/pnpm/rfcs/pull/1
|
||||
*/
|
||||
export interface Catalogs {
|
||||
/**
|
||||
* The default catalog.
|
||||
*
|
||||
* The default catalog can be defined in 2 ways.
|
||||
*
|
||||
* 1. Users can specify a top-level "catalog" field or,
|
||||
* 2. An explicitly named "default" catalog under the "catalogs" map.
|
||||
*
|
||||
* This field contains either definition. Note that it's an error to define
|
||||
* the default catalog using both options. The parser will fail when reading
|
||||
* the workspace manifest.
|
||||
*/
|
||||
readonly default?: Catalog
|
||||
|
||||
/**
|
||||
* Named catalogs.
|
||||
*/
|
||||
readonly [catalogName: string]: Catalog | undefined
|
||||
}
|
||||
|
||||
export interface Catalog {
|
||||
readonly [dependencyName: string]: string | undefined
|
||||
}
|
||||
13
catalogs/types/tsconfig.json
Normal file
13
catalogs/types/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"composite": true,
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
8
catalogs/types/tsconfig.lint.json
Normal file
8
catalogs/types/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -32,6 +32,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/config/config#readme",
|
||||
"dependencies": {
|
||||
"@pnpm/catalogs.config": "workspace:*",
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/config.env-replace": "3.0.0",
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Catalogs } from '@pnpm/catalogs.types'
|
||||
import {
|
||||
type Project,
|
||||
type ProjectManifest,
|
||||
@@ -134,6 +135,7 @@ export interface Config {
|
||||
workspaceConcurrency: number
|
||||
workspaceDir?: string
|
||||
workspacePackagePatterns?: string[]
|
||||
catalogs?: Catalogs
|
||||
reporter?: string
|
||||
aggregateOutput: boolean
|
||||
linkWorkspacePackages: boolean | 'deep'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import { getCatalogsFromWorkspaceManifest } from '@pnpm/catalogs.config'
|
||||
import { LAYOUT_VERSION } from '@pnpm/constants'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import loadNpmConf from '@pnpm/npm-conf'
|
||||
@@ -487,12 +488,10 @@ export async function getConfig (opts: {
|
||||
}
|
||||
|
||||
if (pnpmConfig.workspaceDir != null) {
|
||||
if (cliOptions['workspace-packages']) {
|
||||
pnpmConfig.workspacePackagePatterns = cliOptions['workspace-packages'] as string[]
|
||||
} else {
|
||||
const workspaceManifest = await readWorkspaceManifest(pnpmConfig.workspaceDir)
|
||||
pnpmConfig.workspacePackagePatterns = workspaceManifest?.packages
|
||||
}
|
||||
const workspaceManifest = await readWorkspaceManifest(pnpmConfig.workspaceDir)
|
||||
|
||||
pnpmConfig.workspacePackagePatterns = cliOptions['workspace-packages'] as string[] ?? workspaceManifest?.packages
|
||||
pnpmConfig.catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
|
||||
}
|
||||
|
||||
pnpmConfig.failedToLoadBuiltInConfig = failedToLoadBuiltInConfig
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
{
|
||||
"path": "../../__utils__/test-fixtures"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/config"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/types"
|
||||
},
|
||||
{
|
||||
"path": "../../hooks/pnpmfile"
|
||||
},
|
||||
|
||||
@@ -116,6 +116,9 @@ function normalizeLockfile (lockfile: InlineSpecifiersLockfile, opts: NormalizeL
|
||||
if (lockfileToSave.time) {
|
||||
lockfileToSave.time = pruneTimeInLockfileV6(lockfileToSave.time, lockfile.importers ?? {})
|
||||
}
|
||||
if ((lockfileToSave.catalogs != null) && isEmpty(lockfileToSave.catalogs)) {
|
||||
delete lockfileToSave.catalogs
|
||||
}
|
||||
if ((lockfileToSave.overrides != null) && isEmpty(lockfileToSave.overrides)) {
|
||||
delete lockfileToSave.overrides
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ type RootKey = keyof LockfileFile
|
||||
const ROOT_KEYS: readonly RootKey[] = [
|
||||
'lockfileVersion',
|
||||
'settings',
|
||||
'catalogs',
|
||||
'overrides',
|
||||
'packageExtensionsChecksum',
|
||||
'pnpmfileChecksum',
|
||||
@@ -86,6 +87,15 @@ export function sortLockfileKeys (lockfile: LockfileFileV9): LockfileFileV9 {
|
||||
})
|
||||
}
|
||||
}
|
||||
if (lockfile.catalogs != null) {
|
||||
lockfile.catalogs = sortKeys(lockfile.catalogs)
|
||||
for (const [catalogName, catalog] of Object.entries(lockfile.catalogs)) {
|
||||
lockfile.catalogs[catalogName] = sortKeys(catalog, {
|
||||
compare: lexCompare,
|
||||
deep: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const key of ['dependencies', 'devDependencies', 'optionalDependencies', 'time', 'patchedDependencies'] as const) {
|
||||
if (!lockfile[key]) continue
|
||||
lockfile[key] = sortKeys<any>(lockfile[key]) // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Lockfile {
|
||||
importers: Record<ProjectId, ProjectSnapshot>
|
||||
lockfileVersion: string
|
||||
time?: Record<string, string>
|
||||
catalogs?: CatalogSnapshots
|
||||
packages?: PackageSnapshots
|
||||
overrides?: Record<string, string>
|
||||
packageExtensionsChecksum?: string
|
||||
@@ -136,3 +137,24 @@ export type PackageBin = string | { [name: string]: string }
|
||||
* }
|
||||
*/
|
||||
export type ResolvedDependencies = Record<string, string>
|
||||
|
||||
export interface CatalogSnapshots {
|
||||
[catalogName: string]: { [dependencyName: string]: ResolvedCatalogEntry }
|
||||
}
|
||||
|
||||
export interface ResolvedCatalogEntry {
|
||||
/**
|
||||
* The real specifier that should be used for this dependency's catalog entry.
|
||||
* This would be the ^1.2.3 portion of:
|
||||
*
|
||||
* @example
|
||||
* catalog:
|
||||
* foo: ^1.2.3
|
||||
*/
|
||||
readonly specifier: string
|
||||
|
||||
/**
|
||||
* The concrete version that the requested specifier resolved to. Ex: 1.2.3
|
||||
*/
|
||||
readonly version: string
|
||||
}
|
||||
|
||||
@@ -36,7 +36,12 @@ export async function makeDedicatedLockfile (lockfileDir: string, projectDir: st
|
||||
await writeWantedLockfile(projectDir, dedicatedLockfile)
|
||||
|
||||
const { manifest, writeProjectManifest } = await readProjectManifest(projectDir)
|
||||
const publishManifest = await createExportableManifest(projectDir, manifest)
|
||||
const publishManifest = await createExportableManifest(projectDir, manifest, {
|
||||
// Since @pnpm/make-dedicated-lockfile is deprecated, avoid supporting new
|
||||
// features like pnpm catalogs. Passing in an empty catalog object
|
||||
// intentionally.
|
||||
catalogs: {},
|
||||
})
|
||||
await writeProjectManifest(publishManifest)
|
||||
|
||||
const modulesDir = path.join(projectDir, 'node_modules')
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"@pnpm/build-modules": "workspace:*",
|
||||
"@pnpm/builder.policy": "3.0.0",
|
||||
"@pnpm/calc-dep-state": "workspace:*",
|
||||
"@pnpm/catalogs.protocol-parser": "workspace:*",
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/core-loggers": "workspace:*",
|
||||
"@pnpm/crypto.base32-hash": "workspace:*",
|
||||
@@ -89,6 +91,7 @@
|
||||
"@pnpm/store.cafs": "workspace:*",
|
||||
"@pnpm/test-fixtures": "workspace:*",
|
||||
"@pnpm/test-ipc-server": "workspace:*",
|
||||
"@pnpm/workspace.find-packages": "workspace:*",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/is-windows": "^1.0.2",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DEFAULT_REGISTRIES } from '@pnpm/normalize-registries'
|
||||
|
||||
export type ListMissingPeersOptions = Partial<GetContextOptions>
|
||||
& Pick<InstallOptions, 'hooks'
|
||||
| 'catalogs'
|
||||
| 'dedupePeerDependents'
|
||||
| 'ignoreCompatibilityDb'
|
||||
| 'linkWorkspacePackagesDepth'
|
||||
@@ -60,6 +61,7 @@ export async function getPeerDependencyIssues (
|
||||
currentLockfile: ctx.currentLockfile,
|
||||
allowedDeprecatedVersions: {},
|
||||
allowNonAppliedPatches: false,
|
||||
catalogs: opts.catalogs,
|
||||
defaultUpdateDepth: -1,
|
||||
dedupePeerDependents: opts.dedupePeerDependents,
|
||||
dryRun: true,
|
||||
|
||||
11
pkg-manager/core/src/install/allCatalogsAreUpToDate.ts
Normal file
11
pkg-manager/core/src/install/allCatalogsAreUpToDate.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { type CatalogSnapshots } from '@pnpm/lockfile-file'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
|
||||
export function allCatalogsAreUpToDate (
|
||||
catalogsConfig: Catalogs,
|
||||
snapshot: CatalogSnapshots | undefined
|
||||
): boolean {
|
||||
return Object.entries(snapshot ?? {})
|
||||
.every(([catalogName, catalog]) => Object.entries(catalog ?? {})
|
||||
.every(([alias, entry]) => entry.specifier === catalogsConfig[catalogName]?.[alias]))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from 'path'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type ProjectOptions } from '@pnpm/get-context'
|
||||
import {
|
||||
type PackageSnapshot,
|
||||
@@ -21,10 +22,12 @@ import pEvery from 'p-every'
|
||||
import any from 'ramda/src/any'
|
||||
import semver from 'semver'
|
||||
import getVersionSelectorType from 'version-selector-type'
|
||||
import { allCatalogsAreUpToDate } from './allCatalogsAreUpToDate'
|
||||
|
||||
export async function allProjectsAreUpToDate (
|
||||
projects: Array<Pick<ProjectOptions, 'manifest' | 'rootDir'> & { id: ProjectId }>,
|
||||
opts: {
|
||||
catalogs: Catalogs
|
||||
autoInstallPeers: boolean
|
||||
excludeLinksFromLockfile: boolean
|
||||
linkWorkspacePackages: boolean
|
||||
@@ -33,6 +36,13 @@ export async function allProjectsAreUpToDate (
|
||||
lockfileDir: string
|
||||
}
|
||||
): Promise<boolean> {
|
||||
// Projects may declare dependencies using catalog protocol specifiers. If the
|
||||
// catalog config definitions are edited by users, projects using them are out
|
||||
// of date.
|
||||
if (!allCatalogsAreUpToDate(opts.catalogs, opts.wantedLockfile.catalogs)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const manifestsByDir = opts.workspacePackages ? getWorkspacePackagesByDirectory(opts.workspacePackages) : {}
|
||||
const _satisfiesPackageManifest = satisfiesPackageManifest.bind(null, {
|
||||
autoInstallPeers: opts.autoInstallPeers,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WANTED_LOCKFILE } from '@pnpm/constants'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { type ProjectOptions } from '@pnpm/get-context'
|
||||
import { type HoistingLimits } from '@pnpm/headless'
|
||||
@@ -22,6 +23,7 @@ import { type PreResolutionHookContext } from '@pnpm/hooks.types'
|
||||
export interface StrictInstallOptions {
|
||||
autoInstallPeers: boolean
|
||||
autoInstallPeersFromHighestMatch: boolean
|
||||
catalogs: Catalogs
|
||||
frozenLockfile: boolean
|
||||
frozenLockfileIfExists: boolean
|
||||
enablePnp: boolean
|
||||
|
||||
@@ -2,6 +2,7 @@ import crypto from 'crypto'
|
||||
import path from 'path'
|
||||
import { buildModules, type DepsStateCache, linkBinsOfDependencies } from '@pnpm/build-modules'
|
||||
import { createAllowBuildFunction } from '@pnpm/builder.policy'
|
||||
import { parseCatalogProtocol } from '@pnpm/catalogs.protocol-parser'
|
||||
import {
|
||||
LAYOUT_VERSION,
|
||||
LOCKFILE_VERSION,
|
||||
@@ -395,6 +396,7 @@ export async function mutateModules (
|
||||
ctx.wantedLockfile.lockfileVersion === '6.1'
|
||||
) &&
|
||||
await allProjectsAreUpToDate(Object.values(ctx.projects), {
|
||||
catalogs: opts.catalogs,
|
||||
autoInstallPeers: opts.autoInstallPeers,
|
||||
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
|
||||
linkWorkspacePackages: opts.linkWorkspacePackagesDepth >= 0,
|
||||
@@ -629,6 +631,28 @@ Note that in CI environments, this setting is enabled by default.`,
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
function isWantedDepPrefSame (alias: string, prevPref: string | undefined, nextPref: string): boolean {
|
||||
if (prevPref !== nextPref) {
|
||||
return false
|
||||
}
|
||||
|
||||
// When pnpm catalogs are used, the specifiers can be the same (e.g.
|
||||
// "catalog:default"), but the wanted versions for the dependency can be
|
||||
// different after resolution if the catalog config was just edited.
|
||||
const catalogName = parseCatalogProtocol(prevPref)
|
||||
|
||||
// If there's no catalog name, the catalog protocol was not used and we
|
||||
// can assume the pref is the same since prevPref and nextPref match.
|
||||
if (catalogName === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
const prevCatalogEntrySpec = ctx.wantedLockfile.catalogs?.[catalogName]?.[alias]?.specifier
|
||||
const nextCatalogEntrySpec = opts.catalogs[catalogName]?.[alias]
|
||||
|
||||
return prevCatalogEntrySpec === nextCatalogEntrySpec
|
||||
}
|
||||
|
||||
async function installCase (project: any) { // eslint-disable-line
|
||||
const wantedDependencies = getWantedDependencies(project.manifest, {
|
||||
autoInstallPeers: opts.autoInstallPeers,
|
||||
@@ -639,7 +663,7 @@ Note that in CI environments, this setting is enabled by default.`,
|
||||
.map((wantedDependency) => ({ ...wantedDependency, updateSpec: true, preserveNonSemverVersionSpec: true }))
|
||||
|
||||
if (ctx.wantedLockfile?.importers) {
|
||||
forgetResolutionsOfPrevWantedDeps(ctx.wantedLockfile.importers[project.id], wantedDependencies)
|
||||
forgetResolutionsOfPrevWantedDeps(ctx.wantedLockfile.importers[project.id], wantedDependencies, isWantedDepPrefSame)
|
||||
}
|
||||
if (opts.ignoreScripts && project.manifest?.scripts &&
|
||||
(project.manifest.scripts.preinstall ||
|
||||
@@ -808,13 +832,17 @@ function pkgHasDependencies (manifest: ProjectManifest): boolean {
|
||||
|
||||
// If the specifier is new, the old resolution probably does not satisfy it anymore.
|
||||
// By removing these resolutions we ensure that they are resolved again using the new specs.
|
||||
function forgetResolutionsOfPrevWantedDeps (importer: ProjectSnapshot, wantedDeps: WantedDependency[]): void {
|
||||
function forgetResolutionsOfPrevWantedDeps (
|
||||
importer: ProjectSnapshot,
|
||||
wantedDeps: WantedDependency[],
|
||||
isWantedDepPrefSame: (alias: string, prevPref: string | undefined, nextPref: string) => boolean
|
||||
): void {
|
||||
if (!importer.specifiers) return
|
||||
importer.dependencies = importer.dependencies ?? {}
|
||||
importer.devDependencies = importer.devDependencies ?? {}
|
||||
importer.optionalDependencies = importer.optionalDependencies ?? {}
|
||||
for (const { alias, pref } of wantedDeps) {
|
||||
if (alias && importer.specifiers[alias] !== pref) {
|
||||
if (alias && !isWantedDepPrefSame(alias, importer.specifiers[alias], pref)) {
|
||||
if (!importer.dependencies[alias]?.startsWith('link:')) {
|
||||
delete importer.dependencies[alias]
|
||||
}
|
||||
@@ -1035,6 +1063,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
allowNonAppliedPatches: opts.allowNonAppliedPatches,
|
||||
autoInstallPeers: opts.autoInstallPeers,
|
||||
autoInstallPeersFromHighestMatch: opts.autoInstallPeersFromHighestMatch,
|
||||
catalogs: opts.catalogs,
|
||||
currentLockfile: ctx.currentLockfile,
|
||||
defaultUpdateDepth: opts.depth,
|
||||
dedupeDirectDeps: opts.dedupeDirectDeps,
|
||||
@@ -1412,6 +1441,7 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
if (allProjectsLocatedInsideWorkspace.length > projects.length) {
|
||||
if (
|
||||
await allProjectsAreUpToDate(allProjectsLocatedInsideWorkspace, {
|
||||
catalogs: opts.catalogs,
|
||||
autoInstallPeers: opts.autoInstallPeers,
|
||||
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
|
||||
linkWorkspacePackages: opts.linkWorkspacePackagesDepth >= 0,
|
||||
|
||||
@@ -36,6 +36,7 @@ test('allProjectsAreUpToDate(): works with packages linked through the workspace
|
||||
},
|
||||
], {
|
||||
autoInstallPeers: false,
|
||||
catalogs: {},
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
@@ -77,6 +78,7 @@ test('allProjectsAreUpToDate(): works with aliased local dependencies', async ()
|
||||
},
|
||||
], {
|
||||
autoInstallPeers: false,
|
||||
catalogs: {},
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
@@ -118,6 +120,7 @@ test('allProjectsAreUpToDate(): works with aliased local dependencies that speci
|
||||
},
|
||||
], {
|
||||
autoInstallPeers: false,
|
||||
catalogs: {},
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
@@ -159,6 +162,7 @@ test('allProjectsAreUpToDate(): returns false if the aliased dependency version
|
||||
},
|
||||
], {
|
||||
autoInstallPeers: false,
|
||||
catalogs: {},
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
@@ -230,6 +234,7 @@ test('allProjectsAreUpToDate(): use link and registry version if linkWorkspacePa
|
||||
],
|
||||
{
|
||||
autoInstallPeers: false,
|
||||
catalogs: {},
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: false,
|
||||
wantedLockfile: {
|
||||
@@ -296,6 +301,7 @@ test('allProjectsAreUpToDate(): returns false if dependenciesMeta differs', asyn
|
||||
},
|
||||
], {
|
||||
autoInstallPeers: false,
|
||||
catalogs: {},
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
@@ -342,6 +348,7 @@ test('allProjectsAreUpToDate(): returns true if dependenciesMeta matches', async
|
||||
},
|
||||
], {
|
||||
autoInstallPeers: false,
|
||||
catalogs: {},
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
@@ -400,6 +407,7 @@ describe('local file dependency', () => {
|
||||
]
|
||||
const options = {
|
||||
autoInstallPeers: false,
|
||||
catalogs: {},
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
@@ -499,6 +507,7 @@ test('allProjectsAreUpToDate(): returns true if workspace dependency\'s version
|
||||
]
|
||||
const options = {
|
||||
autoInstallPeers: false,
|
||||
catalogs: {},
|
||||
excludeLinksFromLockfile: false,
|
||||
linkWorkspacePackages: true,
|
||||
wantedLockfile: {
|
||||
|
||||
371
pkg-manager/core/test/catalogs.ts
Normal file
371
pkg-manager/core/test/catalogs.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import { createPeersDirSuffix } from '@pnpm/dependency-path'
|
||||
import { type ProjectId, type ProjectManifest } from '@pnpm/types'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { type MutatedProject, mutateModules, type ProjectOptions } from '@pnpm/core'
|
||||
import { arrayOfWorkspacePackagesToMap } from '@pnpm/workspace.find-packages'
|
||||
import path from 'path'
|
||||
import { testDefaults } from './utils'
|
||||
|
||||
function preparePackagesAndReturnObjects (manifests: Array<ProjectManifest & Required<Pick<ProjectManifest, 'name'>>>) {
|
||||
const project = prepareEmpty()
|
||||
const projects: Record<ProjectId, ProjectManifest> = {}
|
||||
for (const manifest of manifests) {
|
||||
projects[manifest.name as ProjectId] = manifest
|
||||
}
|
||||
const allProjects: ProjectOptions[] = Object.entries(projects)
|
||||
.map(([id, manifest]) => ({
|
||||
buildIndex: 0,
|
||||
manifest,
|
||||
dir: path.resolve(id),
|
||||
rootDir: path.resolve(id),
|
||||
}))
|
||||
return {
|
||||
...project,
|
||||
projects,
|
||||
options: testDefaults({
|
||||
allProjects,
|
||||
workspacePackages: arrayOfWorkspacePackagesToMap(allProjects),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function installProjects (projects: Record<ProjectId, ProjectManifest>): MutatedProject[] {
|
||||
return Object.entries(projects)
|
||||
.map(([id, manifest]) => ({
|
||||
mutation: 'install',
|
||||
id,
|
||||
manifest,
|
||||
rootDir: path.resolve(id),
|
||||
}))
|
||||
}
|
||||
|
||||
test('installing with "catalog:" should work', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([
|
||||
{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
},
|
||||
// Empty second project to create a multi-package workspace.
|
||||
{
|
||||
name: 'project2',
|
||||
},
|
||||
])
|
||||
|
||||
await mutateModules(installProjects(projects), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
default: { 'is-positive': '1.0.0' },
|
||||
},
|
||||
})
|
||||
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.importers['project1' as ProjectId]).toEqual({
|
||||
dependencies: {
|
||||
'is-positive': {
|
||||
specifier: 'catalog:',
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('importer to importer dependency with "catalog:" should work', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([
|
||||
{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
project2: 'workspace:*',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project2',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await mutateModules(installProjects(projects), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
default: { 'is-positive': '1.0.0' },
|
||||
},
|
||||
})
|
||||
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.importers['project2' as ProjectId]).toEqual({
|
||||
dependencies: {
|
||||
'is-positive': {
|
||||
specifier: 'catalog:',
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('importer with different peers uses correct peer', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([
|
||||
{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/has-foo100-peer': 'catalog:',
|
||||
// Define a peer with an exact version to ensure the dep above uses
|
||||
// this peer.
|
||||
'@pnpm.e2e/foo': '100.0.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project2',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/has-foo100-peer': 'catalog:',
|
||||
// Note that this peer is intentionally different than the one above
|
||||
// for project 1. (100.1.0 instead of 100.0.0).
|
||||
//
|
||||
// We want to ensure project2 resolves to the same catalog version for
|
||||
// @pnpm.e2e/has-foo100-peer, but uses a different peers suffix.
|
||||
//
|
||||
// Catalogs allow versions to be reused, but this test ensures we
|
||||
// don't reuse versions too aggressively.
|
||||
'@pnpm.e2e/foo': '100.1.0',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await mutateModules(installProjects(projects), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/has-foo100-peer': '^1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.importers['project1' as ProjectId]?.dependencies?.['@pnpm.e2e/has-foo100-peer']).toEqual({
|
||||
specifier: 'catalog:',
|
||||
version: `1.0.0${createPeersDirSuffix([{ name: '@pnpm.e2e/foo', version: '100.0.0' }])}`,
|
||||
})
|
||||
expect(lockfile.importers['project2' as ProjectId]?.dependencies?.['@pnpm.e2e/has-foo100-peer']).toEqual({
|
||||
specifier: 'catalog:',
|
||||
// This version is intentionally different from the one above ꜜ
|
||||
version: `1.0.0${createPeersDirSuffix([{ name: '@pnpm.e2e/foo', version: '100.1.0' }])}`,
|
||||
})
|
||||
})
|
||||
|
||||
test('lockfile contains catalog snapshots', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([
|
||||
{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project2',
|
||||
dependencies: {
|
||||
'is-negative': 'catalog:',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await mutateModules(installProjects(projects), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
default: {
|
||||
'is-positive': '^1.0.0',
|
||||
'is-negative': '^1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.catalogs).toStrictEqual({
|
||||
default: {
|
||||
'is-positive': { specifier: '^1.0.0', version: '1.0.0' },
|
||||
'is-negative': { specifier: '^1.0.0', version: '1.0.0' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('lockfile is updated if catalog config changes', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([
|
||||
{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await mutateModules(installProjects(projects), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
default: {
|
||||
'is-positive': '=1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(readLockfile().importers['project1' as ProjectId]).toEqual({
|
||||
dependencies: {
|
||||
'is-positive': {
|
||||
specifier: 'catalog:',
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await mutateModules(installProjects(projects), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
default: {
|
||||
'is-positive': '=3.1.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(readLockfile().importers['project1' as ProjectId]).toEqual({
|
||||
dependencies: {
|
||||
'is-positive': {
|
||||
specifier: 'catalog:',
|
||||
version: '3.1.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('lockfile catalog snapshots retain existing entries on --filter', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([
|
||||
{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-negative': 'catalog:',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project2',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await mutateModules(installProjects(projects), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
default: {
|
||||
'is-positive': '^1.0.0',
|
||||
'is-negative': '^1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(readLockfile().catalogs).toStrictEqual({
|
||||
default: {
|
||||
'is-negative': { specifier: '^1.0.0', version: '1.0.0' },
|
||||
'is-positive': { specifier: '^1.0.0', version: '1.0.0' },
|
||||
},
|
||||
})
|
||||
|
||||
// Update catalog definitions so pnpm triggers a rerun.
|
||||
await mutateModules(installProjects(projects).slice(1), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs: {
|
||||
default: {
|
||||
'is-positive': '=3.1.0',
|
||||
'is-negative': '^1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(readLockfile().catalogs).toStrictEqual({
|
||||
default: {
|
||||
// The is-negative snapshot should be carried from the previous install,
|
||||
// despite the current filtered install not using it.
|
||||
'is-negative': { specifier: '^1.0.0', version: '1.0.0' },
|
||||
|
||||
'is-positive': { specifier: '=3.1.0', version: '3.1.0' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// If a catalog specifier was used in one or more package.json files and all
|
||||
// usages were removed later, we should remove the catalog snapshot from
|
||||
// pnpm-lock.yaml. This should happen even if the dependency is still defined in
|
||||
// a catalog under pnpm-workspace.yaml.
|
||||
//
|
||||
// Note that this behavior may not be desirable in all cases. If someone removes
|
||||
// the last usage of a catalog entry, and another person adds it back later,
|
||||
// that dependency will be re-resolved to a newer version. This is probably
|
||||
// desirable most of the time, but there could be a good argument to cache the
|
||||
// older unused resolution. For now we'll remove the unused entries since that's
|
||||
// what would happen anyway if catalogs aren't used.
|
||||
test('lockfile catalog snapshots should remove unused entries', async () => {
|
||||
const { options, projects, readLockfile } = preparePackagesAndReturnObjects([
|
||||
{
|
||||
name: 'project1',
|
||||
dependencies: {
|
||||
'is-negative': 'catalog:',
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const catalogs = {
|
||||
default: {
|
||||
'is-negative': '=1.0.0',
|
||||
'is-positive': '=1.0.0',
|
||||
},
|
||||
}
|
||||
await mutateModules(installProjects(projects), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs,
|
||||
})
|
||||
|
||||
{
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.importers['project1' as ProjectId]?.dependencies).toEqual({
|
||||
'is-negative': { specifier: 'catalog:', version: '1.0.0' },
|
||||
'is-positive': { specifier: 'catalog:', version: '1.0.0' },
|
||||
})
|
||||
expect(lockfile.catalogs?.default).toStrictEqual({
|
||||
'is-negative': { specifier: '=1.0.0', version: '1.0.0' },
|
||||
'is-positive': { specifier: '=1.0.0', version: '1.0.0' },
|
||||
})
|
||||
}
|
||||
|
||||
// Update package.json to no longer depend on is-positive.
|
||||
projects['project1' as ProjectId].dependencies = {
|
||||
'is-negative': 'catalog:',
|
||||
}
|
||||
await mutateModules(installProjects(projects), {
|
||||
...options,
|
||||
lockfileOnly: true,
|
||||
catalogs,
|
||||
})
|
||||
|
||||
{
|
||||
const lockfile = readLockfile()
|
||||
expect(lockfile.importers['project1' as ProjectId]?.dependencies).toEqual({
|
||||
'is-negative': { specifier: 'catalog:', version: '1.0.0' },
|
||||
})
|
||||
// Only "is-negative" should be in the catalogs section of the lockfile
|
||||
// since all packages in the workspace no longer use is-positive. Note that
|
||||
// this should be the case even if pnpm-workspace.yaml still has
|
||||
// "is-positive" configured.
|
||||
expect(lockfile.catalogs?.default).toStrictEqual({
|
||||
'is-negative': { specifier: '=1.0.0', version: '1.0.0' },
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -24,6 +24,12 @@
|
||||
{
|
||||
"path": "../../__utils__/test-ipc-server"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/protocol-parser"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/types"
|
||||
},
|
||||
{
|
||||
"path": "../../config/matcher"
|
||||
},
|
||||
@@ -135,6 +141,9 @@
|
||||
{
|
||||
"path": "../../worker"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/find-packages"
|
||||
},
|
||||
{
|
||||
"path": "../client"
|
||||
},
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"_test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/catalogs.resolver": "workspace:*",
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/core-loggers": "workspace:*",
|
||||
"@pnpm/dependency-path": "workspace:*",
|
||||
|
||||
21
pkg-manager/resolve-dependencies/src/getCatalogSnapshots.ts
Normal file
21
pkg-manager/resolve-dependencies/src/getCatalogSnapshots.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { type CatalogSnapshots } from '@pnpm/lockfile-types'
|
||||
import { type ResolvedDirectDependency } from './resolveDependencyTree'
|
||||
|
||||
export function getCatalogSnapshots (resolvedDirectDeps: readonly ResolvedDirectDependency[]): CatalogSnapshots {
|
||||
const catalogSnapshots: CatalogSnapshots = {}
|
||||
const catalogedDeps = resolvedDirectDeps.filter(isCatalogedDep)
|
||||
|
||||
for (const dep of catalogedDeps) {
|
||||
const snapshotForSingleCatalog = (catalogSnapshots[dep.catalogLookup.catalogName] ??= {})
|
||||
snapshotForSingleCatalog[dep.alias] = {
|
||||
specifier: dep.catalogLookup.specifier,
|
||||
version: dep.version,
|
||||
}
|
||||
}
|
||||
|
||||
return catalogSnapshots
|
||||
}
|
||||
|
||||
function isCatalogedDep (dep: ResolvedDirectDependency): dep is ResolvedDirectDependency & { catalogLookup: Required<ResolvedDirectDependency>['catalogLookup'] } {
|
||||
return dep.catalogLookup != null
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
import { toResolveImporter } from './toResolveImporter'
|
||||
import { updateLockfile } from './updateLockfile'
|
||||
import { updateProjectManifest } from './updateProjectManifest'
|
||||
import { getCatalogSnapshots } from './getCatalogSnapshots'
|
||||
|
||||
export type DependenciesGraph = GenericDependenciesGraphWithResolvedChildren<ResolvedPackage>
|
||||
|
||||
@@ -304,6 +305,8 @@ export async function resolveDependencies (
|
||||
}
|
||||
}
|
||||
|
||||
newLockfile.catalogs = getCatalogSnapshots(Object.values(resolvedImporters).flatMap(({ directDependencies }) => directDependencies))
|
||||
|
||||
// waiting till package requests are finished
|
||||
async function waitTillAllFetchingsFinish (): Promise<void> {
|
||||
await Promise.all(Object.values(resolvedPkgsById).map(async ({ fetching }) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from 'path'
|
||||
import { matchCatalogResolveResult, type CatalogResolver } from '@pnpm/catalogs.resolver'
|
||||
import {
|
||||
deprecationLogger,
|
||||
progressLogger,
|
||||
@@ -56,6 +57,7 @@ import { type NodeId, nextNodeId } from './nextNodeId'
|
||||
import { parentIdsContainSequence } from './parentIdsContainSequence'
|
||||
import { hoistPeers, getHoistableOptionalPeers } from './hoistPeers'
|
||||
import { wantedDepIsLocallyAvailable } from './wantedDepIsLocallyAvailable'
|
||||
import { type CatalogLookupMetadata } from './resolveDependencyTree'
|
||||
import { replaceVersionInPref } from './replaceVersionInPref'
|
||||
|
||||
const dependencyResolvedLogger = logger('_dependency_resolved')
|
||||
@@ -110,6 +112,7 @@ export interface LinkedDependency {
|
||||
name: string
|
||||
normalizedPref?: string
|
||||
alias: string
|
||||
catalogLookup?: CatalogLookupMetadata
|
||||
}
|
||||
|
||||
export interface PendingNode {
|
||||
@@ -137,6 +140,7 @@ export interface ResolutionContext {
|
||||
allPreferredVersions?: PreferredVersions
|
||||
appliedPatches: Set<string>
|
||||
updatedSet: Set<string>
|
||||
catalogResolver: CatalogResolver
|
||||
defaultTag: string
|
||||
dryRun: boolean
|
||||
forceFullResolution: boolean
|
||||
@@ -196,6 +200,7 @@ export type PkgAddress = {
|
||||
missingPeers: MissingPeers
|
||||
missingPeersOfChildren?: MissingPeersOfChildren
|
||||
publishedAt?: string
|
||||
catalogLookup?: CatalogLookupMetadata
|
||||
optional: boolean
|
||||
} & ({
|
||||
isLinkedDependency: true
|
||||
@@ -409,18 +414,12 @@ async function resolveDependenciesOfImporters (
|
||||
const postponedPeersResolutionQueue: PostponedPeersResolutionFunction[] = []
|
||||
const pkgAddresses: PkgAddress[] = []
|
||||
|
||||
const resolvedDependenciesOfImporter = await Promise.all(
|
||||
extendedWantedDeps.map((extendedWantedDep) => resolveDependenciesOfDependency(
|
||||
ctx,
|
||||
importer.preferredVersions,
|
||||
{
|
||||
...importer.options,
|
||||
parentPkgAliases: importer.parentPkgAliases,
|
||||
pickLowestVersion: pickLowestVersion && !importer.updatePackageManifest,
|
||||
},
|
||||
extendedWantedDep
|
||||
))
|
||||
)
|
||||
const resolveDependenciesOfImporterWantedDep = resolveDependenciesOfImporterDependency.bind(null, {
|
||||
ctx,
|
||||
importer,
|
||||
pickLowestVersion,
|
||||
})
|
||||
const resolvedDependenciesOfImporter = await Promise.all(extendedWantedDeps.map(resolveDependenciesOfImporterWantedDep))
|
||||
|
||||
for (const { resolveDependencyResult, postponedPeersResolution, postponedResolution } of resolvedDependenciesOfImporter) {
|
||||
if (resolveDependencyResult) {
|
||||
@@ -511,6 +510,51 @@ async function resolveDependenciesOfImporters (
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResolveDependenciesOfImporterDependencyOpts {
|
||||
readonly ctx: ResolutionContext
|
||||
readonly importer: ImporterToResolve
|
||||
readonly pickLowestVersion: boolean
|
||||
}
|
||||
|
||||
async function resolveDependenciesOfImporterDependency (
|
||||
{ ctx, importer, pickLowestVersion }: ResolveDependenciesOfImporterDependencyOpts,
|
||||
extendedWantedDep: ExtendedWantedDependency
|
||||
): Promise<ResolveDependenciesOfDependency> {
|
||||
// The catalog protocol is only usable in importers (i.e. packages in the
|
||||
// workspace. Replacing catalog protocol while resolving importers here before
|
||||
// resolving dependencies of packages outside of the workspace/monorepo.
|
||||
const catalogLookup = matchCatalogResolveResult(ctx.catalogResolver(extendedWantedDep.wantedDependency), {
|
||||
found: (result) => result.resolution,
|
||||
unused: () => undefined,
|
||||
misconfiguration: (result) => {
|
||||
throw result.error
|
||||
},
|
||||
})
|
||||
|
||||
if (catalogLookup != null) {
|
||||
extendedWantedDep.wantedDependency.pref = catalogLookup.specifier
|
||||
}
|
||||
|
||||
const result = await resolveDependenciesOfDependency(
|
||||
ctx,
|
||||
importer.preferredVersions,
|
||||
{
|
||||
...importer.options,
|
||||
parentPkgAliases: importer.parentPkgAliases,
|
||||
pickLowestVersion: pickLowestVersion && !importer.updatePackageManifest,
|
||||
},
|
||||
extendedWantedDep
|
||||
)
|
||||
|
||||
// If the catalog protocol was used, store metadata about the catalog
|
||||
// lookup to use in the lockfile.
|
||||
if (result.resolveDependencyResult != null && catalogLookup != null) {
|
||||
result.resolveDependencyResult.catalogLookup = catalogLookup
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function filterMissingPeersFromPkgAddresses (
|
||||
pkgAddresses: PkgAddress[],
|
||||
currentParentPkgAliases: ParentPkgAliases,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { resolveFromCatalog } from '@pnpm/catalogs.resolver'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type Lockfile, type PatchFile } from '@pnpm/lockfile-types'
|
||||
import { type PreferredVersions, type Resolution, type WorkspacePackages } from '@pnpm/resolver-base'
|
||||
import { type StoreController } from '@pnpm/store-controller-types'
|
||||
@@ -49,6 +51,16 @@ export interface ResolvedDirectDependency {
|
||||
version: string
|
||||
name: string
|
||||
normalizedPref?: string
|
||||
catalogLookup?: CatalogLookupMetadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Information related to the catalog entry for this dependency if it was
|
||||
* requested through the catalog protocol.
|
||||
*/
|
||||
export interface CatalogLookupMetadata {
|
||||
readonly catalogName: string
|
||||
readonly specifier: string
|
||||
}
|
||||
|
||||
export interface Importer<WantedDepExtraProps> {
|
||||
@@ -74,6 +86,7 @@ export interface ResolveDependenciesOptions {
|
||||
allowBuild?: (pkgName: string) => boolean
|
||||
allowedDeprecatedVersions: AllowedDeprecatedVersions
|
||||
allowNonAppliedPatches: boolean
|
||||
catalogs?: Catalogs
|
||||
currentLockfile: Lockfile
|
||||
dedupePeerDependents?: boolean
|
||||
dryRun: boolean
|
||||
@@ -129,6 +142,7 @@ export async function resolveDependencyTree<T> (
|
||||
autoInstallPeersFromHighestMatch: opts.autoInstallPeersFromHighestMatch === true,
|
||||
allowBuild: opts.allowBuild,
|
||||
allowedDeprecatedVersions: opts.allowedDeprecatedVersions,
|
||||
catalogResolver: resolveFromCatalog.bind(null, opts.catalogs ?? {}),
|
||||
childrenByParentId: {} as ChildrenByParentId,
|
||||
currentLockfile: opts.currentLockfile,
|
||||
defaultTag: opts.tag,
|
||||
@@ -229,6 +243,7 @@ export async function resolveDependencyTree<T> (
|
||||
const resolvedPackage = ctx.dependenciesTree.get(dep.nodeId)!.resolvedPackage as ResolvedPackage
|
||||
return {
|
||||
alias: dep.alias,
|
||||
catalogLookup: dep.catalogLookup,
|
||||
dev: resolvedPackage.dev,
|
||||
name: resolvedPackage.name,
|
||||
normalizedPref: dep.normalizedPref,
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../catalogs/resolver"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/types"
|
||||
},
|
||||
{
|
||||
"path": "../../config/pick-registry-for-package"
|
||||
},
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/pkg-manifest/exportable-manifest#readme",
|
||||
"devDependencies": {
|
||||
"@pnpm/catalogs.config": "workspace:*",
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/exportable-manifest": "workspace:*",
|
||||
"@pnpm/prepare": "workspace:*",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
@@ -39,6 +41,7 @@
|
||||
"dependencies": {
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/read-project-manifest": "workspace:*",
|
||||
"@pnpm/catalogs.resolver": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"p-map-values": "^1.0.0",
|
||||
"ramda": "npm:@pnpm/ramda@0.28.1"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import path from 'path'
|
||||
import { type CatalogResolver, resolveFromCatalog } from '@pnpm/catalogs.resolver'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { tryReadProjectManifest } from '@pnpm/read-project-manifest'
|
||||
import { type Dependencies, type ProjectManifest } from '@pnpm/types'
|
||||
@@ -16,6 +18,7 @@ const PREPUBLISH_SCRIPTS = [
|
||||
]
|
||||
|
||||
export interface MakePublishManifestOptions {
|
||||
catalogs: Catalogs
|
||||
modulesDir?: string
|
||||
readmeFile?: string
|
||||
}
|
||||
@@ -23,14 +26,22 @@ export interface MakePublishManifestOptions {
|
||||
export async function createExportableManifest (
|
||||
dir: string,
|
||||
originalManifest: ProjectManifest,
|
||||
opts?: MakePublishManifestOptions
|
||||
opts: MakePublishManifestOptions
|
||||
): Promise<ProjectManifest> {
|
||||
const publishManifest: ProjectManifest = omit(['pnpm', 'scripts', 'packageManager'], originalManifest)
|
||||
if (originalManifest.scripts != null) {
|
||||
publishManifest.scripts = omit(PREPUBLISH_SCRIPTS, originalManifest.scripts)
|
||||
}
|
||||
|
||||
const catalogResolver = resolveFromCatalog.bind(null, opts.catalogs)
|
||||
const replaceCatalogProtocol = resolveCatalogProtocol.bind(null, catalogResolver)
|
||||
|
||||
const convertDependencyForPublish = combineConverters(replaceWorkspaceProtocol, replaceCatalogProtocol)
|
||||
await Promise.all((['dependencies', 'devDependencies', 'optionalDependencies'] as const).map(async (depsField) => {
|
||||
const deps = await makePublishDependencies(dir, originalManifest[depsField], { modulesDir: opts?.modulesDir })
|
||||
const deps = await makePublishDependencies(dir, originalManifest[depsField], {
|
||||
modulesDir: opts?.modulesDir,
|
||||
convertDependencyForPublish,
|
||||
})
|
||||
if (deps != null) {
|
||||
publishManifest[depsField] = deps
|
||||
}
|
||||
@@ -38,7 +49,11 @@ export async function createExportableManifest (
|
||||
|
||||
const peerDependencies = originalManifest.peerDependencies
|
||||
if (peerDependencies) {
|
||||
publishManifest.peerDependencies = await makePublishDependencies(dir, peerDependencies, { modulesDir: opts?.modulesDir, convertDependencyForPublish: makePublishPeerDependency })
|
||||
const convertPeersForPublish = combineConverters(replaceWorkspaceProtocolPeerDependency, replaceCatalogProtocol)
|
||||
publishManifest.peerDependencies = await makePublishDependencies(dir, peerDependencies, {
|
||||
modulesDir: opts?.modulesDir,
|
||||
convertDependencyForPublish: convertPeersForPublish,
|
||||
})
|
||||
}
|
||||
|
||||
overridePublishConfig(publishManifest)
|
||||
@@ -50,17 +65,37 @@ export async function createExportableManifest (
|
||||
return publishManifest
|
||||
}
|
||||
|
||||
export type PublishDependencyConverter = (
|
||||
depName: string,
|
||||
depSpec: string,
|
||||
dir: string,
|
||||
modulesDir?: string
|
||||
) => Promise<string> | string
|
||||
|
||||
function combineConverters (...converters: readonly PublishDependencyConverter[]): PublishDependencyConverter {
|
||||
return async (depName, depSpec, dir, modulesDir) => {
|
||||
let pref = depSpec
|
||||
for (const converter of converters) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
pref = await converter(depName, pref, dir, modulesDir)
|
||||
}
|
||||
return pref
|
||||
}
|
||||
}
|
||||
|
||||
export interface MakePublishDependenciesOpts {
|
||||
readonly modulesDir?: string
|
||||
readonly convertDependencyForPublish: PublishDependencyConverter
|
||||
}
|
||||
|
||||
async function makePublishDependencies (
|
||||
dir: string,
|
||||
dependencies: Dependencies | undefined,
|
||||
{ modulesDir, convertDependencyForPublish = makePublishDependency }: {
|
||||
modulesDir?: string
|
||||
convertDependencyForPublish?: (depName: string, depSpec: string, dir: string, modulesDir?: string) => Promise<string>
|
||||
} = {}
|
||||
{ modulesDir, convertDependencyForPublish }: MakePublishDependenciesOpts
|
||||
): Promise<Dependencies | undefined> {
|
||||
if (dependencies == null) return dependencies
|
||||
const publishDependencies = await pMapValues(
|
||||
(depSpec, depName) => convertDependencyForPublish(depName, depSpec, dir, modulesDir),
|
||||
async (depSpec, depName) => convertDependencyForPublish(depName, depSpec, dir, modulesDir),
|
||||
dependencies
|
||||
)
|
||||
return publishDependencies
|
||||
@@ -79,7 +114,17 @@ async function resolveManifest (depName: string, modulesDir: string): Promise<Pr
|
||||
return manifest
|
||||
}
|
||||
|
||||
async function makePublishDependency (depName: string, depSpec: string, dir: string, modulesDir?: string): Promise<string> {
|
||||
function resolveCatalogProtocol (catalogResolver: CatalogResolver, alias: string, pref: string): string {
|
||||
const result = catalogResolver({ alias, pref })
|
||||
|
||||
switch (result.type) {
|
||||
case 'found': return result.resolution.specifier
|
||||
case 'unused': return pref
|
||||
case 'misconfiguration': throw result.error
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceWorkspaceProtocol (depName: string, depSpec: string, dir: string, modulesDir?: string): Promise<string> {
|
||||
if (!depSpec.startsWith('workspace:')) {
|
||||
return depSpec
|
||||
}
|
||||
@@ -109,7 +154,7 @@ async function makePublishDependency (depName: string, depSpec: string, dir: str
|
||||
return depSpec
|
||||
}
|
||||
|
||||
async function makePublishPeerDependency (depName: string, depSpec: string, dir: string, modulesDir?: string) {
|
||||
async function replaceWorkspaceProtocolPeerDependency (depName: string, depSpec: string, dir: string, modulesDir?: string) {
|
||||
if (!depSpec.includes('workspace:')) {
|
||||
return depSpec
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/// <reference path="../../../__typings__/index.d.ts"/>
|
||||
import { createExportableManifest } from '@pnpm/exportable-manifest'
|
||||
import { getCatalogsFromWorkspaceManifest } from '@pnpm/catalogs.config'
|
||||
import { type MakePublishManifestOptions, createExportableManifest } from '@pnpm/exportable-manifest'
|
||||
import { preparePackages } from '@pnpm/prepare'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
import { type ProjectManifest } from '@pnpm/types'
|
||||
@@ -8,6 +9,10 @@ import path from 'path'
|
||||
|
||||
const pnpmBin = path.join(__dirname, '../../../pnpm/bin/pnpm.cjs')
|
||||
|
||||
const defaultOpts: MakePublishManifestOptions = {
|
||||
catalogs: {},
|
||||
}
|
||||
|
||||
test('the pnpm options are removed', async () => {
|
||||
expect(await createExportableManifest(process.cwd(), {
|
||||
name: 'foo',
|
||||
@@ -20,7 +25,7 @@ test('the pnpm options are removed', async () => {
|
||||
bar: '1',
|
||||
},
|
||||
},
|
||||
})).toStrictEqual({
|
||||
}, defaultOpts)).toStrictEqual({
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
@@ -37,7 +42,7 @@ test('the packageManager field is removed', async () => {
|
||||
qar: '2',
|
||||
},
|
||||
packageManager: 'pnpm@8.0.0',
|
||||
})).toStrictEqual({
|
||||
}, defaultOpts)).toStrictEqual({
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
@@ -60,7 +65,7 @@ test('publish lifecycle scripts are removed', async () => {
|
||||
postinstall: 'echo',
|
||||
test: 'echo',
|
||||
},
|
||||
})).toStrictEqual({
|
||||
}, defaultOpts)).toStrictEqual({
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
@@ -74,7 +79,7 @@ test('readme added to published manifest', async () => {
|
||||
expect(await createExportableManifest(process.cwd(), {
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
}, { readmeFile: 'readme content' })).toStrictEqual({
|
||||
}, { ...defaultOpts, readmeFile: 'readme content' })).toStrictEqual({
|
||||
name: 'foo',
|
||||
version: '1.0.0',
|
||||
readme: 'readme content',
|
||||
@@ -119,7 +124,7 @@ test('workspace deps are replaced', async () => {
|
||||
|
||||
process.chdir('workspace-protocol-package')
|
||||
|
||||
expect(await createExportableManifest(process.cwd(), workspaceProtocolPackageManifest)).toStrictEqual({
|
||||
expect(await createExportableManifest(process.cwd(), workspaceProtocolPackageManifest, defaultOpts)).toStrictEqual({
|
||||
name: 'workspace-protocol-package',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
@@ -133,3 +138,57 @@ test('workspace deps are replaced', async () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('catalog deps are replace', async () => {
|
||||
const catalogProtocolPackageManifest: ProjectManifest = {
|
||||
name: 'catalog-protocol-package',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
bar: 'catalog:',
|
||||
},
|
||||
optionalDependencies: {
|
||||
baz: 'catalog:baz',
|
||||
},
|
||||
peerDependencies: {
|
||||
foo: 'catalog:foo',
|
||||
},
|
||||
}
|
||||
|
||||
preparePackages([catalogProtocolPackageManifest])
|
||||
|
||||
const workspaceManifest = {
|
||||
packages: ['**', '!store/**'],
|
||||
catalog: {
|
||||
bar: '^1.2.3',
|
||||
},
|
||||
catalogs: {
|
||||
foo: {
|
||||
foo: '^1.2.4',
|
||||
},
|
||||
baz: {
|
||||
baz: '^1.2.5',
|
||||
},
|
||||
},
|
||||
}
|
||||
writeYamlFile('pnpm-workspace.yaml', workspaceManifest)
|
||||
|
||||
crossSpawn.sync(pnpmBin, ['install', '--store-dir=store'])
|
||||
|
||||
process.chdir('catalog-protocol-package')
|
||||
|
||||
const catalogs = getCatalogsFromWorkspaceManifest(workspaceManifest)
|
||||
expect(await createExportableManifest(process.cwd(), catalogProtocolPackageManifest, { catalogs })).toStrictEqual({
|
||||
name: 'catalog-protocol-package',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
bar: '^1.2.3',
|
||||
},
|
||||
optionalDependencies: {
|
||||
baz: '^1.2.5',
|
||||
},
|
||||
peerDependencies: {
|
||||
foo: '^1.2.4',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
{
|
||||
"path": "../../__utils__/prepare"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/config"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/resolver"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/types"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
|
||||
80
pnpm-lock.yaml
generated
80
pnpm-lock.yaml
generated
@@ -365,6 +365,50 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
catalogs/config:
|
||||
dependencies:
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/error
|
||||
devDependencies:
|
||||
'@pnpm/catalogs.config':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: link:../types
|
||||
'@pnpm/workspace.read-manifest':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/read-manifest
|
||||
|
||||
catalogs/protocol-parser:
|
||||
devDependencies:
|
||||
'@pnpm/catalogs.protocol-parser':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
catalogs/resolver:
|
||||
dependencies:
|
||||
'@pnpm/catalogs.protocol-parser':
|
||||
specifier: workspace:^
|
||||
version: link:../protocol-parser
|
||||
'@pnpm/error':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/error
|
||||
devDependencies:
|
||||
'@pnpm/catalogs.resolver':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: link:../types
|
||||
|
||||
catalogs/types:
|
||||
devDependencies:
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
cli/cli-meta:
|
||||
dependencies:
|
||||
'@pnpm/types':
|
||||
@@ -598,6 +642,12 @@ importers:
|
||||
|
||||
config/config:
|
||||
dependencies:
|
||||
'@pnpm/catalogs.config':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/config
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/types
|
||||
'@pnpm/config.env-replace':
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
@@ -3084,6 +3134,12 @@ importers:
|
||||
'@pnpm/calc-dep-state':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/calc-dep-state
|
||||
'@pnpm/catalogs.protocol-parser':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/protocol-parser
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/types
|
||||
'@pnpm/constants':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/constants
|
||||
@@ -3295,6 +3351,9 @@ importers:
|
||||
'@pnpm/test-ipc-server':
|
||||
specifier: workspace:*
|
||||
version: link:../../__utils__/test-ipc-server
|
||||
'@pnpm/workspace.find-packages':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/find-packages
|
||||
'@types/fs-extra':
|
||||
specifier: ^9.0.13
|
||||
version: 9.0.13
|
||||
@@ -4271,6 +4330,12 @@ importers:
|
||||
|
||||
pkg-manager/resolve-dependencies:
|
||||
dependencies:
|
||||
'@pnpm/catalogs.resolver':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/resolver
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/types
|
||||
'@pnpm/constants':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/constants
|
||||
@@ -4392,6 +4457,9 @@ importers:
|
||||
|
||||
pkg-manifest/exportable-manifest:
|
||||
dependencies:
|
||||
'@pnpm/catalogs.resolver':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/resolver
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/error
|
||||
@@ -4408,6 +4476,12 @@ importers:
|
||||
specifier: npm:@pnpm/ramda@0.28.1
|
||||
version: '@pnpm/ramda@0.28.1'
|
||||
devDependencies:
|
||||
'@pnpm/catalogs.config':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/config
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/types
|
||||
'@pnpm/exportable-manifest':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
@@ -4962,6 +5036,9 @@ importers:
|
||||
|
||||
releasing/plugin-commands-publishing:
|
||||
dependencies:
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/types
|
||||
'@pnpm/cli-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../cli/cli-utils
|
||||
@@ -5047,6 +5124,9 @@ importers:
|
||||
specifier: ^4.3.0
|
||||
version: 4.3.0
|
||||
devDependencies:
|
||||
'@pnpm/catalogs.config':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/config
|
||||
'@pnpm/plugin-commands-publishing':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
@@ -3,6 +3,7 @@ packages:
|
||||
- __typings__
|
||||
- __utils__/*
|
||||
- "!__utils__/build-artifacts"
|
||||
- catalogs/*
|
||||
- cli/*
|
||||
- completion/*
|
||||
- config/*
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/releasing/plugin-commands-publishing#readme",
|
||||
"devDependencies": {
|
||||
"@pnpm/catalogs.config": "workspace:*",
|
||||
"@pnpm/plugin-commands-publishing": "workspace:*",
|
||||
"@pnpm/prepare": "workspace:*",
|
||||
"@pnpm/registry-mock": "3.32.1",
|
||||
@@ -53,6 +54,7 @@
|
||||
"write-yaml-file": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/cli-utils": "workspace:*",
|
||||
"@pnpm/client": "workspace:*",
|
||||
"@pnpm/common-cli-options-help": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { createGzip } from 'zlib'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { types as allTypes, type UniversalOptions, type Config } from '@pnpm/config'
|
||||
import { readProjectManifest } from '@pnpm/cli-utils'
|
||||
@@ -58,7 +59,7 @@ export function help (): string {
|
||||
}
|
||||
|
||||
export async function handler (
|
||||
opts: Pick<UniversalOptions, 'dir'> & Pick<Config, 'ignoreScripts' | 'rawConfig' | 'embedReadme' | 'packGzipLevel' | 'nodeLinker'> & Partial<Pick<Config, 'extraBinPaths' | 'extraEnv'>> & {
|
||||
opts: Pick<UniversalOptions, 'dir'> & Pick<Config, 'catalogs' | 'ignoreScripts' | 'rawConfig' | 'embedReadme' | 'packGzipLevel' | 'nodeLinker'> & Partial<Pick<Config, 'extraBinPaths' | 'extraEnv'>> & {
|
||||
argv: {
|
||||
original: string[]
|
||||
}
|
||||
@@ -103,6 +104,7 @@ export async function handler (
|
||||
modulesDir: path.join(opts.dir, 'node_modules'),
|
||||
manifest,
|
||||
embedReadme: opts.embedReadme,
|
||||
catalogs: opts.catalogs ?? {},
|
||||
})
|
||||
const files = await packlist(dir, {
|
||||
packageJsonCache: {
|
||||
@@ -202,8 +204,13 @@ async function createPublishManifest (opts: {
|
||||
embedReadme?: boolean
|
||||
modulesDir: string
|
||||
manifest: ProjectManifest
|
||||
catalogs: Catalogs
|
||||
}): Promise<ProjectManifest> {
|
||||
const { projectDir, embedReadme, modulesDir, manifest } = opts
|
||||
const { projectDir, embedReadme, modulesDir, manifest, catalogs } = opts
|
||||
const readmeFile = embedReadme ? await readReadmeFile(projectDir) : undefined
|
||||
return createExportableManifest(projectDir, manifest, { readmeFile, modulesDir })
|
||||
return createExportableManifest(projectDir, manifest, {
|
||||
catalogs,
|
||||
readmeFile,
|
||||
modulesDir,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export type PublishRecursiveOpts = Required<Pick<Config,
|
||||
Partial<Pick<Config,
|
||||
| 'tag'
|
||||
| 'ca'
|
||||
| 'catalogs'
|
||||
| 'cert'
|
||||
| 'fetchTimeout'
|
||||
| 'force'
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from 'path'
|
||||
import execa from 'execa'
|
||||
import { isCI } from 'ci-info'
|
||||
import isWindows from 'is-windows'
|
||||
import { getCatalogsFromWorkspaceManifest } from '@pnpm/catalogs.config'
|
||||
import { pack, publish } from '@pnpm/plugin-commands-publishing'
|
||||
import { prepare, preparePackages } from '@pnpm/prepare'
|
||||
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
|
||||
@@ -613,6 +614,127 @@ test.skip('convert specs with relative workspace protocols to regular version ra
|
||||
})
|
||||
})
|
||||
|
||||
describe('catalog protocol converted when publishing', () => {
|
||||
test('default catalog', async () => {
|
||||
const testPackageName = 'workspace-package-with-default-catalog'
|
||||
preparePackages([
|
||||
{
|
||||
name: testPackageName,
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
devDependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
optionalDependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
peerDependencies: {
|
||||
'is-positive': 'catalog:',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'target',
|
||||
private: true,
|
||||
},
|
||||
])
|
||||
|
||||
const workspaceManifest = {
|
||||
packages: ['**', '!store/**'],
|
||||
catalog: { 'is-positive': '1.0.0' },
|
||||
}
|
||||
writeYamlFile('pnpm-workspace.yaml', workspaceManifest)
|
||||
|
||||
process.chdir(testPackageName)
|
||||
|
||||
await publish.handler({
|
||||
...DEFAULT_OPTS,
|
||||
argv: { original: ['publish', ...CREDENTIALS] },
|
||||
catalogs: getCatalogsFromWorkspaceManifest(workspaceManifest),
|
||||
dir: process.cwd(),
|
||||
}, [])
|
||||
|
||||
process.chdir('../target')
|
||||
|
||||
crossSpawn.sync(pnpmBin, [
|
||||
'add',
|
||||
'--store-dir=../store',
|
||||
testPackageName,
|
||||
'--no-link-workspace-packages',
|
||||
`--registry=http://localhost:${REGISTRY_MOCK_PORT}`,
|
||||
])
|
||||
|
||||
const { default: publishedManifest } = await import(path.resolve(`node_modules/${testPackageName}/package.json`))
|
||||
expect(publishedManifest.dependencies).toStrictEqual({ 'is-positive': '1.0.0' })
|
||||
expect(publishedManifest.devDependencies).toStrictEqual({ 'is-positive': '1.0.0' })
|
||||
expect(publishedManifest.optionalDependencies).toStrictEqual({ 'is-positive': '1.0.0' })
|
||||
expect(publishedManifest.peerDependencies).toStrictEqual({ 'is-positive': '1.0.0' })
|
||||
})
|
||||
|
||||
test('named catalog', async () => {
|
||||
const testPackageName = 'workspace-package-with-named-catalog'
|
||||
preparePackages([
|
||||
{
|
||||
name: testPackageName,
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'is-positive': 'catalog:foo',
|
||||
},
|
||||
devDependencies: {
|
||||
'is-positive': 'catalog:bar',
|
||||
},
|
||||
optionalDependencies: {
|
||||
'is-positive': 'catalog:baz',
|
||||
},
|
||||
peerDependencies: {
|
||||
'is-positive': 'catalog:qux',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'target',
|
||||
private: true,
|
||||
},
|
||||
])
|
||||
|
||||
const workspaceManifest = {
|
||||
packages: ['**', '!store/**'],
|
||||
catalogs: {
|
||||
foo: { 'is-positive': '1.0.0' },
|
||||
bar: { 'is-positive': '1.0.0' },
|
||||
baz: { 'is-positive': '1.0.0' },
|
||||
qux: { 'is-positive': '1.0.0' },
|
||||
},
|
||||
}
|
||||
writeYamlFile('pnpm-workspace.yaml', workspaceManifest)
|
||||
|
||||
process.chdir(testPackageName)
|
||||
|
||||
await publish.handler({
|
||||
...DEFAULT_OPTS,
|
||||
argv: { original: ['publish', ...CREDENTIALS] },
|
||||
catalogs: getCatalogsFromWorkspaceManifest(workspaceManifest),
|
||||
dir: process.cwd(),
|
||||
}, [])
|
||||
|
||||
process.chdir('../target')
|
||||
|
||||
crossSpawn.sync(pnpmBin, [
|
||||
'add',
|
||||
'--store-dir=../store',
|
||||
testPackageName,
|
||||
'--no-link-workspace-packages',
|
||||
`--registry=http://localhost:${REGISTRY_MOCK_PORT}`,
|
||||
])
|
||||
|
||||
const { default: publishedManifest } = await import(path.resolve(`node_modules/${testPackageName}/package.json`))
|
||||
expect(publishedManifest.dependencies).toStrictEqual({ 'is-positive': '1.0.0' })
|
||||
expect(publishedManifest.devDependencies).toStrictEqual({ 'is-positive': '1.0.0' })
|
||||
expect(publishedManifest.optionalDependencies).toStrictEqual({ 'is-positive': '1.0.0' })
|
||||
expect(publishedManifest.peerDependencies).toStrictEqual({ 'is-positive': '1.0.0' })
|
||||
})
|
||||
})
|
||||
|
||||
test('publish: runs all the lifecycle scripts', async () => {
|
||||
await using server = await createTestIpcServer()
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
{
|
||||
"path": "../../__utils__/test-ipc-server"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/config"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/types"
|
||||
},
|
||||
{
|
||||
"path": "../../cli/cli-utils"
|
||||
},
|
||||
|
||||
59
workspace/read-manifest/src/catalogs.ts
Normal file
59
workspace/read-manifest/src/catalogs.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { InvalidWorkspaceManifestError } from './errors/InvalidWorkspaceManifestError'
|
||||
|
||||
export interface WorkspaceNamedCatalogs {
|
||||
readonly [catalogName: string]: WorkspaceCatalog
|
||||
}
|
||||
|
||||
export interface WorkspaceCatalog {
|
||||
readonly [dependencyName: string]: string
|
||||
}
|
||||
|
||||
export function assertValidWorkspaceManifestCatalog (manifest: { packages?: readonly string[], catalog?: unknown }): asserts manifest is { catalog?: WorkspaceCatalog } {
|
||||
if (manifest.catalog == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(manifest.catalog)) {
|
||||
throw new InvalidWorkspaceManifestError('Expected catalog field to be an object, but found - array')
|
||||
}
|
||||
|
||||
if (typeof manifest.catalog !== 'object') {
|
||||
throw new InvalidWorkspaceManifestError(`Expected catalog field to be an object, but found - ${typeof manifest.catalog}`)
|
||||
}
|
||||
|
||||
for (const [alias, specifier] of Object.entries(manifest.catalog)) {
|
||||
if (typeof specifier !== 'string') {
|
||||
throw new InvalidWorkspaceManifestError(`Invalid catalog entry for ${alias}. Expected string, but found: ${typeof specifier}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function assertValidWorkspaceManifestCatalogs (manifest: { packages?: readonly string[], catalogs?: unknown }): asserts manifest is { catalogs?: WorkspaceNamedCatalogs } {
|
||||
if (manifest.catalogs == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(manifest.catalogs)) {
|
||||
throw new InvalidWorkspaceManifestError('Expected catalogs field to be an object, but found - array')
|
||||
}
|
||||
|
||||
if (typeof manifest.catalogs !== 'object') {
|
||||
throw new InvalidWorkspaceManifestError(`Expected catalogs field to be an object, but found - ${typeof manifest.catalogs}`)
|
||||
}
|
||||
|
||||
for (const [catalogName, catalog] of Object.entries(manifest.catalogs)) {
|
||||
if (Array.isArray(catalog)) {
|
||||
throw new InvalidWorkspaceManifestError(`Expected named catalog ${catalogName} to be an object, but found - array`)
|
||||
}
|
||||
|
||||
if (typeof catalog !== 'object') {
|
||||
throw new InvalidWorkspaceManifestError(`Expected named catalog ${catalogName} to be an object, but found - ${typeof catalog}`)
|
||||
}
|
||||
|
||||
for (const [alias, specifier] of Object.entries(catalog)) {
|
||||
if (typeof specifier !== 'string') {
|
||||
throw new InvalidWorkspaceManifestError(`Catalog '${catalogName}' has invalid entry '${alias}'. Expected string specifier, but found: ${typeof specifier}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
|
||||
export class InvalidWorkspaceManifestError extends PnpmError {
|
||||
constructor (message: string) {
|
||||
super('INVALID_WORKSPACE_CONFIGURATION', message)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,30 @@
|
||||
import util from 'util'
|
||||
import { WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import path from 'node:path'
|
||||
import readYamlFile from 'read-yaml-file'
|
||||
import {
|
||||
assertValidWorkspaceManifestCatalog,
|
||||
assertValidWorkspaceManifestCatalogs,
|
||||
type WorkspaceCatalog,
|
||||
type WorkspaceNamedCatalogs,
|
||||
} from './catalogs'
|
||||
import { InvalidWorkspaceManifestError } from './errors/InvalidWorkspaceManifestError'
|
||||
|
||||
export interface WorkspaceManifest {
|
||||
packages: string[]
|
||||
|
||||
/**
|
||||
* The default catalog. Package manifests may refer to dependencies in this
|
||||
* definition through the `catalog:default` specifier or the `catalog:`
|
||||
* shorthand.
|
||||
*/
|
||||
catalog?: WorkspaceCatalog
|
||||
|
||||
/**
|
||||
* A dictionary of named catalogs. Package manifests may refer to dependencies
|
||||
* in this definition through the `catalog:<name>` specifier.
|
||||
*/
|
||||
catalogs?: WorkspaceNamedCatalogs
|
||||
}
|
||||
|
||||
export async function readWorkspaceManifest (dir: string): Promise<WorkspaceManifest | undefined> {
|
||||
@@ -48,6 +67,9 @@ function validateWorkspaceManifest (manifest: unknown): asserts manifest is Work
|
||||
}
|
||||
|
||||
assertValidWorkspaceManifestPackages(manifest)
|
||||
assertValidWorkspaceManifestCatalog(manifest)
|
||||
assertValidWorkspaceManifestCatalogs(manifest)
|
||||
|
||||
checkWorkspaceManifestAssignability(manifest)
|
||||
}
|
||||
|
||||
@@ -79,9 +101,3 @@ function assertValidWorkspaceManifestPackages (manifest: { packages?: unknown })
|
||||
* the future.
|
||||
*/
|
||||
function checkWorkspaceManifestAssignability (_manifest: WorkspaceManifest): void {}
|
||||
|
||||
class InvalidWorkspaceManifestError extends PnpmError {
|
||||
constructor (message: string) {
|
||||
super('INVALID_WORKSPACE_CONFIGURATION', message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalog:
|
||||
- test
|
||||
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalog: 5
|
||||
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalog:
|
||||
foo: {}
|
||||
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalog:
|
||||
foo: ^1.0.0
|
||||
@@ -0,0 +1,10 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalog:
|
||||
foo: ^1.0.0
|
||||
|
||||
catalogs:
|
||||
default:
|
||||
bar: ^2.0.0
|
||||
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalogs:
|
||||
- foo
|
||||
@@ -0,0 +1,7 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalogs:
|
||||
foo:
|
||||
- bar
|
||||
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalogs:
|
||||
foo: 92
|
||||
@@ -0,0 +1,7 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalogs:
|
||||
foo:
|
||||
bar: {}
|
||||
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalogs: 5
|
||||
@@ -0,0 +1,10 @@
|
||||
packages:
|
||||
- "packages/**"
|
||||
- "types"
|
||||
|
||||
catalog:
|
||||
bar: ^1.0.0
|
||||
|
||||
catalogs:
|
||||
foo:
|
||||
bar: ^2.0.0
|
||||
@@ -61,4 +61,95 @@ test('readWorkspaceManifest() works when workspace file is null', async () => {
|
||||
const manifest = await readWorkspaceManifest(path.join(__dirname, '__fixtures__/null'))
|
||||
|
||||
expect(manifest).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('readWorkspaceManifest() catalog field', () => {
|
||||
test('works on simple catalog', async () => {
|
||||
await expect(readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalog-ok'))).resolves.toEqual({
|
||||
packages: ['packages/**', 'types'],
|
||||
catalog: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('throws on invalid array', async () => {
|
||||
await expect(
|
||||
readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalog-invalid-array'))
|
||||
).rejects.toThrow('Expected catalog field to be an object, but found - array')
|
||||
})
|
||||
|
||||
test('throws on invalid object', async () => {
|
||||
await expect(
|
||||
readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalog-invalid-object'))
|
||||
).rejects.toThrow('Expected catalog field to be an object, but found - number')
|
||||
})
|
||||
|
||||
test('throws on invalid specifier', async () => {
|
||||
await expect(
|
||||
readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalog-invalid-specifier'))
|
||||
).rejects.toThrow('Invalid catalog entry for foo. Expected string, but found: object')
|
||||
})
|
||||
})
|
||||
|
||||
describe('readWorkspaceManifest() catalogs field', () => {
|
||||
test('works with simple named catalogs', async () => {
|
||||
await expect(readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalogs-ok'))).resolves.toEqual({
|
||||
packages: ['packages/**', 'types'],
|
||||
catalog: {
|
||||
bar: '^1.0.0',
|
||||
},
|
||||
catalogs: {
|
||||
foo: {
|
||||
bar: '^2.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('throws on invalid array', async () => {
|
||||
await expect(
|
||||
readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalogs-invalid-array'))
|
||||
).rejects.toThrow('Expected catalogs field to be an object, but found - array')
|
||||
})
|
||||
|
||||
test('throws on invalid value', async () => {
|
||||
await expect(
|
||||
readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalogs-invalid-object'))
|
||||
).rejects.toThrow('Expected catalogs field to be an object, but found - number')
|
||||
})
|
||||
|
||||
test('throws on invalid named catalog array', async () => {
|
||||
await expect(
|
||||
readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalogs-invalid-named-catalog-array'))
|
||||
).rejects.toThrow('Expected named catalog foo to be an object, but found - array')
|
||||
})
|
||||
|
||||
test('throws on invalid named catalog object', async () => {
|
||||
await expect(
|
||||
readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalogs-invalid-named-catalog-object'))
|
||||
).rejects.toThrow('Expected named catalog foo to be an object, but found - number')
|
||||
})
|
||||
|
||||
test('throws on invalid named catalog specifier', async () => {
|
||||
await expect(
|
||||
readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalogs-invalid-named-catalog-specifier'))
|
||||
).rejects.toThrow('Catalog \'foo\' has invalid entry \'bar\'. Expected string specifier, but found: object')
|
||||
})
|
||||
})
|
||||
|
||||
describe('readWorkspaceManifest() reads default catalog defined alongside named catalogs', () => {
|
||||
test('works when implicit default catalog is configured alongside named catalogs', async () => {
|
||||
await expect(readWorkspaceManifest(path.join(__dirname, '__fixtures__/catalogs-ok'))).resolves.toEqual({
|
||||
packages: ['packages/**', 'types'],
|
||||
catalog: {
|
||||
bar: '^1.0.0',
|
||||
},
|
||||
catalogs: {
|
||||
foo: {
|
||||
bar: '^2.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user