Skip to content

Commit

Permalink
feat: onlyBuiltDependencies (#4014)
Browse files Browse the repository at this point in the history
close #4001

Co-authored-by: Jack Works <jackworks@protonmail.com>
  • Loading branch information
zkochan and Jack-Works committed Feb 17, 2022
1 parent 5e07a4d commit 5d2b5ee
Show file tree
Hide file tree
Showing 15 changed files with 193 additions and 16 deletions.
13 changes: 13 additions & 0 deletions .changeset/dry-flowers-grab.md
@@ -0,0 +1,13 @@
---
"pnpm": minor
---

A new setting is supported in the `pnpm` section of the `package.json` file [#4001](https://github.com/pnpm/pnpm/issues/4001). `onlyBuiltDependencies` is an array of package names that are allowed to be executed during installation. If this field exists, only mentioned packages will be able to run install scripts.

```json
{
"pnpm": {
"onlyBuiltDependencies": ["fsevents"]
}
}
```
8 changes: 8 additions & 0 deletions .changeset/large-games-hug.md
@@ -0,0 +1,8 @@
---
"@pnpm/core": minor
"@pnpm/lockfile-file": minor
"@pnpm/lockfile-types": minor
"@pnpm/types": minor
---

New optional field supported: `onlyBuiltDependencies`.
5 changes: 5 additions & 0 deletions .changeset/serious-frogs-yell.md
@@ -0,0 +1,5 @@
---
"@pnpm/resolve-dependencies": major
---

Removed the `neverBuiltDependencies` option. In order to ignore scripts of some dependencies, use the new `allowBuild`. `allowBuild` is a function that accepts the package name and returns `true` if the package should be allowed to build.
4 changes: 4 additions & 0 deletions packages/core/src/install/extendInstallOptions.ts
Expand Up @@ -49,6 +49,7 @@ export interface StrictInstallOptions {
verifyStoreIntegrity: boolean
engineStrict: boolean
neverBuiltDependencies: string[]
onlyBuiltDependencies?: string[]
nodeExecPath?: string
nodeLinker?: 'isolated' | 'hoisted' | 'pnp'
nodeVersion: string
Expand Down Expand Up @@ -180,6 +181,9 @@ export default async (
}
}
}
if (opts.onlyBuiltDependencies && opts.neverBuiltDependencies) {
throw new PnpmError('CONFIG_CONFLICT_BUILT_DEPENDENCIES', 'Cannot have both neverBuiltDependencies and onlyBuiltDependencies')
}
const defaultOpts = await defaults(opts)
const extendedOpts = {
...defaultOpts,
Expand Down
52 changes: 46 additions & 6 deletions packages/core/src/install/index.ts
Expand Up @@ -223,14 +223,18 @@ export async function mutateModules (
)
}
const packageExtensionsChecksum = isEmpty(opts.packageExtensions ?? {}) ? undefined : createObjectChecksum(opts.packageExtensions!)
let needsFullResolution = !maybeOpts.ignorePackageManifest && (
!equals(ctx.wantedLockfile.overrides ?? {}, opts.overrides ?? {}) ||
!equals((ctx.wantedLockfile.neverBuiltDependencies ?? []).sort(), (opts.neverBuiltDependencies ?? []).sort()) ||
ctx.wantedLockfile.packageExtensionsChecksum !== packageExtensionsChecksum) ||
let needsFullResolution = !maybeOpts.ignorePackageManifest &&
lockfileIsUpToDate(ctx.wantedLockfile, {
overrides: opts.overrides,
neverBuiltDependencies: opts.neverBuiltDependencies,
onlyBuiltDependencies: opts.onlyBuiltDependencies,
packageExtensionsChecksum,
}) ||
opts.fixLockfile
if (needsFullResolution) {
ctx.wantedLockfile.overrides = opts.overrides
ctx.wantedLockfile.neverBuiltDependencies = opts.neverBuiltDependencies
ctx.wantedLockfile.onlyBuiltDependencies = opts.onlyBuiltDependencies
ctx.wantedLockfile.packageExtensionsChecksum = packageExtensionsChecksum
}
const frozenLockfile = opts.frozenLockfile ||
Expand Down Expand Up @@ -467,6 +471,25 @@ export async function mutateModules (
}
}

function lockfileIsUpToDate (
lockfile: Lockfile,
{
neverBuiltDependencies,
onlyBuiltDependencies,
overrides,
packageExtensionsChecksum,
}: {
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
overrides?: Record<string, string>
packageExtensionsChecksum?: string
}) {
return !equals(lockfile.overrides ?? {}, overrides ?? {}) ||
!equals((lockfile.neverBuiltDependencies ?? []).sort(), (neverBuiltDependencies ?? []).sort()) ||
!equals(onlyBuiltDependencies?.sort(), lockfile.onlyBuiltDependencies) ||
lockfile.packageExtensionsChecksum !== packageExtensionsChecksum
}

