mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat(config)!: project-specific packageConfigs (#10304)
* feat(config)!: project level `config.yaml` * test: fix * refactor: shorten some names * docs(changeset): change wording * feat: move project settings to pnpm-workspace.yaml * test: remove unneeded fixture * docs(changeset): correct * refactor: replace validation with creation * docs: consistent terminology * perf: validate once * test: projectConfig * refactor: explicitly use `undefined` * refactor: reuse `ProjectConfigRecord` * chore(deps): remove unused dependency * style: remove extra pipe character * refactor: rename to `projectConfigs` * feat: flatten `projectConfig` with `match` * refactor: correct error class names * docs(changeset): update * test: fix * feat: rename to `packageConfigs` Rename `projectConfigs` to `packageConfigs` in the workspace manifest. The term "project config" is still used internally, because, internally, "project" refers to workspace packages whilst "package" refers to 3rd party packages and dependencies. * docs(changeset): clarify `project-N`
This commit is contained in:
33
.changeset/cruel-colts-smoke.md
Normal file
33
.changeset/cruel-colts-smoke.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
"@pnpm/config": major
|
||||
"pnpm": major
|
||||
---
|
||||
|
||||
Replace workspace project specific `.npmrc` with `packageConfigs` in `pnpm-workspace.yaml`.
|
||||
|
||||
A workspace manifest with `packageConfigs` would look something like this:
|
||||
|
||||
```yaml
|
||||
# File: pnpm-workspace.yaml
|
||||
packages:
|
||||
- 'packages/project-1'
|
||||
- 'packages/project-2'
|
||||
packageConfigs:
|
||||
'project-1':
|
||||
saveExact: true
|
||||
'project-2':
|
||||
savePrefix: '~'
|
||||
```
|
||||
|
||||
Or this:
|
||||
|
||||
```yaml
|
||||
# File: pnpm-workspace.yaml
|
||||
packages:
|
||||
- 'packages/project-1'
|
||||
- 'packages/project-2'
|
||||
packageConfigs:
|
||||
- match: ['project-1', 'project-2']
|
||||
modulesDir: 'node_modules'
|
||||
saveExact: true
|
||||
```
|
||||
@@ -237,6 +237,8 @@ export interface Config extends OptionsFromRootManifest {
|
||||
fetchMinSpeedKiBps?: number
|
||||
trustPolicy?: TrustPolicy
|
||||
trustPolicyExclude?: string[]
|
||||
|
||||
packageConfigs?: ProjectConfigSet
|
||||
}
|
||||
|
||||
export interface ConfigWithDeprecatedSettings extends Config {
|
||||
@@ -244,3 +246,22 @@ export interface ConfigWithDeprecatedSettings extends Config {
|
||||
proxy?: string
|
||||
shamefullyFlatten?: boolean
|
||||
}
|
||||
|
||||
export const PROJECT_CONFIG_FIELDS = [
|
||||
'hoist',
|
||||
'modulesDir',
|
||||
'saveExact',
|
||||
'savePrefix',
|
||||
] as const satisfies Array<keyof Config>
|
||||
|
||||
export type ProjectConfig = Partial<Pick<Config, typeof PROJECT_CONFIG_FIELDS[number] | 'hoistPattern'>>
|
||||
|
||||
/** Simple map from project names to {@link ProjectConfig} */
|
||||
export type ProjectConfigRecord = Record<string, ProjectConfig>
|
||||
|
||||
/** Map multiple project names to a shared {@link ProjectConfig} */
|
||||
export type ProjectConfigMultiMatch = { match: string[] } & ProjectConfig
|
||||
|
||||
export type ProjectConfigSet =
|
||||
| ProjectConfigRecord
|
||||
| ProjectConfigMultiMatch[]
|
||||
|
||||
@@ -31,6 +31,7 @@ import { getCacheDir, getConfigDir, getDataDir, getStateDir } from './dirs.js'
|
||||
import {
|
||||
type Config,
|
||||
type ConfigWithDeprecatedSettings,
|
||||
type ProjectConfig,
|
||||
type UniversalOptions,
|
||||
type VerifyDepsBeforeRun,
|
||||
type WantedPackageManager,
|
||||
@@ -48,10 +49,22 @@ import {
|
||||
export { types }
|
||||
|
||||
export { getOptionsFromRootManifest, getOptionsFromPnpmSettings, type OptionsFromRootManifest } from './getOptionsFromRootManifest.js'
|
||||
export * from './readLocalConfig.js'
|
||||
export { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency } from './concurrency.js'
|
||||
|
||||
export type { Config, UniversalOptions, WantedPackageManager, VerifyDepsBeforeRun }
|
||||
export {
|
||||
ProjectConfigInvalidValueTypeError,
|
||||
ProjectConfigIsNotAnObjectError,
|
||||
ProjectConfigUnsupportedFieldError,
|
||||
ProjectConfigsArrayItemIsNotAnObjectError,
|
||||
ProjectConfigsArrayItemMatchIsNotAnArrayError,
|
||||
ProjectConfigsArrayItemMatchIsNotDefinedError,
|
||||
ProjectConfigsIsNeitherObjectNorArrayError,
|
||||
ProjectConfigsMatchItemIsNotAStringError,
|
||||
type CreateProjectConfigRecordOptions,
|
||||
createProjectConfigRecord,
|
||||
} from './projectConfig.js'
|
||||
|
||||
export type { Config, ProjectConfig, UniversalOptions, WantedPackageManager, VerifyDepsBeforeRun }
|
||||
|
||||
export { isIniConfigKey } from './auth.js'
|
||||
export { type ConfigFileKey, isConfigFileKey } from './configFileKey.js'
|
||||
|
||||
153
config/config/src/projectConfig.ts
Normal file
153
config/config/src/projectConfig.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { omit } from 'ramda'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { PROJECT_CONFIG_FIELDS, type Config, type ProjectConfig, type ProjectConfigRecord } from './Config.js'
|
||||
|
||||
export type CreateProjectConfigRecordOptions = Pick<Config, 'packageConfigs'>
|
||||
|
||||
export function createProjectConfigRecord (opts: CreateProjectConfigRecordOptions): ProjectConfigRecord | undefined {
|
||||
return createProjectConfigRecordFromConfigSet(opts.packageConfigs)
|
||||
}
|
||||
|
||||
export class ProjectConfigIsNotAnObjectError extends PnpmError {
|
||||
readonly actualRawConfig: unknown
|
||||
constructor (actualRawConfig: unknown) {
|
||||
super('PROJECT_CONFIG_NOT_AN_OBJECT', `Expecting project-specific config to be an object, but received ${JSON.stringify(actualRawConfig)}`)
|
||||
this.actualRawConfig = actualRawConfig
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectConfigInvalidValueTypeError extends PnpmError {
|
||||
readonly expectedType: string
|
||||
readonly actualType: string
|
||||
readonly actualValue: unknown
|
||||
constructor (expectedType: string, actualValue: unknown) {
|
||||
const actualType = typeof actualValue
|
||||
super('PROJECT_CONFIG_INVALID_VALUE_TYPE', `Expecting a value of type ${expectedType} but received a value of type ${actualType}: ${JSON.stringify(actualValue)}`)
|
||||
this.expectedType = expectedType
|
||||
this.actualType = actualType
|
||||
this.actualValue = actualValue
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectConfigUnsupportedFieldError extends PnpmError {
|
||||
readonly field: string
|
||||
constructor (field: string) {
|
||||
super('PROJECT_CONFIG_UNSUPPORTED_FIELD', `Field ${field} is not supported but was specified`)
|
||||
this.field = field
|
||||
}
|
||||
}
|
||||
|
||||
function createProjectConfigFromRaw (config: unknown): ProjectConfig {
|
||||
if (typeof config !== 'object' || !config || Array.isArray(config)) {
|
||||
throw new ProjectConfigIsNotAnObjectError(config)
|
||||
}
|
||||
|
||||
if ('hoist' in config && config.hoist !== undefined && typeof config.hoist !== 'boolean') {
|
||||
throw new ProjectConfigInvalidValueTypeError('boolean', config.hoist)
|
||||
}
|
||||
|
||||
if ('modulesDir' in config && config.modulesDir !== undefined && typeof config.modulesDir !== 'string') {
|
||||
throw new ProjectConfigInvalidValueTypeError('string', config.modulesDir)
|
||||
}
|
||||
|
||||
if ('saveExact' in config && config.saveExact !== undefined && typeof config.saveExact !== 'boolean') {
|
||||
throw new ProjectConfigInvalidValueTypeError('boolean', config.saveExact)
|
||||
}
|
||||
|
||||
if ('savePrefix' in config && config.savePrefix !== undefined && typeof config.savePrefix !== 'string') {
|
||||
throw new ProjectConfigInvalidValueTypeError('string', config.savePrefix)
|
||||
}
|
||||
|
||||
for (const key in config) {
|
||||
if ((config as Record<string, unknown>)[key] !== undefined && !(PROJECT_CONFIG_FIELDS as string[]).includes(key)) {
|
||||
throw new ProjectConfigUnsupportedFieldError(key)
|
||||
}
|
||||
}
|
||||
|
||||
const result: ProjectConfig = config
|
||||
if (result.hoist === false) {
|
||||
return { ...result, hoistPattern: undefined }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export class ProjectConfigsIsNeitherObjectNorArrayError extends PnpmError {
|
||||
readonly configSet: unknown
|
||||
constructor (configSet: unknown) {
|
||||
super('PROJECT_CONFIGS_IS_NEITHER_OBJECT_NOR_ARRAY', `Expecting packageConfigs to be either an object or an array but received ${JSON.stringify(configSet)}`)
|
||||
this.configSet = configSet
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectConfigsArrayItemIsNotAnObjectError extends PnpmError {
|
||||
readonly item: unknown
|
||||
constructor (item: unknown) {
|
||||
super('PROJECT_CONFIGS_ARRAY_ITEM_IS_NOT_AN_OBJECT', `Expecting a packageConfigs item to be an object but received ${JSON.stringify(item)}`)
|
||||
this.item = item
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectConfigsArrayItemMatchIsNotDefinedError extends PnpmError {
|
||||
constructor () {
|
||||
super('PROJECT_CONFIGS_ARRAY_ITEM_MATCH_IS_NOT_DEFINED', 'A packageConfigs match is not defined')
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectConfigsArrayItemMatchIsNotAnArrayError extends PnpmError {
|
||||
readonly match: unknown
|
||||
constructor (match: unknown) {
|
||||
super('PROJECT_CONFIGS_ARRAY_ITEM_MATCH_IS_NOT_AN_ARRAY', `Expecting a packageConfigs match to be an array but received ${JSON.stringify(match)}`)
|
||||
this.match = match
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectConfigsMatchItemIsNotAStringError extends PnpmError {
|
||||
readonly matchItem: unknown
|
||||
constructor (matchItem: unknown) {
|
||||
super('PROJECT_CONFIGS_MATCH_ITEM_IS_NOT_A_STRING', `Expecting a match item to be a string but received ${JSON.stringify(matchItem)}`)
|
||||
this.matchItem = matchItem
|
||||
}
|
||||
}
|
||||
|
||||
const withoutMatch = omit(['match'])
|
||||
|
||||
function createProjectConfigRecordFromConfigSet (configSet: unknown): ProjectConfigRecord | undefined {
|
||||
if (configSet == null) return undefined
|
||||
if (typeof configSet !== 'object') throw new ProjectConfigsIsNeitherObjectNorArrayError(configSet)
|
||||
|
||||
const result: ProjectConfigRecord = {}
|
||||
|
||||
if (!Array.isArray(configSet)) {
|
||||
for (const projectName in configSet) {
|
||||
const projectConfig = (configSet as Record<string, unknown>)[projectName]
|
||||
result[projectName] = createProjectConfigFromRaw(projectConfig)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
for (const item of configSet as unknown[]) {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
||||
throw new ProjectConfigsArrayItemIsNotAnObjectError(item)
|
||||
}
|
||||
|
||||
if (!('match' in item)) {
|
||||
throw new ProjectConfigsArrayItemMatchIsNotDefinedError()
|
||||
}
|
||||
|
||||
if (typeof item.match !== 'object' || !Array.isArray(item.match)) {
|
||||
throw new ProjectConfigsArrayItemMatchIsNotAnArrayError(item.match)
|
||||
}
|
||||
|
||||
const projectConfig = createProjectConfigFromRaw(withoutMatch(item))
|
||||
|
||||
for (const projectName of item.match as unknown[]) {
|
||||
if (typeof projectName !== 'string') {
|
||||
throw new ProjectConfigsMatchItemIsNotAStringError(projectName)
|
||||
}
|
||||
|
||||
result[projectName] = projectConfig
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import path from 'path'
|
||||
import util from 'util'
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
import { envReplace } from '@pnpm/config.env-replace'
|
||||
import { readIniFile } from 'read-ini-file'
|
||||
import { parseField } from '@pnpm/npm-conf/lib/util.js'
|
||||
import { types } from './types.js'
|
||||
|
||||
export type LocalConfig = Record<string, string> & { hoist?: boolean }
|
||||
|
||||
export async function readLocalConfig (prefix: string): Promise<LocalConfig> {
|
||||
try {
|
||||
const ini = await readIniFile(path.join(prefix, '.npmrc')) as Record<string, string>
|
||||
for (let [key, val] of Object.entries(ini)) {
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
key = envReplace(key, process.env)
|
||||
ini[key] = parseField(types, envReplace(val, process.env), key) as any // eslint-disable-line
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
const config = camelcaseKeys(ini) as LocalConfig
|
||||
if (config.shamefullyFlatten) {
|
||||
config.hoistPattern = '*'
|
||||
// TODO: print a warning
|
||||
}
|
||||
if (config.hoist === false) {
|
||||
config.hoistPattern = ''
|
||||
}
|
||||
return config
|
||||
} catch (err: unknown) {
|
||||
if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') return {}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
548
config/config/test/projectConfig.test.ts
Normal file
548
config/config/test/projectConfig.test.ts
Normal file
@@ -0,0 +1,548 @@
|
||||
import { omit } from 'ramda'
|
||||
import {
|
||||
type Config,
|
||||
type ProjectConfig,
|
||||
type ProjectConfigMultiMatch,
|
||||
type ProjectConfigRecord,
|
||||
type ProjectConfigSet,
|
||||
} from '../src/Config.js'
|
||||
import { createProjectConfigRecord } from '../src/projectConfig.js'
|
||||
|
||||
it('returns undefined for undefined', () => {
|
||||
expect(createProjectConfigRecord({})).toBeUndefined()
|
||||
expect(createProjectConfigRecord({ packageConfigs: undefined })).toBeUndefined()
|
||||
expect(createProjectConfigRecord({ packageConfigs: null as unknown as undefined })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('errors on invalid packageConfigs', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: 0 as unknown as ProjectConfigSet,
|
||||
})).toThrow(expect.objectContaining({
|
||||
configSet: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_IS_NEITHER_OBJECT_NOR_ARRAY',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: 'some string' as unknown as ProjectConfigSet,
|
||||
})).toThrow(expect.objectContaining({
|
||||
configSet: 'some string',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_IS_NEITHER_OBJECT_NOR_ARRAY',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: true as unknown as ProjectConfigSet,
|
||||
})).toThrow(expect.objectContaining({
|
||||
configSet: true,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_IS_NEITHER_OBJECT_NOR_ARRAY',
|
||||
}))
|
||||
})
|
||||
|
||||
describe('record', () => {
|
||||
it('returns an empty record for an empty record', () => {
|
||||
expect(createProjectConfigRecord({ packageConfigs: {} })).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('returns a valid record for a valid record', () => {
|
||||
const packageConfigs: ProjectConfigRecord = {
|
||||
'project-1': {
|
||||
modulesDir: 'foo',
|
||||
},
|
||||
'project-2': {
|
||||
saveExact: true,
|
||||
},
|
||||
'project-3': {
|
||||
savePrefix: '~',
|
||||
},
|
||||
}
|
||||
expect(createProjectConfigRecord({ packageConfigs })).toStrictEqual(packageConfigs)
|
||||
})
|
||||
|
||||
it('explicitly sets hoistPattern to undefined when hoist is false', () => {
|
||||
expect(createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': { hoist: false },
|
||||
},
|
||||
})).toStrictEqual({
|
||||
'project-1': {
|
||||
hoist: false,
|
||||
hoistPattern: undefined,
|
||||
},
|
||||
} as ProjectConfigRecord)
|
||||
})
|
||||
|
||||
it('errors on invalid project config', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': 0 as unknown as ProjectConfig,
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
actualRawConfig: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_NOT_AN_OBJECT',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': 'some string' as unknown as ProjectConfig,
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
actualRawConfig: 'some string',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_NOT_AN_OBJECT',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': true as unknown as ProjectConfig,
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
actualRawConfig: true,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_NOT_AN_OBJECT',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': null as unknown as ProjectConfig,
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
actualRawConfig: null,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_NOT_AN_OBJECT',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': [0, 1, 2] as unknown as ProjectConfig,
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
actualRawConfig: [0, 1, 2],
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_NOT_AN_OBJECT',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on invalid hoist', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': { hoist: 'invalid' as unknown as boolean },
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'boolean',
|
||||
actualValue: 'invalid',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': { hoist: 0 as unknown as boolean },
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'boolean',
|
||||
actualValue: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on invalid modulesDir', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': { modulesDir: 0 as unknown as string },
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'string',
|
||||
actualValue: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': { modulesDir: true as unknown as string },
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'string',
|
||||
actualValue: true,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on invalid saveExact', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': { saveExact: 'invalid' as unknown as boolean },
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'boolean',
|
||||
actualValue: 'invalid',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': { saveExact: 0 as unknown as boolean },
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'boolean',
|
||||
actualValue: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on invalid savePrefix', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': { savePrefix: 0 as unknown as string },
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'string',
|
||||
actualValue: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': { savePrefix: false as unknown as string },
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'string',
|
||||
actualValue: false,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on unsupported fields', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': {
|
||||
ignoreScripts: true,
|
||||
} as Partial<Config>,
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
field: 'ignoreScripts',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_UNSUPPORTED_FIELD',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': {
|
||||
hoistPattern: ['*'],
|
||||
} as Partial<Config>,
|
||||
},
|
||||
})).toThrow(expect.objectContaining({
|
||||
field: 'hoistPattern',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_UNSUPPORTED_FIELD',
|
||||
}))
|
||||
})
|
||||
|
||||
it('does not error on unsupported but undefined fields', () => {
|
||||
expect(createProjectConfigRecord({
|
||||
packageConfigs: {
|
||||
'project-1': {
|
||||
ignoreScripts: undefined,
|
||||
hoistPattern: undefined,
|
||||
} as Partial<Config>,
|
||||
},
|
||||
})).toStrictEqual({
|
||||
'project-1': {
|
||||
ignoreScripts: undefined,
|
||||
hoistPattern: undefined,
|
||||
} as Partial<Config>,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('array', () => {
|
||||
type ProjectConfigWithExtraFields = Pick<ProjectConfigMultiMatch, 'match'> & Partial<Config>
|
||||
|
||||
it('returns an empty record for an empty array', () => {
|
||||
expect(createProjectConfigRecord({ packageConfigs: [] })).toStrictEqual({})
|
||||
})
|
||||
|
||||
it('returns a map of project-specific settings for a non-empty array', () => {
|
||||
const withoutMatch: (withMatch: ProjectConfigMultiMatch) => ProjectConfig = omit(['match'])
|
||||
|
||||
const packageConfigs = [
|
||||
{
|
||||
match: ['project-1'],
|
||||
modulesDir: 'foo',
|
||||
},
|
||||
{
|
||||
match: ['project-2', 'project-3'],
|
||||
saveExact: true,
|
||||
},
|
||||
{
|
||||
match: ['project-4', 'project-5', 'project-6'],
|
||||
savePrefix: '~',
|
||||
},
|
||||
] as const satisfies ProjectConfigMultiMatch[]
|
||||
|
||||
const record: ProjectConfigRecord | undefined = createProjectConfigRecord({ packageConfigs })
|
||||
|
||||
expect(record).toStrictEqual({
|
||||
'project-1': withoutMatch(packageConfigs[0]),
|
||||
'project-2': withoutMatch(packageConfigs[1]),
|
||||
'project-3': withoutMatch(packageConfigs[1]),
|
||||
'project-4': withoutMatch(packageConfigs[2]),
|
||||
'project-5': withoutMatch(packageConfigs[2]),
|
||||
'project-6': withoutMatch(packageConfigs[2]),
|
||||
} as ProjectConfigRecord)
|
||||
|
||||
expect(createProjectConfigRecord({ packageConfigs: record })).toStrictEqual(record)
|
||||
})
|
||||
|
||||
it('explicitly sets hoistPattern to undefined when hoist is false', () => {
|
||||
expect(createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
hoist: false,
|
||||
}],
|
||||
})).toStrictEqual({
|
||||
'project-1': {
|
||||
hoist: false,
|
||||
hoistPattern: undefined,
|
||||
},
|
||||
} as ProjectConfigRecord)
|
||||
})
|
||||
|
||||
it('errors on invalid array items', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [0 as unknown as ProjectConfigMultiMatch],
|
||||
})).toThrow(expect.objectContaining({
|
||||
item: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_IS_NOT_AN_OBJECT',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: ['some string' as unknown as ProjectConfigMultiMatch],
|
||||
})).toThrow(expect.objectContaining({
|
||||
item: 'some string',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_IS_NOT_AN_OBJECT',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [true as unknown as ProjectConfigMultiMatch],
|
||||
})).toThrow(expect.objectContaining({
|
||||
item: true,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_IS_NOT_AN_OBJECT',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [null as unknown as ProjectConfigMultiMatch],
|
||||
})).toThrow(expect.objectContaining({
|
||||
item: null,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_IS_NOT_AN_OBJECT',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on undefined match', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{} as ProjectConfigMultiMatch],
|
||||
})).toThrow(expect.objectContaining({
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_MATCH_IS_NOT_DEFINED',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on non-array match', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: 0 as unknown as string[],
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
match: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_MATCH_IS_NOT_AN_ARRAY',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: 'some string' as unknown as string[],
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
match: 'some string',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_MATCH_IS_NOT_AN_ARRAY',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: true as unknown as string[],
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
match: true,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_MATCH_IS_NOT_AN_ARRAY',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: undefined as unknown as string[],
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
match: undefined,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_MATCH_IS_NOT_AN_ARRAY',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: null as unknown as string[],
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
match: null,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_ARRAY_ITEM_MATCH_IS_NOT_AN_ARRAY',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on non-string match item', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: [0 as unknown as string],
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
matchItem: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_MATCH_ITEM_IS_NOT_A_STRING',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: [null as unknown as string],
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
matchItem: null,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_MATCH_ITEM_IS_NOT_A_STRING',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: [{} as unknown as string],
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
matchItem: {},
|
||||
code: 'ERR_PNPM_PROJECT_CONFIGS_MATCH_ITEM_IS_NOT_A_STRING',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on invalid hoist', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
hoist: 'invalid' as unknown as boolean,
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'boolean',
|
||||
actualValue: 'invalid',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
hoist: 0 as unknown as boolean,
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'boolean',
|
||||
actualValue: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on invalid modulesDir', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
modulesDir: 0 as unknown as string,
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'string',
|
||||
actualValue: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
modulesDir: true as unknown as string,
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'string',
|
||||
actualValue: true,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on invalid saveExact', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
saveExact: 'invalid' as unknown as boolean,
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'boolean',
|
||||
actualValue: 'invalid',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
saveExact: 0 as unknown as boolean,
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'boolean',
|
||||
actualValue: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on invalid savePrefix', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
savePrefix: 0 as unknown as string,
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'string',
|
||||
actualValue: 0,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
savePrefix: false as unknown as string,
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
expectedType: 'string',
|
||||
actualValue: false,
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_INVALID_VALUE_TYPE',
|
||||
}))
|
||||
})
|
||||
|
||||
it('errors on unsupported fields', () => {
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
ignoreScripts: true,
|
||||
} as ProjectConfigWithExtraFields],
|
||||
})).toThrow(expect.objectContaining({
|
||||
field: 'ignoreScripts',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_UNSUPPORTED_FIELD',
|
||||
}))
|
||||
|
||||
expect(() => createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
hoistPattern: ['*'],
|
||||
}],
|
||||
})).toThrow(expect.objectContaining({
|
||||
field: 'hoistPattern',
|
||||
code: 'ERR_PNPM_PROJECT_CONFIG_UNSUPPORTED_FIELD',
|
||||
}))
|
||||
})
|
||||
|
||||
it('does not error on unsupported but undefined fields', () => {
|
||||
expect(createProjectConfigRecord({
|
||||
packageConfigs: [{
|
||||
match: ['project-1'],
|
||||
ignoreScripts: undefined,
|
||||
hoistPattern: undefined,
|
||||
} as ProjectConfigWithExtraFields],
|
||||
})).toStrictEqual({
|
||||
'project-1': {
|
||||
ignoreScripts: undefined,
|
||||
hoistPattern: undefined,
|
||||
} as Partial<Config>,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { readLocalConfig } from '@pnpm/config'
|
||||
|
||||
const f = fixtures(import.meta.dirname)
|
||||
|
||||
test('readLocalConfig parse number field', async () => {
|
||||
const config = await readLocalConfig(f.find('has-number-setting'))
|
||||
expect(typeof config.childConcurrency).toBe('number')
|
||||
})
|
||||
@@ -6,14 +6,13 @@ import {
|
||||
} from '@pnpm/cli-utils'
|
||||
import {
|
||||
type Config,
|
||||
readLocalConfig,
|
||||
createProjectConfigRecord,
|
||||
getWorkspaceConcurrency,
|
||||
} from '@pnpm/config'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { sortPackages } from '@pnpm/sort-packages'
|
||||
import { createOrConnectStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
|
||||
import { type Project, type ProjectManifest, type ProjectRootDir } from '@pnpm/types'
|
||||
import mem from 'mem'
|
||||
import pLimit from 'p-limit'
|
||||
import { rebuildProjects as rebuildAll, type RebuildOptions, rebuildSelectedPkgs } from './implementation/index.js'
|
||||
|
||||
@@ -25,6 +24,7 @@ type RecursiveRebuildOpts = CreateStoreControllerOptions & Pick<Config,
|
||||
| 'lockfileDir'
|
||||
| 'lockfileOnly'
|
||||
| 'nodeLinker'
|
||||
| 'packageConfigs'
|
||||
| 'rawLocalConfig'
|
||||
| 'registries'
|
||||
| 'rootProjectManifest'
|
||||
@@ -74,7 +74,7 @@ export async function recursiveRebuild (
|
||||
|
||||
const result: RecursiveSummary = {}
|
||||
|
||||
const memReadLocalConfig = mem(readLocalConfig)
|
||||
const projectConfigRecord = createProjectConfigRecord(opts) ?? {}
|
||||
|
||||
async function getImporters () {
|
||||
const importers = [] as Array<{ buildIndex: number, manifest: ProjectManifest, rootDir: ProjectRootDir }>
|
||||
@@ -121,7 +121,8 @@ export async function recursiveRebuild (
|
||||
return
|
||||
}
|
||||
result[rootDir] = { status: 'running' }
|
||||
const localConfig = await memReadLocalConfig(rootDir)
|
||||
const { manifest } = opts.selectedProjectsGraph[rootDir].package
|
||||
const localConfig = manifest.name ? projectConfigRecord[manifest.name] : undefined
|
||||
await rebuild(
|
||||
[
|
||||
{
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"get-npm-tarball-url": "catalog:",
|
||||
"is-subdir": "catalog:",
|
||||
"load-json-file": "catalog:",
|
||||
"mem": "catalog:",
|
||||
"normalize-path": "catalog:",
|
||||
"p-filter": "catalog:",
|
||||
"p-limit": "catalog:",
|
||||
|
||||
@@ -332,6 +332,7 @@ export type InstallCommandOptions = Pick<Config,
|
||||
| 'updateConfig'
|
||||
| 'overrides'
|
||||
| 'supportedArchitectures'
|
||||
| 'packageConfigs'
|
||||
> & CreateStoreControllerOptions & {
|
||||
argv: {
|
||||
original: string[]
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
import {
|
||||
type Config,
|
||||
type OptionsFromRootManifest,
|
||||
type ProjectConfig,
|
||||
createProjectConfigRecord,
|
||||
getOptionsFromRootManifest,
|
||||
getWorkspaceConcurrency,
|
||||
readLocalConfig,
|
||||
} from '@pnpm/config'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { arrayOfWorkspacePackagesToMap } from '@pnpm/get-context'
|
||||
@@ -45,7 +46,6 @@ import {
|
||||
type WorkspacePackages,
|
||||
} from '@pnpm/core'
|
||||
import isSubdir from 'is-subdir'
|
||||
import mem from 'mem'
|
||||
import pFilter from 'p-filter'
|
||||
import pLimit from 'p-limit'
|
||||
import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies.js'
|
||||
@@ -84,6 +84,7 @@ export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
|
||||
| 'sharedWorkspaceLockfile'
|
||||
| 'tag'
|
||||
| 'cleanupUnusedCatalogs'
|
||||
| 'packageConfigs'
|
||||
> & {
|
||||
include?: IncludedDependencies
|
||||
includeDirect?: IncludedDependencies
|
||||
@@ -166,7 +167,11 @@ export async function recursive (
|
||||
|
||||
const result: RecursiveSummary = {}
|
||||
|
||||
const memReadLocalConfig = mem(readLocalConfig)
|
||||
const projectConfigRecord = createProjectConfigRecord(opts)
|
||||
const getProjectConfig: (manifest: Pick<ProjectManifest, 'name'>) => ProjectConfig | undefined =
|
||||
projectConfigRecord
|
||||
? manifest => manifest.name ? projectConfigRecord[manifest.name] : undefined
|
||||
: () => undefined
|
||||
|
||||
const updateToLatest = opts.update && opts.latest
|
||||
const includeDirect = opts.includeDirect ?? {
|
||||
@@ -208,9 +213,9 @@ export async function recursive (
|
||||
}
|
||||
const mutatedImporters = [] as MutatedProject[]
|
||||
await Promise.all(importers.map(async ({ rootDir }) => {
|
||||
const localConfig = await memReadLocalConfig(rootDir)
|
||||
const modulesDir = localConfig.modulesDir ?? opts.modulesDir
|
||||
const { manifest } = manifestsByPath[rootDir]
|
||||
const localConfig = getProjectConfig(manifest) ?? {}
|
||||
const modulesDir = localConfig.modulesDir ?? opts.modulesDir
|
||||
let currentInput = [...params]
|
||||
if (updateMatch != null) {
|
||||
currentInput = matchDependencies(updateMatch, manifest, includeDirect)
|
||||
@@ -386,7 +391,7 @@ export async function recursive (
|
||||
break
|
||||
}
|
||||
|
||||
const localConfig = await memReadLocalConfig(rootDir)
|
||||
const localConfig = getProjectConfig(manifest) ?? {}
|
||||
const {
|
||||
updatedCatalogs: newCatalogsAddition,
|
||||
updatedManifest: newManifest,
|
||||
|
||||
@@ -621,7 +621,7 @@ test('recursive install on workspace with custom lockfile-dir', async () => {
|
||||
expect(Object.keys(lockfile.importers!)).toStrictEqual(['../project-1', '../project-2'])
|
||||
})
|
||||
|
||||
test('recursive install in a monorepo with different modules directories', async () => {
|
||||
test('recursive install in a monorepo with different modules directories specified by packageConfigs record', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
name: 'project-1',
|
||||
@@ -640,8 +640,6 @@ test('recursive install in a monorepo with different modules directories', async
|
||||
},
|
||||
},
|
||||
])
|
||||
fs.writeFileSync('project-1/.npmrc', 'modules-dir=modules_1', 'utf8')
|
||||
fs.writeFileSync('project-2/.npmrc', 'modules-dir=modules_2', 'utf8')
|
||||
|
||||
const { allProjects, allProjectsGraph, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [])
|
||||
await install.handler({
|
||||
@@ -652,16 +650,28 @@ test('recursive install in a monorepo with different modules directories', async
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
workspaceDir: process.cwd(),
|
||||
packageConfigs: {
|
||||
'project-1': { modulesDir: 'modules_1' },
|
||||
'project-2': { modulesDir: 'modules_2' },
|
||||
},
|
||||
})
|
||||
|
||||
projects['project-1'].has('is-positive', 'modules_1')
|
||||
projects['project-2'].has('is-positive', 'modules_2')
|
||||
})
|
||||
|
||||
test('recursive install in a monorepo with parsing env variables', async () => {
|
||||
test('recursive install in a monorepo with different modules directories specified by packageConfigs multi match', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
name: 'project',
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-2',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
@@ -670,10 +680,6 @@ test('recursive install in a monorepo with parsing env variables', async () => {
|
||||
},
|
||||
])
|
||||
|
||||
process.env['SOME_NAME'] = 'some_name'
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
fs.writeFileSync('project/.npmrc', 'modules-dir=${SOME_NAME}_modules', 'utf8')
|
||||
|
||||
const { allProjects, allProjectsGraph, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [])
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
@@ -683,9 +689,14 @@ test('recursive install in a monorepo with parsing env variables', async () => {
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
workspaceDir: process.cwd(),
|
||||
packageConfigs: [{
|
||||
match: ['project-1', 'project-2'],
|
||||
modulesDir: 'different_node_modules',
|
||||
}],
|
||||
})
|
||||
|
||||
projects['project'].has('is-positive', `${process.env['SOME_NAME']}_modules`)
|
||||
projects['project-1'].has('is-positive', 'different_node_modules')
|
||||
projects['project-2'].has('is-positive', 'different_node_modules')
|
||||
})
|
||||
|
||||
test('prefer-workspace-package', async () => {
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -6006,9 +6006,6 @@ importers:
|
||||
load-json-file:
|
||||
specifier: 'catalog:'
|
||||
version: 7.0.1
|
||||
mem:
|
||||
specifier: 'catalog:'
|
||||
version: 10.0.0
|
||||
normalize-path:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.0
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// cspell:ignore buildscript
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { type Config } from '@pnpm/config'
|
||||
import { LOCKFILE_VERSION, WANTED_LOCKFILE } from '@pnpm/constants'
|
||||
import { type WorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
import { findWorkspacePackages } from '@pnpm/workspace.find-packages'
|
||||
import { type LockfileFile } from '@pnpm/lockfile.types'
|
||||
import { readModulesManifest } from '@pnpm/modules-yaml'
|
||||
@@ -662,17 +664,13 @@ test('recursive install with link-workspace-packages and shared-workspace-lockfi
|
||||
},
|
||||
])
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
fs.writeFileSync(
|
||||
'is-positive/.npmrc',
|
||||
'save-exact = true',
|
||||
'utf8'
|
||||
)
|
||||
fs.writeFileSync(
|
||||
'project-1/.npmrc',
|
||||
'save-prefix = ~',
|
||||
'utf8'
|
||||
)
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
packages: ['**', '!store/**'],
|
||||
packageConfigs: {
|
||||
'is-positive': { saveExact: true },
|
||||
'project-1': { savePrefix: '~' },
|
||||
},
|
||||
} satisfies Partial<Config> & WorkspaceManifest)
|
||||
|
||||
await execPnpm(['recursive', 'install', '--link-workspace-packages', '--shared-workspace-lockfile=true', '--store-dir', 'store'])
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { type Config } from '@pnpm/config'
|
||||
import { STORE_VERSION } from '@pnpm/constants'
|
||||
import { preparePackages } from '@pnpm/prepare'
|
||||
import { type WorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
import { type LockfileFile } from '@pnpm/lockfile.types'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import { isCI } from 'ci-info'
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
|
||||
const skipOnWindows = isWindows() ? test.skip : test
|
||||
|
||||
test('recursive installation with package-specific .npmrc', async () => {
|
||||
test('recursive installation with packageConfigs', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
name: 'project-1',
|
||||
@@ -36,7 +38,13 @@ test('recursive installation with package-specific .npmrc', async () => {
|
||||
},
|
||||
])
|
||||
|
||||
fs.writeFileSync('project-2/.npmrc', 'hoist = false', 'utf8')
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
packages: ['*'],
|
||||
packageConfigs: {
|
||||
'project-2': { hoist: false },
|
||||
},
|
||||
sharedWorkspaceLockfile: false,
|
||||
} satisfies Partial<Config> & WorkspaceManifest)
|
||||
|
||||
await execPnpm(['recursive', 'install'])
|
||||
|
||||
@@ -50,7 +58,7 @@ test('recursive installation with package-specific .npmrc', async () => {
|
||||
expect(modulesYaml2?.hoistPattern).toBeFalsy()
|
||||
})
|
||||
|
||||
test('workspace .npmrc is always read', async () => {
|
||||
test('workspace packageConfigs is always read', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
location: 'workspace/project-1',
|
||||
@@ -79,10 +87,12 @@ test('workspace .npmrc is always read', async () => {
|
||||
const storeDir = path.resolve('../store')
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
packages: ['workspace/*'],
|
||||
shamefullyFlatten: true,
|
||||
packageConfigs: {
|
||||
'project-2': { hoist: false },
|
||||
},
|
||||
shamefullyHoist: true,
|
||||
sharedWorkspaceLockfile: false,
|
||||
})
|
||||
fs.writeFileSync('workspace/project-2/.npmrc', 'hoist=false', 'utf8')
|
||||
} satisfies Partial<Config> & WorkspaceManifest)
|
||||
|
||||
process.chdir('workspace/project-1')
|
||||
await execPnpm(['install', '--store-dir', storeDir, '--filter', '.'])
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
import { type Config } from '@pnpm/config'
|
||||
import { preparePackages } from '@pnpm/prepare'
|
||||
import { type WorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
import { addDistTag } from '@pnpm/registry-mock'
|
||||
import { execPnpm } from '../utils/index.js'
|
||||
|
||||
@@ -35,17 +37,13 @@ test.skip('recursive update --latest should update deps with correct specs', asy
|
||||
},
|
||||
])
|
||||
|
||||
fs.writeFileSync(
|
||||
'project-2/.npmrc',
|
||||
'save-exact = true',
|
||||
'utf8'
|
||||
)
|
||||
|
||||
fs.writeFileSync(
|
||||
'project-3/.npmrc',
|
||||
'save-prefix = ~',
|
||||
'utf8'
|
||||
)
|
||||
writeYamlFile('pnpm-workspace.yaml', {
|
||||
packages: ['*'],
|
||||
packageConfigs: {
|
||||
'project-2': { saveExact: true },
|
||||
'project-3': { savePrefix: '~' },
|
||||
},
|
||||
} satisfies Partial<Config> & WorkspaceManifest)
|
||||
|
||||
await execPnpm(['recursive', 'update', '--latest'])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user