mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat: pnpm add option to add new entries to catalogs (#9484)
close #9425 --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
11
.changeset/tangy-nails-hide.md
Normal file
11
.changeset/tangy-nails-hide.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-installation": minor
|
||||
"@pnpm/resolve-dependencies": minor
|
||||
"@pnpm/workspace.read-manifest": minor
|
||||
"@pnpm/workspace.manifest-writer": minor
|
||||
"@pnpm/core": minor
|
||||
"@pnpm/config": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Added two new CLI options (`--save-catalog` and `--save-catalog-name=<name>`) to `pnpm add` to save new dependencies as catalog entries. `catalog:` or `catalog:<name>` will be added to `package.json` and the package specifier will be added to the `catalogs` or `catalog[<name>]` object in `pnpm-workspace.yaml` [#9425](https://github.com/pnpm/pnpm/issues/9425).
|
||||
@@ -52,6 +52,7 @@ export interface Config extends OptionsFromRootManifest {
|
||||
saveDev?: boolean
|
||||
saveOptional?: boolean
|
||||
savePeer?: boolean
|
||||
saveCatalogName?: string
|
||||
saveWorkspaceProtocol?: boolean | 'rolling'
|
||||
lockfileIncludeTarballUrl?: boolean
|
||||
scriptShell?: string
|
||||
|
||||
@@ -175,6 +175,7 @@ export async function getConfig (opts: {
|
||||
'resolution-mode': 'highest',
|
||||
'resolve-peers-from-workspace-root': true,
|
||||
'save-peer': false,
|
||||
'save-catalog-name': undefined,
|
||||
'save-workspace-protocol': 'rolling',
|
||||
'scripts-prepend-node-path': false,
|
||||
'strict-dep-builds': false,
|
||||
|
||||
@@ -88,6 +88,7 @@ export const types = Object.assign({
|
||||
'aggregate-output': Boolean,
|
||||
'reporter-hide-prefix': Boolean,
|
||||
'save-peer': Boolean,
|
||||
'save-catalog-name': String,
|
||||
'save-workspace-protocol': Boolean,
|
||||
'script-shell': String,
|
||||
'shamefully-flatten': Boolean,
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface StrictInstallOptions {
|
||||
lockfileIncludeTarballUrl: boolean
|
||||
preferWorkspacePackages: boolean
|
||||
preserveWorkspaceProtocol: boolean
|
||||
saveCatalogName?: string
|
||||
scriptsPrependNodePath: boolean | 'warn-only'
|
||||
scriptShell?: string
|
||||
shellEmulator: boolean
|
||||
|
||||
@@ -141,12 +141,18 @@ type Opts = Omit<InstallOptions, 'allProjects'> & {
|
||||
binsDir?: string
|
||||
} & InstallMutationOptions
|
||||
|
||||
export interface InstallResult {
|
||||
newCatalogs: CatalogSnapshots | undefined
|
||||
updatedManifest: ProjectManifest
|
||||
ignoredBuilds: string[] | undefined
|
||||
}
|
||||
|
||||
export async function install (
|
||||
manifest: ProjectManifest,
|
||||
opts: Opts
|
||||
): Promise<{ updatedManifest: ProjectManifest, ignoredBuilds: string[] | undefined }> {
|
||||
): Promise<InstallResult> {
|
||||
const rootDir = (opts.dir ?? process.cwd()) as ProjectRootDir
|
||||
const { updatedProjects: projects, ignoredBuilds } = await mutateModules(
|
||||
const { newCatalogs, updatedProjects: projects, ignoredBuilds } = await mutateModules(
|
||||
[
|
||||
{
|
||||
mutation: 'install',
|
||||
@@ -168,7 +174,7 @@ export async function install (
|
||||
}],
|
||||
}
|
||||
)
|
||||
return { updatedManifest: projects[0].manifest, ignoredBuilds }
|
||||
return { newCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds }
|
||||
}
|
||||
|
||||
interface ProjectToBeInstalled {
|
||||
@@ -188,6 +194,12 @@ export type MutateModulesOptions = InstallOptions & {
|
||||
} | InstallOptions['hooks']
|
||||
}
|
||||
|
||||
export interface MutateModulesInSingleProjectResult {
|
||||
newCatalogs: CatalogSnapshots | undefined
|
||||
updatedProject: UpdatedProject
|
||||
ignoredBuilds: string[] | undefined
|
||||
}
|
||||
|
||||
export async function mutateModulesInSingleProject (
|
||||
project: MutatedProject & {
|
||||
binsDir?: string
|
||||
@@ -196,7 +208,7 @@ export async function mutateModulesInSingleProject (
|
||||
modulesDir?: string
|
||||
},
|
||||
maybeOpts: Omit<MutateModulesOptions, 'allProjects'> & InstallMutationOptions
|
||||
): Promise<{ updatedProject: UpdatedProject, ignoredBuilds: string[] | undefined }> {
|
||||
): Promise<MutateModulesInSingleProjectResult> {
|
||||
const result = await mutateModules(
|
||||
[
|
||||
{
|
||||
@@ -215,10 +227,15 @@ export async function mutateModulesInSingleProject (
|
||||
}],
|
||||
}
|
||||
)
|
||||
return { updatedProject: result.updatedProjects[0], ignoredBuilds: result.ignoredBuilds }
|
||||
return {
|
||||
newCatalogs: result.newCatalogs,
|
||||
updatedProject: result.updatedProjects[0],
|
||||
ignoredBuilds: result.ignoredBuilds,
|
||||
}
|
||||
}
|
||||
|
||||
export interface MutateModulesResult {
|
||||
newCatalogs?: CatalogSnapshots
|
||||
updatedProjects: UpdatedProject[]
|
||||
stats: InstallationResultStats
|
||||
depsRequiringBuild?: DepPath[]
|
||||
@@ -315,6 +332,7 @@ export async function mutateModules (
|
||||
}
|
||||
|
||||
return {
|
||||
newCatalogs: result.newCatalogs,
|
||||
updatedProjects: result.updatedProjects,
|
||||
stats: result.stats ?? { added: 0, removed: 0, linkedToRoot: 0 },
|
||||
depsRequiringBuild: result.depsRequiringBuild,
|
||||
@@ -322,6 +340,7 @@ export async function mutateModules (
|
||||
}
|
||||
|
||||
interface InnerInstallResult {
|
||||
readonly newCatalogs?: CatalogSnapshots
|
||||
readonly updatedProjects: UpdatedProject[]
|
||||
readonly stats?: InstallationResultStats
|
||||
readonly depsRequiringBuild?: DepPath[]
|
||||
@@ -520,6 +539,7 @@ export async function mutateModules (
|
||||
optionalDependencies,
|
||||
updateWorkspaceDependencies: project.update,
|
||||
preferredSpecs,
|
||||
saveCatalogName: opts.saveCatalogName,
|
||||
overrides: opts.overrides,
|
||||
defaultCatalog: opts.catalogs?.default,
|
||||
})
|
||||
@@ -548,6 +568,7 @@ export async function mutateModules (
|
||||
})
|
||||
|
||||
return {
|
||||
newCatalogs: result.newCatalogs,
|
||||
updatedProjects: result.projects,
|
||||
stats: result.stats,
|
||||
depsRequiringBuild: result.depsRequiringBuild,
|
||||
@@ -854,9 +875,9 @@ export async function addDependenciesToPackage (
|
||||
pinnedVersion?: 'major' | 'minor' | 'patch'
|
||||
targetDependenciesField?: DependenciesField
|
||||
} & InstallMutationOptions
|
||||
): Promise<{ updatedManifest: ProjectManifest, ignoredBuilds: string[] | undefined }> {
|
||||
): Promise<InstallResult> {
|
||||
const rootDir = (opts.dir ?? process.cwd()) as ProjectRootDir
|
||||
const { updatedProjects: projects, ignoredBuilds } = await mutateModules(
|
||||
const { newCatalogs, updatedProjects: projects, ignoredBuilds } = await mutateModules(
|
||||
[
|
||||
{
|
||||
allowNew: opts.allowNew,
|
||||
@@ -884,7 +905,7 @@ export async function addDependenciesToPackage (
|
||||
},
|
||||
],
|
||||
})
|
||||
return { updatedManifest: projects[0].manifest, ignoredBuilds }
|
||||
return { newCatalogs, updatedManifest: projects[0].manifest, ignoredBuilds }
|
||||
}
|
||||
|
||||
export type ImporterToUpdate = {
|
||||
@@ -909,6 +930,7 @@ export interface UpdatedProject {
|
||||
}
|
||||
|
||||
interface InstallFunctionResult {
|
||||
newCatalogs?: CatalogSnapshots
|
||||
newLockfile: LockfileObject
|
||||
projects: UpdatedProject[]
|
||||
stats?: InstallationResultStats
|
||||
@@ -1023,6 +1045,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
dependenciesGraph,
|
||||
dependenciesByProjectId,
|
||||
linkedDependenciesByProjectId,
|
||||
newCatalogs,
|
||||
newLockfile,
|
||||
outdatedDependencies,
|
||||
peerDependencyIssuesByProjects,
|
||||
@@ -1417,6 +1440,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
}
|
||||
|
||||
return {
|
||||
newCatalogs,
|
||||
newLockfile,
|
||||
projects: projects.map(({ id, manifest, rootDir }) => ({
|
||||
manifest,
|
||||
|
||||
@@ -16,6 +16,7 @@ export function parseWantedDependencies (
|
||||
overrides?: Record<string, string>
|
||||
updateWorkspaceDependencies?: boolean
|
||||
preferredSpecs?: Record<string, string>
|
||||
saveCatalogName?: string
|
||||
defaultCatalog?: Catalog
|
||||
}
|
||||
): WantedDependency[] {
|
||||
@@ -43,7 +44,8 @@ export function parseWantedDependencies (
|
||||
dev: Boolean(opts.dev || alias && !!opts.devDependencies[alias]),
|
||||
optional: Boolean(opts.optional || alias && !!opts.optionalDependencies[alias]),
|
||||
prevSpecifier: alias && opts.currentBareSpecifiers[alias],
|
||||
}
|
||||
saveCatalogName: opts.saveCatalogName,
|
||||
} satisfies Partial<WantedDependency>
|
||||
if (bareSpecifier) {
|
||||
return {
|
||||
...result,
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"@pnpm/store-connection-manager": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"@pnpm/workspace.find-packages": "workspace:*",
|
||||
"@pnpm/workspace.manifest-writer": "workspace:*",
|
||||
"@pnpm/workspace.pkgs-graph": "workspace:*",
|
||||
"@pnpm/workspace.state": "workspace:*",
|
||||
"@pnpm/write-project-manifest": "workspace:*",
|
||||
|
||||
@@ -11,6 +11,10 @@ import { type InstallCommandOptions } from './install'
|
||||
import { installDeps } from './installDeps'
|
||||
import { writeSettings } from '@pnpm/config.config-writer'
|
||||
|
||||
export const shorthands: Record<string, string> = {
|
||||
'save-catalog': '--save-catalog-name=default',
|
||||
}
|
||||
|
||||
export function rcOptionsTypes (): Record<string, unknown> {
|
||||
return pick([
|
||||
'cache-dir',
|
||||
@@ -51,6 +55,7 @@ export function rcOptionsTypes (): Record<string, unknown> {
|
||||
'public-hoist-pattern',
|
||||
'registry',
|
||||
'reporter',
|
||||
'save-catalog-name',
|
||||
'save-dev',
|
||||
'save-exact',
|
||||
'save-optional',
|
||||
@@ -116,6 +121,14 @@ export function help (): string {
|
||||
description: 'Save package to your `peerDependencies` and `devDependencies`',
|
||||
name: '--save-peer',
|
||||
},
|
||||
{
|
||||
description: 'Save package to the default catalog',
|
||||
name: '--save-catalog',
|
||||
},
|
||||
{
|
||||
description: 'Save package to the specified catalog',
|
||||
name: '--save-catalog-name=<name>',
|
||||
},
|
||||
{
|
||||
description: 'Install exact version',
|
||||
name: '--[no-]save-exact',
|
||||
|
||||
@@ -297,6 +297,7 @@ export type InstallCommandOptions = Pick<Config,
|
||||
| 'savePeer'
|
||||
| 'savePrefix'
|
||||
| 'saveProd'
|
||||
| 'saveCatalogName'
|
||||
| 'saveWorkspaceProtocol'
|
||||
| 'lockfileIncludeTarballUrl'
|
||||
| 'allProjectsGraph'
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '@pnpm/core'
|
||||
import { globalInfo, logger } from '@pnpm/logger'
|
||||
import { sequenceGraph } from '@pnpm/sort-packages'
|
||||
import { addCatalogs } from '@pnpm/workspace.manifest-writer'
|
||||
import { createPkgGraph } from '@pnpm/workspace.pkgs-graph'
|
||||
import { updateWorkspaceState, type WorkspaceStateSettings } from '@pnpm/workspace.state'
|
||||
import isSubdir from 'is-subdir'
|
||||
@@ -313,9 +314,12 @@ when running add/update with the --workspace option')
|
||||
rootDir: opts.dir as ProjectRootDir,
|
||||
targetDependenciesField: getSaveType(opts),
|
||||
}
|
||||
const { updatedProject, ignoredBuilds } = await mutateModulesInSingleProject(mutatedProject, installOpts)
|
||||
const { newCatalogs, updatedProject, ignoredBuilds } = await mutateModulesInSingleProject(mutatedProject, installOpts)
|
||||
if (opts.save !== false) {
|
||||
await writeProjectManifest(updatedProject.manifest)
|
||||
await Promise.all([
|
||||
writeProjectManifest(updatedProject.manifest),
|
||||
newCatalogs && addCatalogs(opts.workspaceDir ?? opts.dir, newCatalogs),
|
||||
])
|
||||
}
|
||||
if (!opts.lockfileOnly) {
|
||||
await updateWorkspaceState({
|
||||
@@ -333,9 +337,12 @@ when running add/update with the --workspace option')
|
||||
return
|
||||
}
|
||||
|
||||
const { updatedManifest, ignoredBuilds } = await install(manifest, installOpts)
|
||||
const { newCatalogs, updatedManifest, ignoredBuilds } = await install(manifest, installOpts)
|
||||
if (opts.update === true && opts.save !== false) {
|
||||
await writeProjectManifest(updatedManifest)
|
||||
await Promise.all([
|
||||
writeProjectManifest(updatedManifest),
|
||||
newCatalogs && addCatalogs(opts.workspaceDir ?? opts.dir, newCatalogs),
|
||||
])
|
||||
}
|
||||
if (opts.strictDepBuilds && ignoredBuilds?.length) {
|
||||
throw new IgnoredBuildsError(ignoredBuilds)
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@pnpm/config'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { arrayOfWorkspacePackagesToMap } from '@pnpm/get-context'
|
||||
import { type CatalogSnapshots } from '@pnpm/lockfile.types'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { filterDependenciesByType } from '@pnpm/manifest-utils'
|
||||
import { createMatcherWithIndex } from '@pnpm/matcher'
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
type ProjectRootDir,
|
||||
type ProjectRootDirRealPath,
|
||||
} from '@pnpm/types'
|
||||
import { addCatalogs } from '@pnpm/workspace.manifest-writer'
|
||||
import {
|
||||
addDependenciesToPackage,
|
||||
install,
|
||||
@@ -70,6 +72,7 @@ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
|
||||
| 'rootProjectManifest'
|
||||
| 'rootProjectManifestDir'
|
||||
| 'save'
|
||||
| 'saveCatalogName'
|
||||
| 'saveDev'
|
||||
| 'saveExact'
|
||||
| 'saveOptional'
|
||||
@@ -149,6 +152,7 @@ export async function recursive (
|
||||
pruneLockfileImporters: opts.pruneLockfileImporters ??
|
||||
(((opts.ignoredPackages == null) || opts.ignoredPackages.size === 0) &&
|
||||
pkgs.length === allProjects.length),
|
||||
saveCatalogName: opts.saveCatalogName,
|
||||
storeController: store.ctrl,
|
||||
storeDir: store.dir,
|
||||
targetDependenciesField,
|
||||
@@ -275,17 +279,22 @@ export async function recursive (
|
||||
throw new PnpmError('NO_PACKAGE_IN_DEPENDENCIES',
|
||||
'None of the specified packages were found in the dependencies of any of the projects.')
|
||||
}
|
||||
const { updatedProjects: mutatedPkgs, ignoredBuilds } = await mutateModules(mutatedImporters, {
|
||||
const {
|
||||
newCatalogs,
|
||||
updatedProjects: mutatedPkgs,
|
||||
ignoredBuilds,
|
||||
} = await mutateModules(mutatedImporters, {
|
||||
...installOpts,
|
||||
storeController: store.ctrl,
|
||||
})
|
||||
if (opts.save !== false) {
|
||||
await Promise.all(
|
||||
mutatedPkgs
|
||||
.map(async ({ originalManifest, manifest, rootDir }) => {
|
||||
return manifestsByPath[rootDir].writeProjectManifest(originalManifest ?? manifest)
|
||||
})
|
||||
)
|
||||
const promises: Array<Promise<void>> = mutatedPkgs.map(async ({ originalManifest, manifest, rootDir }) => {
|
||||
return manifestsByPath[rootDir].writeProjectManifest(originalManifest ?? manifest)
|
||||
})
|
||||
if (newCatalogs) {
|
||||
promises.push(addCatalogs(opts.workspaceDir, newCatalogs))
|
||||
}
|
||||
await Promise.all(promises)
|
||||
}
|
||||
if (opts.strictDepBuilds && ignoredBuilds?.length) {
|
||||
throw new IgnoredBuildsError(ignoredBuilds)
|
||||
@@ -295,6 +304,8 @@ export async function recursive (
|
||||
|
||||
const pkgPaths = (Object.keys(opts.selectedProjectsGraph) as ProjectRootDir[]).sort()
|
||||
|
||||
let newCatalogs: CatalogSnapshots | undefined
|
||||
|
||||
const limitInstallation = pLimit(getWorkspaceConcurrency(opts.workspaceConcurrency))
|
||||
await Promise.all(pkgPaths.map(async (rootDir) =>
|
||||
limitInstallation(async () => {
|
||||
@@ -339,6 +350,7 @@ export async function recursive (
|
||||
& { pinnedVersion: 'major' | 'minor' | 'patch' }
|
||||
|
||||
interface ActionResult {
|
||||
newCatalogs?: CatalogSnapshots
|
||||
updatedManifest: ProjectManifest
|
||||
ignoredBuilds: string[] | undefined
|
||||
}
|
||||
@@ -356,7 +368,11 @@ export async function recursive (
|
||||
rootDir,
|
||||
},
|
||||
], opts)
|
||||
return { updatedManifest: mutationResult.updatedProjects[0].manifest, ignoredBuilds: mutationResult.ignoredBuilds }
|
||||
return {
|
||||
newCatalogs: undefined, // there's no reason to add new catalogs on `pnpm remove`
|
||||
updatedManifest: mutationResult.updatedProjects[0].manifest,
|
||||
ignoredBuilds: mutationResult.ignoredBuilds,
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
@@ -367,7 +383,11 @@ export async function recursive (
|
||||
}
|
||||
|
||||
const localConfig = await memReadLocalConfig(rootDir)
|
||||
const { updatedManifest: newManifest, ignoredBuilds } = await action(
|
||||
const {
|
||||
newCatalogs: newCatalogsAddition,
|
||||
updatedManifest: newManifest,
|
||||
ignoredBuilds,
|
||||
} = await action(
|
||||
manifest,
|
||||
{
|
||||
...installOpts,
|
||||
@@ -391,6 +411,10 @@ export async function recursive (
|
||||
)
|
||||
if (opts.save !== false) {
|
||||
await writeProjectManifest(newManifest)
|
||||
if (newCatalogsAddition) {
|
||||
newCatalogs ??= {}
|
||||
Object.assign(newCatalogs, newCatalogsAddition)
|
||||
}
|
||||
}
|
||||
if (opts.strictDepBuilds && ignoredBuilds?.length) {
|
||||
throw new IgnoredBuildsError(ignoredBuilds)
|
||||
@@ -415,6 +439,10 @@ export async function recursive (
|
||||
})
|
||||
))
|
||||
|
||||
if (newCatalogs) {
|
||||
await addCatalogs(opts.workspaceDir, newCatalogs)
|
||||
}
|
||||
|
||||
if (
|
||||
!opts.lockfileOnly && !opts.ignoreScripts && (
|
||||
cmdFullName === 'add' ||
|
||||
|
||||
210
pkg-manager/plugin-commands-installation/test/saveCatalog.ts
Normal file
210
pkg-manager/plugin-commands-installation/test/saveCatalog.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { add } from '@pnpm/plugin-commands-installation'
|
||||
import { prepare, preparePackages } from '@pnpm/prepare'
|
||||
import { addDistTag } from '@pnpm/registry-mock'
|
||||
import { type LockfileFile } from '@pnpm/lockfile.types'
|
||||
import { sync as loadJsonFile } from 'load-json-file'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import { DEFAULT_OPTS } from './utils'
|
||||
|
||||
// This must be a function because some of its values depend on CWD
|
||||
const createOptions = (saveCatalogName = 'default'): add.AddCommandOptions => ({
|
||||
...DEFAULT_OPTS,
|
||||
saveCatalogName,
|
||||
dir: process.cwd(),
|
||||
cacheDir: path.resolve('cache'),
|
||||
storeDir: path.resolve('store'),
|
||||
})
|
||||
|
||||
test('saveCatalogName creates new workspace manifest with the new catalogs', async () => {
|
||||
const project = prepare({
|
||||
name: 'test-save-catalog',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
})
|
||||
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
await add.handler(createOptions(), ['@pnpm.e2e/foo'])
|
||||
|
||||
expect(loadJsonFile('package.json')).toHaveProperty(['dependencies'], {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
})
|
||||
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toHaveProperty(['catalog'], {
|
||||
'@pnpm.e2e/foo': '^100.1.0',
|
||||
})
|
||||
|
||||
expect(project.readLockfile()).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@pnpm.e2e/foo@100.1.0': {
|
||||
resolution: expect.anything(),
|
||||
},
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
})
|
||||
|
||||
test('saveCatalogName works with different protocols', async () => {
|
||||
const project = prepare({
|
||||
name: 'test-save-catalog',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
})
|
||||
|
||||
const options = createOptions()
|
||||
options.registries['@jsr'] = options.rawConfig['@jsr:registry'] = 'https://npm.jsr.io/'
|
||||
await add.handler(options, [
|
||||
'@pnpm.e2e/foo@100.1.0',
|
||||
'jsr:@rus/greet@0.0.3',
|
||||
'github:kevva/is-positive#97edff6',
|
||||
])
|
||||
|
||||
expect(loadJsonFile('package.json')).toHaveProperty(['dependencies'], {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
'@rus/greet': 'catalog:',
|
||||
'is-positive': 'catalog:',
|
||||
})
|
||||
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toHaveProperty(['catalog'], {
|
||||
'@pnpm.e2e/foo': '100.1.0',
|
||||
'@rus/greet': 'jsr:0.0.3',
|
||||
'is-positive': 'github:kevva/is-positive#97edff6',
|
||||
})
|
||||
|
||||
expect(project.readLockfile()).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
'@rus/greet': {
|
||||
specifier: 'jsr:0.0.3',
|
||||
version: '0.0.3',
|
||||
},
|
||||
'is-positive': {
|
||||
specifier: 'github:kevva/is-positive#97edff6',
|
||||
version: '3.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
'@rus/greet': {
|
||||
specifier: 'catalog:',
|
||||
version: '@jsr/rus__greet@0.0.3',
|
||||
},
|
||||
'is-positive': {
|
||||
specifier: 'catalog:',
|
||||
version: 'https://codeload.github.com/kevva/is-positive/tar.gz/97edff6',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
})
|
||||
|
||||
test('saveCatalogName does not work with local dependencies', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'local-dep',
|
||||
version: '0.1.2-local',
|
||||
private: true,
|
||||
},
|
||||
{
|
||||
name: 'main',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
},
|
||||
])
|
||||
|
||||
process.chdir('main')
|
||||
|
||||
await add.handler(createOptions(), ['../local-dep'])
|
||||
|
||||
expect(loadJsonFile('package.json')).toStrictEqual({
|
||||
name: 'main',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'local-dep': process.platform === 'win32'
|
||||
? 'link:..\\local-dep'
|
||||
: 'link:../local-dep',
|
||||
},
|
||||
})
|
||||
|
||||
expect(fs.existsSync('pnpm-workspace.yaml')).toBe(false)
|
||||
|
||||
expect(readYamlFile('pnpm-lock.yaml')).not.toHaveProperty(['catalog'])
|
||||
expect(readYamlFile('pnpm-lock.yaml')).not.toHaveProperty(['catalogs'])
|
||||
})
|
||||
|
||||
test('saveCatalogName with non-default name', async () => {
|
||||
const project = prepare({
|
||||
name: 'test-save-catalog',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
})
|
||||
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
await add.handler(createOptions('my-catalog'), ['@pnpm.e2e/foo'])
|
||||
|
||||
expect(loadJsonFile('package.json')).toHaveProperty(['dependencies'], {
|
||||
'@pnpm.e2e/foo': 'catalog:my-catalog',
|
||||
})
|
||||
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toHaveProperty(['catalogs', 'my-catalog'], {
|
||||
'@pnpm.e2e/foo': '^100.1.0',
|
||||
})
|
||||
|
||||
expect(project.readLockfile()).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
'my-catalog': {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:my-catalog',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@pnpm.e2e/foo@100.1.0': {
|
||||
resolution: expect.anything(),
|
||||
},
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
})
|
||||
@@ -120,6 +120,9 @@
|
||||
{
|
||||
"path": "../../workspace/find-workspace-dir"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/manifest-writer"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/pkgs-graph"
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface WantedDependency {
|
||||
dev: boolean
|
||||
optional: boolean
|
||||
injected?: boolean
|
||||
saveCatalogName?: string
|
||||
}
|
||||
|
||||
type GetNonDevWantedDependenciesManifest = Pick<DependencyManifest, 'bundleDependencies' | 'bundledDependencies' | 'optionalDependencies' | 'dependencies' | 'dependenciesMeta'>
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface WantedDependency {
|
||||
dev: boolean
|
||||
optional: boolean
|
||||
nodeExecPath?: string
|
||||
saveCatalogName?: string
|
||||
updateSpec?: boolean
|
||||
prevSpecifier?: string
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
packageManifestLogger,
|
||||
} from '@pnpm/core-loggers'
|
||||
import {
|
||||
type CatalogSnapshots,
|
||||
type LockfileObject,
|
||||
type ProjectSnapshot,
|
||||
} from '@pnpm/lockfile.types'
|
||||
@@ -93,6 +94,7 @@ export interface ImporterToResolve extends Importer<{
|
||||
export interface ResolveDependenciesResult {
|
||||
dependenciesByProjectId: DependenciesByProjectId
|
||||
dependenciesGraph: GenericDependenciesGraphWithResolvedChildren<ResolvedPackage>
|
||||
newCatalogs?: CatalogSnapshots | undefined
|
||||
outdatedDependencies: {
|
||||
[pkgId: string]: string
|
||||
}
|
||||
@@ -135,6 +137,7 @@ export async function resolveDependencies (
|
||||
appliedPatches,
|
||||
time,
|
||||
allPeerDepNames,
|
||||
newCatalogs,
|
||||
} = await resolveDependencyTree(projectsToResolve, opts)
|
||||
|
||||
opts.storeController.clearResolutionCache()
|
||||
@@ -303,7 +306,17 @@ export async function resolveDependencies (
|
||||
}
|
||||
}
|
||||
|
||||
// Q: Why would `newLockfile.catalogs` be constructed twice?
|
||||
// A: `getCatalogSnapshots` handles new dependencies that were resolved as `catalog:*` (e.g. new entries in `package.json` whose values were `catalog:*`),
|
||||
// and `newCatalogs` handles dependencies that were added as CLI parameters from `pnpm add --save-catalog`.
|
||||
newLockfile.catalogs = getCatalogSnapshots(Object.values(resolvedImporters).flatMap(({ directDependencies }) => directDependencies))
|
||||
for (const catalogName in newCatalogs) {
|
||||
for (const dependencyName in newCatalogs[catalogName]) {
|
||||
newLockfile.catalogs ??= {}
|
||||
newLockfile.catalogs[catalogName] ??= {}
|
||||
newLockfile.catalogs[catalogName][dependencyName] = newCatalogs[catalogName][dependencyName]
|
||||
}
|
||||
}
|
||||
|
||||
// waiting till package requests are finished
|
||||
async function waitTillAllFetchingsFinish (): Promise<void> {
|
||||
@@ -319,6 +332,7 @@ export async function resolveDependencies (
|
||||
dependenciesGraph,
|
||||
outdatedDependencies,
|
||||
linkedDependenciesByProjectId,
|
||||
newCatalogs,
|
||||
newLockfile,
|
||||
peerDependencyIssuesByProjects,
|
||||
waitTillAllFetchingsFinish,
|
||||
|
||||
@@ -204,6 +204,7 @@ export type PkgAddress = {
|
||||
catalogLookup?: CatalogLookupMetadata
|
||||
optional: boolean
|
||||
normalizedBareSpecifier?: string
|
||||
saveCatalogName?: string
|
||||
} & ({
|
||||
isLinkedDependency: true
|
||||
version: string
|
||||
@@ -1565,6 +1566,9 @@ async function resolveDependency (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedPkg = ctx.resolvedPkgsById[pkgResponse.body.id]
|
||||
|
||||
return {
|
||||
alias: wantedDependency.alias ?? pkgResponse.body.alias ?? pkg.name,
|
||||
depIsLinked,
|
||||
@@ -1576,7 +1580,9 @@ async function resolveDependency (
|
||||
pkgId: pkgResponse.body.id,
|
||||
rootDir,
|
||||
missingPeers: getMissingPeers(pkg),
|
||||
optional: ctx.resolvedPkgsById[pkgResponse.body.id].optional,
|
||||
optional: resolvedPkg.optional,
|
||||
version: resolvedPkg.version,
|
||||
saveCatalogName: wantedDependency.saveCatalogName,
|
||||
|
||||
// Next fields are actually only needed when isNew = true
|
||||
installable,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveFromCatalog } from '@pnpm/catalogs.resolver'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type LockfileObject } from '@pnpm/lockfile.types'
|
||||
import { type CatalogSnapshots, type LockfileObject } from '@pnpm/lockfile.types'
|
||||
import { globalWarn } from '@pnpm/logger'
|
||||
import { type PatchGroupRecord } from '@pnpm/patching.config'
|
||||
import { type PreferredVersions, type Resolution, type WorkspacePackages } from '@pnpm/resolver-base'
|
||||
import { type StoreController } from '@pnpm/store-controller-types'
|
||||
@@ -136,6 +137,7 @@ export interface ResolveDependenciesOptions {
|
||||
export interface ResolveDependencyTreeResult {
|
||||
allPeerDepNames: Set<string>
|
||||
dependenciesTree: DependenciesTree<ResolvedPackage>
|
||||
newCatalogs?: CatalogSnapshots
|
||||
outdatedDependencies: {
|
||||
[pkgId: string]: string
|
||||
}
|
||||
@@ -234,6 +236,29 @@ export async function resolveDependencyTree<T> (
|
||||
const { pkgAddressesByImporters, time } = await resolveRootDependencies(ctx, resolveArgs)
|
||||
const directDepsByImporterId = zipObj(importers.map(({ id }) => id), pkgAddressesByImporters)
|
||||
|
||||
let newCatalogs: CatalogSnapshots | undefined
|
||||
for (const directDependencies of pkgAddressesByImporters) {
|
||||
for (const directDep of directDependencies as PkgAddress[]) {
|
||||
const { alias, normalizedBareSpecifier, version, saveCatalogName } = directDep
|
||||
const existingCatalog = opts.catalogs?.default?.[alias]
|
||||
if (existingCatalog != null) {
|
||||
if (existingCatalog !== normalizedBareSpecifier) {
|
||||
globalWarn(
|
||||
`Skip adding ${alias} to catalogs.${saveCatalogName} because it already exists as ${existingCatalog}`
|
||||
)
|
||||
}
|
||||
} else if (saveCatalogName != null && normalizedBareSpecifier != null && version != null) {
|
||||
newCatalogs ??= {}
|
||||
newCatalogs[saveCatalogName] ??= {}
|
||||
newCatalogs[saveCatalogName][alias] = {
|
||||
specifier: normalizedBareSpecifier,
|
||||
version,
|
||||
}
|
||||
directDep.normalizedBareSpecifier = `catalog:${saveCatalogName === 'default' ? '' : saveCatalogName}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const pendingNode of ctx.pendingNodes) {
|
||||
ctx.dependenciesTree.set(pendingNode.nodeId, {
|
||||
children: () => buildTree(ctx, pendingNode.resolvedPackage.id,
|
||||
@@ -276,6 +301,7 @@ export async function resolveDependencyTree<T> (
|
||||
|
||||
return {
|
||||
dependenciesTree: ctx.dependenciesTree,
|
||||
newCatalogs,
|
||||
outdatedDependencies: ctx.outdatedDependencies,
|
||||
resolvedImporters,
|
||||
resolvedPkgsById: ctx.resolvedPkgsById,
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -5472,6 +5472,9 @@ importers:
|
||||
'@pnpm/workspace.find-packages':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/find-packages
|
||||
'@pnpm/workspace.manifest-writer':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/manifest-writer
|
||||
'@pnpm/workspace.pkgs-graph':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/pkgs-graph
|
||||
@@ -8273,6 +8276,9 @@ importers:
|
||||
'@pnpm/constants':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/constants
|
||||
'@pnpm/lockfile.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../lockfile/types
|
||||
'@pnpm/object.key-sorting':
|
||||
specifier: workspace:*
|
||||
version: link:../../object/key-sorting
|
||||
|
||||
768
pnpm/test/saveCatalog.ts
Normal file
768
pnpm/test/saveCatalog.ts
Normal file
@@ -0,0 +1,768 @@
|
||||
import { type LockfileFile } from '@pnpm/lockfile.types'
|
||||
import { prepare, preparePackages } from '@pnpm/prepare'
|
||||
import { addDistTag } from '@pnpm/registry-mock'
|
||||
import { type ProjectManifest } from '@pnpm/types'
|
||||
import { sync as loadJsonFile } from 'load-json-file'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
import { execPnpm } from './utils'
|
||||
|
||||
test('--save-catalog adds catalogs to the manifest of a single package workspace', async () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: 'test-save-catalog',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': 'catalog:',
|
||||
},
|
||||
}
|
||||
|
||||
prepare(manifest)
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '^100.1.0',
|
||||
},
|
||||
})
|
||||
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
await execPnpm(['install'])
|
||||
expect(readYamlFile('pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@pnpm.e2e/bar@100.1.0': expect.anything(),
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
|
||||
await execPnpm(['add', '--save-catalog', '@pnpm.e2e/foo'])
|
||||
expect(readYamlFile('pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@pnpm.e2e/bar@100.1.0': expect.anything(),
|
||||
'@pnpm.e2e/foo@100.1.0': expect.anything(),
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toStrictEqual({
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '^100.1.0',
|
||||
'@pnpm.e2e/foo': '^100.1.0',
|
||||
},
|
||||
})
|
||||
expect(loadJsonFile('package.json')).toStrictEqual({
|
||||
...manifest,
|
||||
dependencies: {
|
||||
...manifest.dependencies,
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('--save-catalog adds catalogs to the manifest of a shared lockfile workspace', async () => {
|
||||
const manifests: ProjectManifest[] = [
|
||||
{
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': 'catalog:',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]
|
||||
|
||||
preparePackages(manifests)
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
sharedWorkspaceLockfile: true,
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '^100.1.0',
|
||||
},
|
||||
packages: ['project-0', 'project-1'],
|
||||
})
|
||||
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
await execPnpm(['install'])
|
||||
expect(readYamlFile('pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'project-0': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
'project-1': {},
|
||||
},
|
||||
packages: {
|
||||
'@pnpm.e2e/bar@100.1.0': expect.anything(),
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
|
||||
await execPnpm(['--filter=project-1', 'add', '--save-catalog', '@pnpm.e2e/foo'])
|
||||
expect(readYamlFile('pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'project-0': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
'project-1': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@pnpm.e2e/bar@100.1.0': expect.anything(),
|
||||
'@pnpm.e2e/foo@100.1.0': expect.anything(),
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toStrictEqual({
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '^100.1.0',
|
||||
'@pnpm.e2e/foo': '^100.1.0',
|
||||
},
|
||||
packages: ['project-0', 'project-1'],
|
||||
sharedWorkspaceLockfile: true,
|
||||
})
|
||||
expect(loadJsonFile('project-1/package.json')).toStrictEqual({
|
||||
...manifests[1],
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('--save-catalog adds catalogs to the manifest of a multi-lockfile workspace', async () => {
|
||||
const manifests: ProjectManifest[] = [
|
||||
{
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': 'catalog:',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]
|
||||
|
||||
preparePackages(manifests)
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
sharedWorkspaceLockfile: false,
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '^100.1.0',
|
||||
},
|
||||
packages: ['project-0', 'project-1'],
|
||||
})
|
||||
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
{
|
||||
await execPnpm(['install'])
|
||||
|
||||
const lockfile0: LockfileFile = readYamlFile('project-0/pnpm-lock.yaml')
|
||||
expect(lockfile0.catalogs).toStrictEqual({
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
} as LockfileFile['catalogs'])
|
||||
expect(lockfile0.importers).toStrictEqual({
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as LockfileFile['importers'])
|
||||
|
||||
const lockfile1: LockfileFile = readYamlFile('project-1/pnpm-lock.yaml')
|
||||
expect(lockfile1.catalogs).toBeUndefined()
|
||||
expect(lockfile1.importers).toStrictEqual({
|
||||
'.': {},
|
||||
} as LockfileFile['importers'])
|
||||
}
|
||||
|
||||
{
|
||||
await execPnpm(['--filter=project-1', 'add', '--save-catalog', '@pnpm.e2e/foo'])
|
||||
|
||||
const lockfile0: LockfileFile = readYamlFile('project-0/pnpm-lock.yaml')
|
||||
expect(lockfile0.catalogs).toStrictEqual({
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
} as LockfileFile['catalogs'])
|
||||
expect(lockfile0.importers).toStrictEqual({
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as LockfileFile['importers'])
|
||||
|
||||
const lockfile1: LockfileFile = readYamlFile('project-1/pnpm-lock.yaml')
|
||||
expect(lockfile1.catalogs).toStrictEqual({
|
||||
default: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
} as LockfileFile['catalogs'])
|
||||
expect(lockfile1.importers).toStrictEqual({
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as LockfileFile['importers'])
|
||||
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toStrictEqual({
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '^100.1.0',
|
||||
'@pnpm.e2e/foo': '^100.1.0',
|
||||
},
|
||||
packages: ['project-0', 'project-1'],
|
||||
sharedWorkspaceLockfile: false,
|
||||
})
|
||||
|
||||
expect(loadJsonFile('project-1/package.json')).toStrictEqual({
|
||||
...manifests[1],
|
||||
dependencies: {
|
||||
...manifests[1].dependencies,
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('--save-catalog does not add local workspace dependency as a catalog', async () => {
|
||||
const manifests: ProjectManifest[] = [
|
||||
{
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]
|
||||
|
||||
preparePackages(manifests)
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
packages: ['project-0', 'project-1'],
|
||||
})
|
||||
|
||||
{
|
||||
await execPnpm(['install'])
|
||||
|
||||
const lockfile: LockfileFile = readYamlFile('pnpm-lock.yaml')
|
||||
expect(lockfile.catalogs).toBeUndefined()
|
||||
expect(lockfile.importers).toStrictEqual({
|
||||
'project-0': {},
|
||||
'project-1': {},
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
await execPnpm(['--filter=project-1', 'add', '--save-catalog', 'project-0@workspace:*'])
|
||||
|
||||
const lockfile: LockfileFile = readYamlFile('pnpm-lock.yaml')
|
||||
expect(lockfile.catalogs).toBeUndefined()
|
||||
expect(lockfile.importers).toStrictEqual({
|
||||
'project-0': {},
|
||||
'project-1': {
|
||||
dependencies: {
|
||||
'project-0': {
|
||||
specifier: 'workspace:*',
|
||||
version: 'link:../project-0',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toStrictEqual({
|
||||
packages: ['project-0', 'project-1'],
|
||||
})
|
||||
|
||||
expect(loadJsonFile('project-1/package.json')).toStrictEqual({
|
||||
...manifests[1],
|
||||
dependencies: {
|
||||
'project-0': 'workspace:*',
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('--save-catalog does not affect new dependencies from package.json', async () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: 'test-save-catalog',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/pkg-a': 'catalog:',
|
||||
},
|
||||
}
|
||||
|
||||
const project = prepare(manifest)
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
catalog: {
|
||||
'@pnpm.e2e/pkg-a': '1.0.0',
|
||||
},
|
||||
})
|
||||
|
||||
// initialize the lockfile
|
||||
await execPnpm(['install'])
|
||||
expect(project.readLockfile()).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/pkg-a': {
|
||||
specifier: '1.0.0',
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/pkg-a': {
|
||||
specifier: 'catalog:',
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
|
||||
// add a new dependency to package.json by editing it
|
||||
project.writePackageJson({
|
||||
...manifest,
|
||||
dependencies: {
|
||||
...manifest.dependencies,
|
||||
'@pnpm.e2e/pkg-b': '*',
|
||||
},
|
||||
} as ProjectManifest)
|
||||
|
||||
// add a new dependency by running `pnpm add --save-catalog`
|
||||
await execPnpm(['add', '--save-catalog', '@pnpm.e2e/pkg-c'])
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.catalogs).toStrictEqual({
|
||||
default: {
|
||||
'@pnpm.e2e/pkg-a': {
|
||||
specifier: '1.0.0',
|
||||
version: '1.0.0',
|
||||
},
|
||||
'@pnpm.e2e/pkg-c': {
|
||||
specifier: '^1.0.0',
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
} as LockfileFile['catalogs'])
|
||||
expect(lockfile.catalogs.default).not.toHaveProperty(['@pnpm.e2e/pkg-b'])
|
||||
expect(lockfile.importers).toStrictEqual({
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/pkg-a': {
|
||||
specifier: 'catalog:',
|
||||
version: '1.0.0',
|
||||
},
|
||||
'@pnpm.e2e/pkg-b': {
|
||||
specifier: '*', // unaffected by `pnpm add --save-catalog`
|
||||
version: '1.0.0',
|
||||
},
|
||||
'@pnpm.e2e/pkg-c': {
|
||||
specifier: 'catalog:', // created by `pnpm add --save-catalog`
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as LockfileFile['importers'])
|
||||
|
||||
expect(loadJsonFile('package.json')).toStrictEqual({
|
||||
...manifest,
|
||||
dependencies: {
|
||||
...manifest.dependencies,
|
||||
'@pnpm.e2e/pkg-b': '*', // unaffected by `pnpm add --save-catalog`
|
||||
'@pnpm.e2e/pkg-c': 'catalog:', // created by `pnpm add --save-catalog`
|
||||
},
|
||||
} as ProjectManifest)
|
||||
})
|
||||
|
||||
test('--save-catalog does not overwrite existing catalogs', async () => {
|
||||
const manifests: ProjectManifest[] = [
|
||||
{
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': 'catalog:',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]
|
||||
|
||||
preparePackages(manifests)
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '=100.0.0', // intentionally outdated
|
||||
},
|
||||
packages: ['project-0', 'project-1'],
|
||||
})
|
||||
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
await execPnpm(['install'])
|
||||
expect(readYamlFile('pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '=100.0.0',
|
||||
version: '100.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'project-0': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
'project-1': {},
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
|
||||
await execPnpm(['add', '--filter=project-1', '--save-catalog', '@pnpm.e2e/foo@100.1.0', '@pnpm.e2e/bar@100.1.0'])
|
||||
expect(readYamlFile('pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '=100.0.0', // unchanged
|
||||
version: '100.0.0',
|
||||
},
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '100.1.0', // created by `pnpm add --save-catalog`
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'project-0': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:', // unchanged
|
||||
version: '100.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
'project-1': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '100.1.0', // created by `pnpm add --save-catalog`
|
||||
version: '100.1.0',
|
||||
},
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:', // created by `pnpm add --save-catalog`
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toStrictEqual({
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '=100.0.0', // unchanged
|
||||
'@pnpm.e2e/foo': '100.1.0', // created by `pnpm add --save-catalog`
|
||||
},
|
||||
packages: ['project-0', 'project-1'],
|
||||
})
|
||||
expect(loadJsonFile('project-0/package.json')).toStrictEqual(manifests[0])
|
||||
expect(loadJsonFile('project-1/package.json')).toStrictEqual({
|
||||
...manifests[1],
|
||||
dependencies: {
|
||||
...manifests[1].dependencies,
|
||||
'@pnpm.e2e/bar': '100.1.0',
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
} as ProjectManifest)
|
||||
})
|
||||
|
||||
test('--save-catalog creates new workspace manifest with the new catalog (recursive add)', async () => {
|
||||
const manifests: ProjectManifest[] = [
|
||||
{
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]
|
||||
|
||||
preparePackages(manifests)
|
||||
|
||||
await execPnpm(['add', '--recursive', '--save-catalog', '@pnpm.e2e/foo@100.1.0'])
|
||||
|
||||
expect(readYamlFile('project-0/pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
expect(readYamlFile('project-1/pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toStrictEqual({
|
||||
catalog: {
|
||||
'@pnpm.e2e/foo': '100.1.0',
|
||||
},
|
||||
})
|
||||
|
||||
expect(loadJsonFile('project-0/package.json')).toStrictEqual({
|
||||
...manifests[0],
|
||||
dependencies: {
|
||||
...manifests[0].dependencies,
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
} as ProjectManifest)
|
||||
expect(loadJsonFile('project-1/package.json')).toStrictEqual({
|
||||
...manifests[1],
|
||||
dependencies: {
|
||||
...manifests[1].dependencies,
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
} as ProjectManifest)
|
||||
})
|
||||
|
||||
test('--save-catalog with a non-default catalog name', async () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: 'test-save-catalog',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': 'catalog:',
|
||||
},
|
||||
}
|
||||
|
||||
prepare(manifest)
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '^100.1.0',
|
||||
},
|
||||
})
|
||||
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
await addDistTag({ package: '@pnpm.e2e/bar', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
await execPnpm(['install'])
|
||||
expect(readYamlFile('pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@pnpm.e2e/bar@100.1.0': expect.anything(),
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
|
||||
await execPnpm(['add', '--save-catalog-name=my-catalog', '@pnpm.e2e/foo'])
|
||||
expect(readYamlFile('pnpm-lock.yaml')).toStrictEqual(expect.objectContaining({
|
||||
catalogs: {
|
||||
default: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
'my-catalog': {
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: '^100.1.0',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/bar': {
|
||||
specifier: 'catalog:',
|
||||
version: '100.1.0',
|
||||
},
|
||||
'@pnpm.e2e/foo': {
|
||||
specifier: 'catalog:my-catalog',
|
||||
version: '100.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'@pnpm.e2e/bar@100.1.0': expect.anything(),
|
||||
'@pnpm.e2e/foo@100.1.0': expect.anything(),
|
||||
},
|
||||
} as Partial<LockfileFile>))
|
||||
expect(readYamlFile('pnpm-workspace.yaml')).toStrictEqual({
|
||||
catalog: {
|
||||
'@pnpm.e2e/bar': '^100.1.0',
|
||||
},
|
||||
catalogs: {
|
||||
'my-catalog': {
|
||||
'@pnpm.e2e/foo': '^100.1.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(loadJsonFile('package.json')).toStrictEqual({
|
||||
...manifest,
|
||||
dependencies: {
|
||||
...manifest.dependencies,
|
||||
'@pnpm.e2e/foo': 'catalog:my-catalog',
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -31,6 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/lockfile.types": "workspace:*",
|
||||
"@pnpm/object.key-sorting": "workspace:*",
|
||||
"@pnpm/workspace.read-manifest": "workspace:*",
|
||||
"ramda": "catalog:",
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { type ResolvedCatalogEntry } from '@pnpm/lockfile.types'
|
||||
import { readWorkspaceManifest, type WorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
import { WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import writeYamlFile from 'write-yaml-file'
|
||||
import equals from 'ramda/src/equals'
|
||||
import { sortKeysByPriority } from '@pnpm/object.key-sorting'
|
||||
|
||||
async function writeManifestFile (dir: string, manifest: Partial<WorkspaceManifest>): Promise<void> {
|
||||
manifest = sortKeysByPriority({
|
||||
priority: { packages: 0 },
|
||||
deep: false,
|
||||
}, manifest)
|
||||
return writeYamlFile(path.join(dir, WORKSPACE_MANIFEST_FILENAME), manifest, {
|
||||
lineWidth: -1, // This is setting line width to never wrap
|
||||
blankLines: true,
|
||||
noCompatMode: true,
|
||||
noRefs: true,
|
||||
sortKeys: false,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateWorkspaceManifest (dir: string, updatedFields: Partial<WorkspaceManifest>): Promise<void> {
|
||||
let manifest = await readWorkspaceManifest(dir) ?? {} as WorkspaceManifest
|
||||
const manifest = await readWorkspaceManifest(dir) ?? {} as WorkspaceManifest
|
||||
let shouldBeUpdated = false
|
||||
for (const [key, value] of Object.entries(updatedFields)) {
|
||||
if (!equals(manifest[key as keyof WorkspaceManifest], value)) {
|
||||
@@ -27,14 +42,45 @@ export async function updateWorkspaceManifest (dir: string, updatedFields: Parti
|
||||
await fs.promises.rm(path.join(dir, WORKSPACE_MANIFEST_FILENAME))
|
||||
return
|
||||
}
|
||||
manifest = sortKeysByPriority({
|
||||
priority: { packages: 0 },
|
||||
deep: false,
|
||||
}, manifest)
|
||||
await writeYamlFile(path.join(dir, WORKSPACE_MANIFEST_FILENAME), manifest, {
|
||||
lineWidth: -1, // This is setting line width to never wrap
|
||||
noCompatMode: true,
|
||||
noRefs: true,
|
||||
sortKeys: false,
|
||||
})
|
||||
await writeManifestFile(dir, manifest)
|
||||
}
|
||||
|
||||
export interface NewCatalogs {
|
||||
[catalogName: string]: {
|
||||
[dependencyName: string]: Pick<ResolvedCatalogEntry, 'specifier'>
|
||||
}
|
||||
}
|
||||
|
||||
export async function addCatalogs (workspaceDir: string, newCatalogs: NewCatalogs): Promise<void> {
|
||||
const manifest: Partial<WorkspaceManifest> = await readWorkspaceManifest(workspaceDir) ?? {}
|
||||
let shouldBeUpdated = false
|
||||
|
||||
for (const catalogName in newCatalogs) {
|
||||
let targetCatalog: Record<string, string> | undefined = catalogName === 'default'
|
||||
? manifest.catalog ?? manifest.catalogs?.default
|
||||
: manifest.catalogs?.[catalogName]
|
||||
const targetCatalogWasNil = targetCatalog == null
|
||||
|
||||
for (const dependencyName in newCatalogs[catalogName]) {
|
||||
targetCatalog ??= {}
|
||||
targetCatalog[dependencyName] = newCatalogs[catalogName][dependencyName].specifier
|
||||
}
|
||||
|
||||
if (targetCatalog == null) continue
|
||||
|
||||
shouldBeUpdated = true
|
||||
|
||||
if (targetCatalogWasNil) {
|
||||
if (catalogName === 'default') {
|
||||
manifest.catalog = targetCatalog
|
||||
} else {
|
||||
manifest.catalogs ??= {}
|
||||
manifest.catalogs[catalogName] = targetCatalog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldBeUpdated) {
|
||||
await writeManifestFile(workspaceDir, manifest)
|
||||
}
|
||||
}
|
||||
|
||||
191
workspace/manifest-writer/test/addCatalogs.test.ts
Normal file
191
workspace/manifest-writer/test/addCatalogs.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { WORKSPACE_MANIFEST_FILENAME } from '@pnpm/constants'
|
||||
import { tempDir } from '@pnpm/prepare-temp-dir'
|
||||
import { addCatalogs } from '@pnpm/workspace.manifest-writer'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
|
||||
test('addCatalogs does not write new workspace manifest for empty catalogs', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
await addCatalogs(dir, {})
|
||||
expect(fs.existsSync(filePath)).toBe(false)
|
||||
})
|
||||
|
||||
test('addCatalogs does not write new workspace manifest for empty default catalogs', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
await addCatalogs(dir, {
|
||||
default: {},
|
||||
})
|
||||
expect(fs.existsSync(filePath)).toBe(false)
|
||||
})
|
||||
|
||||
test('addCatalogs does not write new workspace manifest for empty any-named catalogs', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
await addCatalogs(dir, {
|
||||
foo: {},
|
||||
bar: {},
|
||||
})
|
||||
expect(fs.existsSync(filePath)).toBe(false)
|
||||
})
|
||||
|
||||
test('addCatalogs does not add empty catalogs', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
writeYamlFile(filePath, {})
|
||||
await addCatalogs(dir, {})
|
||||
expect(readYamlFile(filePath)).toStrictEqual({})
|
||||
})
|
||||
|
||||
test('addCatalogs does not add empty default catalogs', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
writeYamlFile(filePath, {})
|
||||
await addCatalogs(dir, {
|
||||
default: {},
|
||||
})
|
||||
expect(readYamlFile(filePath)).toStrictEqual({})
|
||||
})
|
||||
|
||||
test('addCatalogs does not add empty any-named catalogs', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
writeYamlFile(filePath, {})
|
||||
await addCatalogs(dir, {
|
||||
foo: {},
|
||||
bar: {},
|
||||
})
|
||||
expect(readYamlFile(filePath)).toStrictEqual({})
|
||||
})
|
||||
|
||||
test('addCatalogs adds `default` catalogs to the `catalog` object by default', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
await addCatalogs(dir, {
|
||||
default: {
|
||||
foo: {
|
||||
specifier: '^0.1.2',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(readYamlFile(filePath)).toStrictEqual({
|
||||
catalog: {
|
||||
foo: '^0.1.2',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('addCatalogs adds `default` catalogs to the `catalog` object if it exists', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
writeYamlFile(filePath, {
|
||||
catalog: {
|
||||
bar: '3.2.1',
|
||||
},
|
||||
})
|
||||
await addCatalogs(dir, {
|
||||
default: {
|
||||
foo: {
|
||||
specifier: '^0.1.2',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(readYamlFile(filePath)).toStrictEqual({
|
||||
catalog: {
|
||||
bar: '3.2.1',
|
||||
foo: '^0.1.2',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('addCatalogs adds `default` catalogs to the `catalogs.default` object if it exists', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
writeYamlFile(filePath, {
|
||||
catalogs: {
|
||||
default: {
|
||||
bar: '3.2.1',
|
||||
},
|
||||
},
|
||||
})
|
||||
await addCatalogs(dir, {
|
||||
default: {
|
||||
foo: {
|
||||
specifier: '^0.1.2',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(readYamlFile(filePath)).toStrictEqual({
|
||||
catalogs: {
|
||||
default: {
|
||||
bar: '3.2.1',
|
||||
foo: '^0.1.2',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('addCatalogs creates a `catalogs` object for any-named catalogs', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
await addCatalogs(dir, {
|
||||
foo: {
|
||||
abc: {
|
||||
specifier: '0.1.2',
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
def: {
|
||||
specifier: '3.2.1',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(readYamlFile(filePath)).toStrictEqual({
|
||||
catalogs: {
|
||||
foo: {
|
||||
abc: '0.1.2',
|
||||
},
|
||||
bar: {
|
||||
def: '3.2.1',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('addCatalogs add any-named catalogs to the `catalogs` object if it already exists', async () => {
|
||||
const dir = tempDir(false)
|
||||
const filePath = path.join(dir, WORKSPACE_MANIFEST_FILENAME)
|
||||
writeYamlFile(filePath, {
|
||||
catalogs: {
|
||||
foo: {
|
||||
ghi: '7.8.9',
|
||||
},
|
||||
},
|
||||
})
|
||||
await addCatalogs(dir, {
|
||||
foo: {
|
||||
abc: {
|
||||
specifier: '0.1.2',
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
def: {
|
||||
specifier: '3.2.1',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(readYamlFile(filePath)).toStrictEqual({
|
||||
catalogs: {
|
||||
foo: {
|
||||
abc: '0.1.2',
|
||||
ghi: '7.8.9',
|
||||
},
|
||||
bar: {
|
||||
def: '3.2.1',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,9 @@
|
||||
{
|
||||
"path": "../../__utils__/prepare-temp-dir"
|
||||
},
|
||||
{
|
||||
"path": "../../lockfile/types"
|
||||
},
|
||||
{
|
||||
"path": "../../object/key-sorting"
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { InvalidWorkspaceManifestError } from './errors/InvalidWorkspaceManifestError'
|
||||
|
||||
export interface WorkspaceNamedCatalogs {
|
||||
readonly [catalogName: string]: WorkspaceCatalog
|
||||
[catalogName: string]: WorkspaceCatalog
|
||||
}
|
||||
|
||||
export interface WorkspaceCatalog {
|
||||
readonly [dependencyName: string]: string
|
||||
[dependencyName: string]: string
|
||||
}
|
||||
|
||||
export function assertValidWorkspaceManifestCatalog (manifest: { packages?: readonly string[], catalog?: unknown }): asserts manifest is { catalog?: WorkspaceCatalog } {
|
||||
|
||||
Reference in New Issue
Block a user