feat: config update hook (#9325)

This commit is contained in:
Zoltan Kochan
2025-03-31 08:08:32 +02:00
committed by GitHub
parent 1b1ed10e1a
commit 1413c25435
26 changed files with 510 additions and 284 deletions

View File

@@ -0,0 +1,20 @@
---
"@pnpm/plugin-commands-installation": major
"@pnpm/pnpmfile": minor
"@pnpm/cli-utils": minor
"pnpm": minor
---
**Experimental.** A new hook is supported for updating configuration settings. The hook can be provided via `.pnpmfile.cjs`. For example:
```js
module.exports = {
hooks: {
updateConfig: (config) => ({
...config,
nodeLinker: 'hoisted',
}),
},
}
```

View File

@@ -0,0 +1,5 @@
---
"@pnpm/config.deps-installer": major
---
Initial release.

View File

@@ -245,6 +245,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest,
scripts = { ...manifest.scripts }
break
case '@pnpm/exec.build-commands':
case '@pnpm/config.deps-installer':
case '@pnpm/headless':
case '@pnpm/outdated':
case '@pnpm/package-requester':

View File

@@ -32,11 +32,14 @@
"dependencies": {
"@pnpm/cli-meta": "workspace:*",
"@pnpm/config": "workspace:*",
"@pnpm/config.deps-installer": "workspace:*",
"@pnpm/default-reporter": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/manifest-utils": "workspace:*",
"@pnpm/package-is-installable": "workspace:*",
"@pnpm/pnpmfile": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/store-connection-manager": "workspace:*",
"@pnpm/types": "workspace:*",
"chalk": "catalog:",
"load-json-file": "catalog:"

View File

@@ -1,6 +1,9 @@
import { packageManager } from '@pnpm/cli-meta'
import { getConfig as _getConfig, type CliOptions, type Config } from '@pnpm/config'
import { formatWarn } from '@pnpm/default-reporter'
import { createOrConnectStoreController } from '@pnpm/store-connection-manager'
import { installConfigDeps } from '@pnpm/config.deps-installer'
import { requireHooks } from '@pnpm/pnpmfile'
export async function getConfig (
cliOptions: CliOptions,
@@ -13,7 +16,7 @@ export async function getConfig (
ignoreNonAuthSettingsFromLocal?: boolean
}
): Promise<Config> {
const { config, warnings } = await _getConfig({
let { config, warnings } = await _getConfig({
cliOptions,
globalDirShouldAllowWrite: opts.globalDirShouldAllowWrite,
packageManager,
@@ -23,6 +26,21 @@ export async function getConfig (
ignoreNonAuthSettingsFromLocal: opts.ignoreNonAuthSettingsFromLocal,
})
config.cliOptions = cliOptions
if (config.configDependencies) {
const store = await createOrConnectStoreController(config)
await installConfigDeps(config.configDependencies, {
registries: config.registries,
rootDir: config.lockfileDir ?? config.rootProjectManifestDir,
store: store.ctrl,
})
}
if (!config.ignorePnpmfile) {
config.hooks = requireHooks(config.lockfileDir ?? config.dir, config)
if (config.hooks?.updateConfig) {
const updateConfigResult = config.hooks.updateConfig(config)
config = updateConfigResult instanceof Promise ? await updateConfigResult : updateConfigResult
}
}
if (opts.excludeReporter) {
delete config.reporter // This is a silly workaround because @pnpm/core expects a function as opts.reporter

View File

@@ -15,9 +15,15 @@
{
"path": "../../config/config"
},
{
"path": "../../config/deps-installer"
},
{
"path": "../../config/package-is-installable"
},
{
"path": "../../hooks/pnpmfile"
},
{
"path": "../../packages/error"
},
@@ -33,6 +39,9 @@
{
"path": "../../pkg-manifest/read-project-manifest"
},
{
"path": "../../store/store-connection-manager"
},
{
"path": "../cli-meta"
},

View File

@@ -0,0 +1,17 @@
# @pnpm/config.deps-installer
> Installer for configurational dependencies
<!--@shields('npm')-->
[![npm version](https://img.shields.io/npm/v/@pnpm/config.deps-installer.svg)](https://www.npmjs.com/package/@pnpm/config.deps-installer)
<!--/@-->
## Installation
```sh
pnpm add @pnpm/config.deps-installer
```
## License
MIT

View File

@@ -0,0 +1,58 @@
{
"name": "@pnpm/config.deps-installer",
"version": "1000.0.0-0",
"description": "Installer for configurational dependencies",
"keywords": [
"pnpm",
"pnpm10",
"config"
],
"license": "MIT",
"funding": "https://opencollective.com/pnpm",
"repository": "https://github.com/pnpm/pnpm/blob/main/config/deps-installer",
"homepage": "https://github.com/pnpm/pnpm/blob/main/config/deps-installer#readme",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"!*.map"
],
"scripts": {
"prepublishOnly": "pnpm run compile",
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
"test-with-preview": "ts-node test",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"start": "tsc --watch",
"compile": "tsc --build && pnpm run lint --fix"
},
"dependencies": {
"@pnpm/error": "workspace:*",
"@pnpm/package-store": "workspace:*",
"@pnpm/pick-registry-for-package": "workspace:*",
"@pnpm/read-modules-dir": "workspace:*",
"@pnpm/read-package-json": "workspace:*",
"@pnpm/types": "workspace:*",
"@zkochan/rimraf": "catalog:",
"get-npm-tarball-url": "catalog:"
},
"devDependencies": {
"@pnpm/config.deps-installer": "workspace:*",
"@pnpm/prepare": "workspace:*",
"@pnpm/registry-mock": "catalog:",
"@pnpm/testing.temp-store": "workspace:*",
"load-json-file": "catalog:"
},
"engines": {
"node": ">=18.12"
},
"jest": {
"preset": "@pnpm/jest-config/with-registry"
}
}

View File

@@ -0,0 +1,104 @@
import fs from 'fs'
import { prepareEmpty } from '@pnpm/prepare'
import { getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import { createTempStore } from '@pnpm/testing.temp-store'
import { installConfigDeps } from '@pnpm/config.deps-installer'
import { sync as loadJsonFile } from 'load-json-file'
const registry = `http://localhost:${REGISTRY_MOCK_PORT}/`
test('configuration dependency is installed', async () => {
prepareEmpty()
const { storeController } = createTempStore()
let configDeps: Record<string, string> = {
'@pnpm.e2e/foo': `100.0.0+${getIntegrity('@pnpm.e2e/foo', '100.0.0')}`,
}
await installConfigDeps(configDeps, {
registries: {
default: registry,
},
rootDir: process.cwd(),
store: storeController,
})
{
const configDepManifest = loadJsonFile<{ name: string, version: string }>('node_modules/.pnpm-config/@pnpm.e2e/foo/package.json')
expect(configDepManifest.name).toBe('@pnpm.e2e/foo')
expect(configDepManifest.version).toBe('100.0.0')
}
// Dependency is updated
configDeps!['@pnpm.e2e/foo'] = `100.1.0+${getIntegrity('@pnpm.e2e/foo', '100.1.0')}`
await installConfigDeps(configDeps, {
registries: {
default: registry,
},
rootDir: process.cwd(),
store: storeController,
})
{
const configDepManifest = loadJsonFile<{ name: string, version: string }>('node_modules/.pnpm-config/@pnpm.e2e/foo/package.json')
expect(configDepManifest.name).toBe('@pnpm.e2e/foo')
expect(configDepManifest.version).toBe('100.1.0')
}
// Dependency is removed
configDeps! = {}
await installConfigDeps(configDeps, {
registries: {
default: registry,
},
rootDir: process.cwd(),
store: storeController,
})
expect(fs.existsSync('node_modules/.pnpm-config/@pnpm.e2e/foo/package.json')).toBeFalsy()
})
test('installation fails if the checksum of the config dependency is invalid', async () => {
prepareEmpty()
const { storeController } = createTempStore({
clientOptions: {
retry: {
retries: 0,
},
},
})
const configDeps: Record<string, string> = {
'@pnpm.e2e/foo': '100.0.0+sha512-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000==',
}
await expect(installConfigDeps(configDeps, {
registries: {
default: registry,
},
rootDir: process.cwd(),
store: storeController,
})).rejects.toThrow('Got unexpected checksum for')
})
test('installation fails if the config dependency does not have a checksum', async () => {
prepareEmpty()
const { storeController } = createTempStore({
clientOptions: {
retry: {
retries: 0,
},
},
})
const configDeps: Record<string, string> = {
'@pnpm.e2e/foo': '100.0.0',
}
await expect(installConfigDeps(configDeps, {
registries: {
default: registry,
},
rootDir: process.cwd(),
store: storeController,
})).rejects.toThrow("doesn't have an integrity checksum")
})

View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "../test.lib",
"rootDir": "."
},
"include": [
"**/*.ts",
"../../../__typings__/**/*.d.ts"
],
"references": [
{
"path": ".."
}
]
}

View File

@@ -0,0 +1,37 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../__utils__/prepare"
},
{
"path": "../../fs/read-modules-dir"
},
{
"path": "../../packages/error"
},
{
"path": "../../packages/types"
},
{
"path": "../../pkg-manifest/read-package-json"
},
{
"path": "../../store/package-store"
},
{
"path": "../../testing/temp-store"
},
{
"path": "../pick-registry-for-package"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

View File

@@ -16,4 +16,6 @@ export interface Hooks {
filterLog?: (log: Log) => boolean
importPackage?: ImportIndexedPackageAsync
fetchers?: CustomFetchers
// eslint-disable-next-line
updateConfig?: (config: any) => any
}

View File

@@ -1,4 +1,5 @@
import type { PreResolutionHookContext, PreResolutionHookLogger } from '@pnpm/hooks.types'
import { PnpmError } from '@pnpm/error'
import { hookLogger } from '@pnpm/core-loggers'
import { createHashFromFile } from '@pnpm/crypto.hash'
import pathAbsolute from 'path-absolute'
@@ -20,6 +21,7 @@ export interface CookedHooks {
preResolution?: Cook<Required<Hooks>['preResolution']>
afterAllResolved?: Array<Cook<Required<Hooks>['afterAllResolved']>>
filterLog?: Array<Cook<Required<Hooks>['filterLog']>>
updateConfig?: Hooks['updateConfig']
importPackage?: ImportIndexedPackageAsync
fetchers?: CustomFetchers
calculatePnpmfileChecksum?: () => Promise<string | undefined>
@@ -79,6 +81,16 @@ export function requireHooks (
: undefined
cookedHooks.fetchers = globalHooks.fetchers
if (hooks.updateConfig != null) {
const updateConfig = hooks.updateConfig
cookedHooks.updateConfig = (config) => {
const updatedConfig = updateConfig(config)
if (updatedConfig == null) {
throw new PnpmError('CONFIG_IS_UNDEFINED', 'The updateConfig hook returned undefined')
}
return updatedConfig
}
}
return cookedHooks
}

View File

@@ -0,0 +1,5 @@
module.exports = {
hooks: {
updateConfig: () => undefined,
},
}

View File

@@ -62,3 +62,9 @@ test('calculatePnpmfileChecksum is undefined if pnpmfile even when it exports un
const hooks = requireHooks(__dirname, { pnpmfile })
expect(hooks.calculatePnpmfileChecksum).toBeUndefined()
})
test('updateConfig throws an error if it returns undefined', async () => {
const pnpmfile = path.join(__dirname, '__fixtures__/updateConfigReturnsUndefined.js')
const hooks = requireHooks(__dirname, { pnpmfile })
expect(() => hooks.updateConfig!({})).toThrow('The updateConfig hook returned undefined')
})

View File

@@ -12,7 +12,6 @@ import { filterDependenciesByType } from '@pnpm/manifest-utils'
import { findWorkspacePackages } from '@pnpm/workspace.find-packages'
import { type LockfileObject } from '@pnpm/lockfile.types'
import { rebuildProjects } from '@pnpm/plugin-commands-rebuild'
import { requireHooks } from '@pnpm/pnpmfile'
import { createOrConnectStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
import { type IncludedDependencies, type Project, type ProjectsGraph, type ProjectRootDir, type PrepareExecutionEnv } from '@pnpm/types'
import {
@@ -40,7 +39,6 @@ import {
recursive,
} from './recursive'
import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies'
import { installConfigDeps } from './installConfigDeps'
const OVERWRITE_UPDATE_OPTIONS = {
allowNew: true,
@@ -172,20 +170,7 @@ when running add/update with the --workspace option')
// @ts-expect-error
opts['preserveWorkspaceProtocol'] = !opts.linkWorkspacePackages
}
let store = await createOrConnectStoreController(opts)
if (opts.configDependencies) {
await installConfigDeps(opts.configDependencies, {
registries: opts.registries,
rootDir: opts.lockfileDir ?? opts.rootProjectManifestDir,
store: store.ctrl,
})
}
if (!opts.ignorePnpmfile && !opts.hooks) {
opts.hooks = requireHooks(opts.lockfileDir ?? opts.dir, opts)
if (opts.hooks.fetchers != null || opts.hooks.importPackage != null) {
store = await createOrConnectStoreController(opts)
}
}
const store = await createOrConnectStoreController(opts)
const includeDirect = opts.includeDirect ?? {
dependencies: true,
devDependencies: true,

View File

@@ -13,13 +13,11 @@ import { getAllDependenciesFromManifest } from '@pnpm/manifest-utils'
import { createOrConnectStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
import { type DependenciesField, type ProjectRootDir } from '@pnpm/types'
import { mutateModulesInSingleProject } from '@pnpm/core'
import { requireHooks } from '@pnpm/pnpmfile'
import pick from 'ramda/src/pick'
import without from 'ramda/src/without'
import renderHelp from 'render-help'
import { getSaveType } from './getSaveType'
import { recursive } from './recursive'
import { installConfigDeps } from './installConfigDeps'
class RemoveMissingDepsError extends PnpmError {
constructor (
@@ -164,20 +162,7 @@ export async function handler (
devDependencies: opts.dev !== false,
optionalDependencies: opts.optional !== false,
}
let store = await createOrConnectStoreController(opts)
if (opts.configDependencies) {
await installConfigDeps(opts.configDependencies, {
registries: opts.registries,
rootDir: opts.lockfileDir ?? opts.rootProjectManifestDir,
store: store.ctrl,
})
}
if (!opts.ignorePnpmfile) {
opts.hooks = requireHooks(opts.lockfileDir ?? opts.dir, opts)
if (opts.hooks.fetchers != null || opts.hooks.importPackage != null) {
store = await createOrConnectStoreController(opts)
}
}
const store = await createOrConnectStoreController(opts)
if (opts.recursive && (opts.allProjects != null) && (opts.selectedProjectsGraph != null) && opts.workspaceDir) {
await recursive(opts.allProjects, params, {
...opts,

View File

@@ -1,205 +0,0 @@
import fs from 'fs'
import { add, install } from '@pnpm/plugin-commands-installation'
import { prepare } from '@pnpm/prepare'
import { getIntegrity } from '@pnpm/registry-mock'
import { type ProjectManifest } from '@pnpm/types'
import { sync as rimraf } from '@zkochan/rimraf'
import { sync as loadJsonFile } from 'load-json-file'
import { DEFAULT_OPTS } from './utils'
test('configuration dependency is installed', async () => {
const rootProjectManifest: ProjectManifest = {
pnpm: {
configDependencies: {
'@pnpm.e2e/foo': `100.0.0+${getIntegrity('@pnpm.e2e/foo', '100.0.0')}`,
},
},
}
prepare(rootProjectManifest)
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
})
{
const configDepManifest = loadJsonFile<{ name: string, version: string }>('node_modules/.pnpm-config/@pnpm.e2e/foo/package.json')
expect(configDepManifest.name).toBe('@pnpm.e2e/foo')
expect(configDepManifest.version).toBe('100.0.0')
}
// Dependency is updated
rootProjectManifest.pnpm!.configDependencies!['@pnpm.e2e/foo'] = `100.1.0+${getIntegrity('@pnpm.e2e/foo', '100.1.0')}`
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
})
{
const configDepManifest = loadJsonFile<{ name: string, version: string }>('node_modules/.pnpm-config/@pnpm.e2e/foo/package.json')
expect(configDepManifest.name).toBe('@pnpm.e2e/foo')
expect(configDepManifest.version).toBe('100.1.0')
}
// Dependency is removed
rootProjectManifest.pnpm!.configDependencies = {}
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
})
expect(fs.existsSync('node_modules/.pnpm-config/@pnpm.e2e/foo/package.json')).toBeFalsy()
})
test('patch from configuration dependency is applied', async () => {
const rootProjectManifest = {
pnpm: {
configDependencies: {
'@pnpm.e2e/has-patch-for-foo': `1.0.0+${getIntegrity('@pnpm.e2e/has-patch-for-foo', '1.0.0')}`,
},
patchedDependencies: {
'@pnpm.e2e/foo@100.0.0': 'node_modules/.pnpm-config/@pnpm.e2e/has-patch-for-foo/@pnpm.e2e__foo@100.0.0.patch',
},
},
}
prepare(rootProjectManifest)
await add.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
patchedDependencies: rootProjectManifest.pnpm?.patchedDependencies,
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
}, ['@pnpm.e2e/foo@100.0.0'])
expect(fs.existsSync('node_modules/@pnpm.e2e/foo/index.js')).toBeTruthy()
})
test('installation fails if the checksum of the config dependency is invalid', async () => {
const rootProjectManifest: ProjectManifest = {
pnpm: {
configDependencies: {
'@pnpm.e2e/foo': '100.0.0+sha512-00000000000000000000000000000000000000000000000000000000000000000000000000000000000000==',
},
},
}
prepare(rootProjectManifest)
await expect(install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
})).rejects.toThrow('Got unexpected checksum for')
})
test('installation fails if the config dependency does not have a checksum', async () => {
const rootProjectManifest: ProjectManifest = {
pnpm: {
configDependencies: {
'@pnpm.e2e/foo': '100.0.0',
},
},
}
prepare(rootProjectManifest)
await expect(install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
})).rejects.toThrow("doesn't have an integrity checksum")
})
test('selectively allow scripts in some dependencies by onlyBuiltDependenciesFile', async () => {
const rootProjectManifest = {
pnpm: {
configDependencies: {
'@pnpm.e2e/build-allow-list': `1.0.0+${getIntegrity('@pnpm.e2e/build-allow-list', '1.0.0')}`,
},
onlyBuiltDependenciesFile: 'node_modules/.pnpm-config/@pnpm.e2e/build-allow-list/list.json',
},
}
prepare(rootProjectManifest)
await add.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
}, ['@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0', '@pnpm.e2e/install-script-example'])
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
rimraf('node_modules')
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
frozenLockfile: true,
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
})
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
})
test('selectively allow scripts in some dependencies by onlyBuiltDependenciesFile and onlyBuiltDependencies', async () => {
const rootProjectManifest = {
pnpm: {
configDependencies: {
'@pnpm.e2e/build-allow-list': `1.0.0+${getIntegrity('@pnpm.e2e/build-allow-list', '1.0.0')}`,
},
onlyBuiltDependenciesFile: 'node_modules/.pnpm-config/@pnpm.e2e/build-allow-list/list.json',
onlyBuiltDependencies: ['@pnpm.e2e/pre-and-postinstall-scripts-example'],
},
}
prepare(rootProjectManifest)
await add.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
}, ['@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0', '@pnpm.e2e/install-script-example'])
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
rimraf('node_modules')
await install.handler({
...DEFAULT_OPTS,
configDependencies: rootProjectManifest.pnpm!.configDependencies,
dir: process.cwd(),
frozenLockfile: true,
rootProjectManifest,
rootProjectManifestDir: process.cwd(),
})
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
})