export function createObjectChecksum (obj: Object) {
const s = JSON.stringify(obj)
return crypto.createHash('md5').update(s).digest('hex')
Expand Down Expand Up @@ -608,7 +631,8 @@ type InstallFunction = (
opts: StrictInstallOptions & {
makePartialCurrentLockfile: boolean
needsFullResolution: boolean
neverBuiltDependencies: string[]
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
overrides?: Record<string, string>
updateLockfileMinorVersion: boolean
preferredVersions?: PreferredVersions
Expand Down Expand Up @@ -705,6 +729,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
} = await resolveDependencies(
projects,
{
allowBuild: createAllowBuildFunction(opts),
currentLockfile: ctx.currentLockfile,
defaultUpdateDepth: (opts.update || (opts.updateMatching != null)) ? opts.depth : -1,
dryRun: opts.lockfileOnly,
Expand All @@ -714,7 +739,6 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
hooks: opts.hooks,
linkWorkspacePackagesDepth: opts.linkWorkspacePackagesDepth ?? (opts.saveWorkspaceProtocol ? 0 : -1),
lockfileDir: opts.lockfileDir,
neverBuiltDependencies: new Set(opts.neverBuiltDependencies),
nodeVersion: opts.nodeVersion,
pnpmVersion: opts.packageManager.name === 'pnpm' ? opts.packageManager.version : '',
preferWorkspacePackages: opts.preferWorkspacePackages,
Expand Down Expand Up @@ -985,6 +1009,22 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
}
}

function createAllowBuildFunction (
opts: {
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
}
): undefined | ((pkgName: string) => boolean) {
if (opts.neverBuiltDependencies != null && opts.neverBuiltDependencies.length > 0) {
const neverBuiltDependencies = new Set(opts.neverBuiltDependencies)
return (pkgName) => !neverBuiltDependencies.has(pkgName)
} else if (opts.onlyBuiltDependencies != null) {
const onlyBuiltDependencies = new Set(opts.onlyBuiltDependencies)
return (pkgName) => onlyBuiltDependencies.has(pkgName)
}
return undefined
}

const installInContext: InstallFunction = async (projects, ctx, opts) => {
try {
if (opts.nodeLinker === 'hoisted' && !opts.lockfileOnly) {
Expand Down
89 changes: 88 additions & 1 deletion packages/core/test/install/lifecycleScripts.ts
Expand Up @@ -443,7 +443,7 @@ test('scripts have access to unlisted bins when hoisting is used', async () => {
expect(project.requireModule('pkg-that-calls-unlisted-dep-in-hooks/output.json')).toStrictEqual(['Hello world!'])
})

test('selectively ignore scripts in some dependencies', async () => {
test('selectively ignore scripts in some dependencies by neverBuiltDependencies', async () => {
const project = prepareEmpty()
const neverBuiltDependencies = ['pre-and-postinstall-scripts-example']
const manifest = await addDependenciesToPackage({},
Expand All @@ -469,6 +469,44 @@ test('selectively ignore scripts in some dependencies', async () => {
expect(await exists('node_modules/install-script-example/generated-by-install.js')).toBeTruthy()
})

test('throw an exception when both neverBuiltDependencies and onlyBuiltDependencies are used', async () => {
prepareEmpty()

await expect(
addDependenciesToPackage(
{},
['pre-and-postinstall-scripts-example'],
await testDefaults({ onlyBuiltDependencies: ['foo'], neverBuiltDependencies: ['bar'] })
)
).rejects.toThrow(/Cannot have both/)
})

test('selectively allow scripts in some dependencies by onlyBuiltDependencies', async () => {
const project = prepareEmpty()
const onlyBuiltDependencies = ['install-script-example']
const manifest = await addDependenciesToPackage({},
['pre-and-postinstall-scripts-example', 'install-script-example'],
await testDefaults({ fastUnpack: false, onlyBuiltDependencies })
)

expect(await exists('node_modules/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
expect(await exists('node_modules/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeFalsy()
expect(await exists('node_modules/install-script-example/generated-by-install.js')).toBeTruthy()

const lockfile = await project.readLockfile()
expect(lockfile.onlyBuiltDependencies).toStrictEqual(onlyBuiltDependencies)
expect(lockfile.packages['/pre-and-postinstall-scripts-example/1.0.0'].requiresBuild).toBe(undefined)
expect(lockfile.packages['/install-script-example/1.0.0'].requiresBuild).toBe(true)

await rimraf('node_modules')

await install(manifest, await testDefaults({ fastUnpack: false, frozenLockfile: true, onlyBuiltDependencies }))

expect(await exists('node_modules/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeFalsy()
expect(await exists('node_modules/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeFalsy()
expect(await exists('node_modules/install-script-example/generated-by-install.js')).toBeTruthy()
})

test('lockfile is updated if neverBuiltDependencies is changed', async () => {
const project = prepareEmpty()
const manifest = await addDependenciesToPackage({},
Expand Down Expand Up @@ -501,6 +539,55 @@ test('lockfile is updated if neverBuiltDependencies is changed', async () => {
}
})

test('lockfile is updated if onlyBuiltDependencies is changed', async () => {
const project = prepareEmpty()
const manifest = await addDependenciesToPackage({},
['pre-and-postinstall-scripts-example', 'install-script-example'],
await testDefaults({ fastUnpack: false })
)

{
const lockfile = await project.readLockfile()
expect(lockfile.onlyBuiltDependencies).toBeFalsy()
expect(lockfile.packages['/pre-and-postinstall-scripts-example/1.0.0'].requiresBuild).toBeTruthy()
expect(lockfile.packages['/install-script-example/1.0.0'].requiresBuild).toBeTruthy()
}

const onlyBuiltDependencies: string[] = []
await mutateModules([
{
buildIndex: 0,
manifest,
mutation: 'install',
rootDir: process.cwd(),
},
], await testDefaults({ onlyBuiltDependencies }))

{
const lockfile = await project.readLockfile()
expect(lockfile.onlyBuiltDependencies).toStrictEqual(onlyBuiltDependencies)
expect(lockfile.packages['/pre-and-postinstall-scripts-example/1.0.0'].requiresBuild).toBe(undefined)
expect(lockfile.packages['/install-script-example/1.0.0'].requiresBuild).toBe(undefined)
}

onlyBuiltDependencies.push('pre-and-postinstall-scripts-example')
await mutateModules([
{
buildIndex: 0,
manifest,
mutation: 'install',
rootDir: process.cwd(),
},
], await testDefaults({ onlyBuiltDependencies }))

{
const lockfile = await project.readLockfile()
expect(lockfile.onlyBuiltDependencies).toStrictEqual(onlyBuiltDependencies)
expect(lockfile.packages['/pre-and-postinstall-scripts-example/1.0.0'].requiresBuild).toBe(true)
expect(lockfile.packages['/install-script-example/1.0.0'].requiresBuild).toBe(undefined)
}
})

test('lifecycle scripts have access to package\'s own binary by binary name', async () => {
const project = prepareEmpty()
await addDependenciesToPackage({},
Expand Down
2 changes: 2 additions & 0 deletions packages/lockfile-file/src/sortLockfileKeys.ts
Expand Up @@ -31,7 +31,9 @@ const ORDERED_KEYS = {

const ROOT_KEYS_ORDER = {
lockfileVersion: 1,
// only and never are conflict options.
neverBuiltDependencies: 2,
onlyBuiltDependencies: 2,
overrides: 3,
packageExtensionsChecksum: 4,
specifiers: 10,
Expand Down
3 changes: 3 additions & 0 deletions packages/lockfile-file/src/write.ts
Expand Up @@ -126,6 +126,9 @@ export function normalizeLockfile (lockfile: Lockfile, forceSharedFormat: boolea
lockfileToSave.neverBuiltDependencies = lockfileToSave.neverBuiltDependencies.sort()
}
}
if (lockfileToSave.onlyBuiltDependencies != null) {
lockfileToSave.onlyBuiltDependencies = lockfileToSave.onlyBuiltDependencies.sort()
}
if (!lockfileToSave.packageExtensionsChecksum) {
delete lockfileToSave.packageExtensionsChecksum
}
Expand Down
3 changes: 3 additions & 0 deletions packages/lockfile-file/test/normalizeLockfile.test.ts
Expand Up @@ -4,6 +4,8 @@ import { normalizeLockfile } from '@pnpm/lockfile-file/lib/write'
test('empty overrides and neverBuiltDependencies are removed during lockfile normalization', () => {
expect(normalizeLockfile({
lockfileVersion: LOCKFILE_VERSION,
// but this should be preserved.
onlyBuiltDependencies: [],
overrides: {},
neverBuiltDependencies: [],
packages: {},
Expand All @@ -19,6 +21,7 @@ test('empty overrides and neverBuiltDependencies are removed during lockfile nor
},
}, false)).toStrictEqual({
lockfileVersion: LOCKFILE_VERSION,
onlyBuiltDependencies: [],
importers: {
foo: {
dependencies: {
Expand Down
1 change: 1 addition & 0 deletions packages/lockfile-types/src/index.ts
Expand Up @@ -5,6 +5,7 @@ export interface Lockfile {
lockfileVersion: number
packages?: PackageSnapshots
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
overrides?: Record<string, string>
packageExtensionsChecksum?: string
}
Expand Down
Expand Up @@ -7,6 +7,7 @@ import {
export default function getOptionsFromRootManifest (manifest: ProjectManifest): {
overrides?: Record<string, string>
neverBuiltDependencies?: string[]
onlyBuiltDependencies?: string[]
packageExtensions?: Record<string, PackageExtension>
peerDependencyRules?: PeerDependencyRules
} {
Expand All @@ -15,11 +16,13 @@ export default function getOptionsFromRootManifest (manifest: ProjectManifest):
// so we cannot call it resolutions
const overrides = manifest.pnpm?.overrides ?? manifest.resolutions
const neverBuiltDependencies = manifest.pnpm?.neverBuiltDependencies ?? []
const onlyBuiltDependencies = manifest.pnpm?.onlyBuiltDependencies
const packageExtensions = manifest.pnpm?.packageExtensions
const peerDependencyRules = manifest.pnpm?.peerDependencyRules
return {
overrides,
neverBuiltDependencies,
onlyBuiltDependencies,
packageExtensions,
peerDependencyRules,
}
Expand Down
7 changes: 6 additions & 1 deletion packages/resolve-dependencies/src/index.ts
Expand Up @@ -195,7 +195,12 @@ export default async function (
if (opts.forceFullResolution && opts.wantedLockfile != null) {
for (const [depPath, pkg] of Object.entries(dependenciesGraph)) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (opts.neverBuiltDependencies?.has(pkg.name) || opts.wantedLockfile.packages?.[depPath] == null || pkg.requiresBuild) continue
if (
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(opts.allowBuild != null && !opts.allowBuild(pkg.name)) ||
(opts.wantedLockfile.packages?.[depPath] == null) ||
pkg.requiresBuild
) continue
pendingRequiresBuilds.push(depPath)
}
}
Expand Down
14 changes: 8 additions & 6 deletions packages/resolve-dependencies/src/resolveDependencies.ts
Expand Up @@ -116,6 +116,7 @@ export interface ChildrenByParentDepPath {
}

export interface ResolutionContext {
allowBuild?: (pkgName: string) => boolean
updatedSet: Set<string>
defaultTag: string
dryRun: boolean
Expand All @@ -128,7 +129,6 @@ export interface ResolutionContext {
currentLockfile: Lockfile
linkWorkspacePackagesDepth: number
lockfileDir: string
neverBuiltDependencies: Set<string>
storeController: StoreController
// the IDs of packages that are not installable
skipped: Set<string>
Expand Down Expand Up @@ -784,11 +784,11 @@ async function resolveDependency (
})

ctx.resolvedPackagesByDepPath[depPath] = getResolvedPackage({
allowBuild: ctx.allowBuild,
dependencyLockfile: currentPkg.dependencyLockfile,
depPath,
force: ctx.force,
hasBin,
neverBuiltDependencies: ctx.neverBuiltDependencies,
pkg,
pkgResponse,
prepare,
Expand Down Expand Up @@ -843,11 +843,11 @@ function pkgIsLeaf (pkg: PackageManifest) {

function getResolvedPackage (
options: {
allowBuild?: (pkgName: string) => boolean
dependencyLockfile?: PackageSnapshot
depPath: string
force: boolean
hasBin: boolean
neverBuiltDependencies: Set<string>
pkg: PackageManifest
pkgResponse: PackageResponse
prepare: boolean
Expand All @@ -856,6 +856,10 @@ function getResolvedPackage (
) {
const peerDependencies = peerDependenciesWithoutOwn(options.pkg)

const requiresBuild = (options.allowBuild == null || options.allowBuild(options.pkg.name))
? ((options.dependencyLockfile != null) ? Boolean(options.dependencyLockfile.requiresBuild) : undefined)
: false

return {
additionalInfo: {
bundledDependencies: options.pkg.bundledDependencies,
Expand All @@ -881,9 +885,7 @@ function getResolvedPackage (
peerDependenciesMeta: options.pkg.peerDependenciesMeta,
prepare: options.prepare,
prod: !options.wantedDependency.dev && !options.wantedDependency.optional,
requiresBuild: options.neverBuiltDependencies.has(options.pkg.name)
? false
: ((options.dependencyLockfile != null) ? Boolean(options.dependencyLockfile.requiresBuild) : undefined),
requiresBuild,
resolution: options.pkgResponse.body.resolution,
version: options.pkg.version,
}
Expand Down

0 comments on commit 5d2b5ee

Please sign in to comment.