View File

@@ -756,47 +756,3 @@ test('installing in monorepo with shared lockfile should work on virtual drives'
projects['project-1'].has('is-positive')
})
test('pass readPackage with shared lockfile', async () => {
const projects = preparePackages([
{
name: 'project-1',
version: '1.0.0',
dependencies: {
'is-negative': '1.0.0',
},
},
{
name: 'project-2',
version: '1.0.0',
dependencies: {
'is-negative': '1.0.0',
},
},
])
fs.writeFileSync('.pnpmfile.cjs', `
module.exports = {
hooks: {
readPackage: (pkg) => ({
...pkg,
dependencies: {
'is-positive': '1.0.0',
},
}),
},
}
`, 'utf8')
await install.handler({
...DEFAULT_OPTS,
...await filterPackagesFromDir(process.cwd(), []),
dir: process.cwd(),
recursive: true,
workspaceDir: process.cwd(),
})
projects['project-1'].has('is-positive')
projects['project-1'].hasNot('is-negative')
projects['project-2'].has('is-positive')
projects['project-2'].hasNot('is-negative')
})

52
pnpm-lock.yaml generated
View File

@@ -1236,6 +1236,9 @@ importers:
'@pnpm/config':
specifier: workspace:*
version: link:../../config/config
'@pnpm/config.deps-installer':
specifier: workspace:*
version: link:../../config/deps-installer
'@pnpm/default-reporter':
specifier: workspace:*
version: link:../default-reporter
@@ -1248,9 +1251,15 @@ importers:
'@pnpm/package-is-installable':
specifier: workspace:*
version: link:../../config/package-is-installable
'@pnpm/pnpmfile':
specifier: workspace:*
version: link:../../hooks/pnpmfile
'@pnpm/read-project-manifest':
specifier: workspace:*
version: link:../../pkg-manifest/read-project-manifest
'@pnpm/store-connection-manager':
specifier: workspace:*
version: link:../../store/store-connection-manager
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
@@ -1580,6 +1589,49 @@ importers:
specifier: 'catalog:'
version: 0.29.12
config/deps-installer:
dependencies:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/package-store':
specifier: workspace:*
version: link:../../store/package-store
'@pnpm/pick-registry-for-package':
specifier: workspace:*
version: link:../pick-registry-for-package
'@pnpm/read-modules-dir':
specifier: workspace:*
version: link:../../fs/read-modules-dir
'@pnpm/read-package-json':
specifier: workspace:*
version: link:../../pkg-manifest/read-package-json
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
'@zkochan/rimraf':
specifier: 'catalog:'
version: 3.0.2
get-npm-tarball-url:
specifier: 'catalog:'
version: 2.1.0
devDependencies:
'@pnpm/config.deps-installer':
specifier: workspace:*
version: 'link:'
'@pnpm/prepare':
specifier: workspace:*
version: link:../../__utils__/prepare
'@pnpm/registry-mock':
specifier: 'catalog:'
version: 4.2.0(encoding@0.1.13)(typanion@3.14.0)
'@pnpm/testing.temp-store':
specifier: workspace:*
version: link:../../testing/temp-store
load-json-file:
specifier: 'catalog:'
version: 6.2.0
config/matcher:
dependencies:
escape-string-regexp:

View File

@@ -0,0 +1,71 @@
import fs from 'fs'
import { prepare } from '@pnpm/prepare'
import { getIntegrity } from '@pnpm/registry-mock'
import { sync as rimraf } from '@zkochan/rimraf'
import { sync as writeYamlFile } from 'write-yaml-file'
import { execPnpm } from './utils'
test('patch from configuration dependency is applied', async () => {
prepare()
writeYamlFile('pnpm-workspace.yaml', {
configDependencies: {
'@pnpm.e2e/has-patch-for-foo': `1.0.0+${getIntegrity('@pnpm.e2e/has-patch-for-foo', '1.0.0')}`,
},
patchedDependencies: {
'@pnpm.e2e/foo@100.0.0': 'node_modules/.pnpm-config/@pnpm.e2e/has-patch-for-foo/@pnpm.e2e__foo@100.0.0.patch',
},
})
await execPnpm(['add', '@pnpm.e2e/foo@100.0.0'])
expect(fs.existsSync('node_modules/@pnpm.e2e/foo/index.js')).toBeTruthy()
})
test('selectively allow scripts in some dependencies by onlyBuiltDependenciesFile', async () => {
prepare({})
writeYamlFile('pnpm-workspace.yaml', {
configDependencies: {
'@pnpm.e2e/build-allow-list': `1.0.0+${getIntegrity('@pnpm.e2e/build-allow-list', '1.0.0')}`,
},
onlyBuiltDependenciesFile: 'node_modules/.pnpm-config/@pnpm.e2e/build-allow-list/list.json',
})
await execPnpm(['add', '@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0', '@pnpm.e2e/install-script-example'])
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
rimraf('node_modules')
await execPnpm(['install'])
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeFalsy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
})
test('selectively allow scripts in some dependencies by onlyBuiltDependenciesFile and onlyBuiltDependencies', async () => {
prepare()
writeYamlFile('pnpm-workspace.yaml', {
configDependencies: {
'@pnpm.e2e/build-allow-list': `1.0.0+${getIntegrity('@pnpm.e2e/build-allow-list', '1.0.0')}`,
},
onlyBuiltDependenciesFile: 'node_modules/.pnpm-config/@pnpm.e2e/build-allow-list/list.json',
onlyBuiltDependencies: ['@pnpm.e2e/pre-and-postinstall-scripts-example'],
})
await execPnpm(['add', '@pnpm.e2e/pre-and-postinstall-scripts-example@1.0.0', '@pnpm.e2e/install-script-example'])
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
rimraf('node_modules')
await execPnpm(['install'])
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
})

View File

@@ -313,3 +313,24 @@ test('loading a pnpmfile from a config dependency', async () => {
expect(fs.readdirSync('node_modules/.pnpm')).toContain('@pnpm+y@1.0.0')
})
test('updateConfig hook', async () => {
prepare()
const pnpmfile = `
module.exports = {
hooks: {
updateConfig: (config) => ({
...config,
nodeLinker: 'hoisted',
}),
},
}`
fs.writeFileSync('.pnpmfile.cjs', pnpmfile, 'utf8')
await execPnpm(['add', 'is-odd@1.0.0'])
const nodeModulesFiles = fs.readdirSync('node_modules')
expect(nodeModulesFiles).toContain('kind-of')
expect(nodeModulesFiles).toContain('is-number')
})

View File

@@ -304,7 +304,7 @@ test('fails when .pnpmfile.cjs requires a non-existed module', async () => {
const proc = execPnpmSync(['add', '@pnpm.e2e/pkg-with-1-dep'])
expect(proc.stdout.toString()).toContain('Error during pnpmfile execution')
expect(proc.stderr.toString()).toContain('Error during pnpmfile execution')
expect(proc.status).toBe(1)
})
@@ -651,3 +651,42 @@ test('preResolution hook', async () => {
'@foo': 'https://foo.com/',
})
})
test('pass readPackage with shared lockfile', async () => {
const projects = preparePackages([
{
name: 'project-1',
version: '1.0.0',
dependencies: {
'is-negative': '1.0.0',
},
},
{
name: 'project-2',
version: '1.0.0',
dependencies: {
'is-negative': '1.0.0',
},
},
])
writeYamlFile('pnpm-workspace.yaml', { packages: ['*'] })
fs.writeFileSync('.pnpmfile.cjs', `
module.exports = {
hooks: {
readPackage: (pkg) => ({
...pkg,
dependencies: {
'is-positive': '1.0.0',
},
}),
},
}
`, 'utf8')
await execPnpm(['install'])
projects['project-1'].has('is-positive')
projects['project-1'].hasNot('is-negative')
projects['project-2'].has('is-positive')
projects['project-2'].hasNot('is-negative')
})

View File

@@ -15,7 +15,7 @@ export interface CreateTempStoreResult {
export function createTempStore (opts?: {
fastUnpack?: boolean
storeDir?: string
clientOptions?: ClientOptions
clientOptions?: Partial<ClientOptions>
storeOptions?: CreatePackageStoreOptions
}): CreateTempStoreResult {
const authConfig = { registry }