mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-01 20:49:45 -04:00
Fixes #11818 ## Summary `devEngines.runtime` / `engines.runtime` entries with `onFail: error` or `warn` silently did nothing — only `onFail: download` had any effect. This PR wires up validation for all three supported runtimes (node, deno, bun). - Add `getSystemDenoVersion` / `getSystemBunVersion` and a generic `getSystemRuntimeVersion(name)` dispatcher in the runtime-version helper package. - Walk each runtime entry in the manifest during pnpm startup, compare to the live system runtime, and throw `ERR_PNPM_BAD_RUNTIME_VERSION` (or warn) on a mismatch. Invalid ranges (e.g. `"invalid range"`) are reported instead of crashing `semver.minVersion`. Missing runtimes ("no Node.js on the system") get the same error path. - The shell-out for deno/bun only runs when the manifest configures them AND `onFail` is `error`/`warn`. `download`/`ignore` short-circuit, and projects with no runtime pin pay nothing. Memoized per runtime. - `pnpm --version`, `pnpm --help`, and `pnpm <cmd> --global` are exempt from the check. - Rename `@pnpm/engine.runtime.system-node-version` → `@pnpm/engine.runtime.system-version` to match its broader scope; hoist `RuntimeName` / `RUNTIME_NAMES` / `isRuntimeAlias` to `@pnpm/types` so callers don't need to depend on `pkg-manifest.utils` just for the alias check. ## Tests - `pnpm --filter pnpm run compile` - `pnpm --filter pnpm exec jest packageManagerCheck.test` — 42 passing. New coverage: node/deno/bun version mismatch, invalid range, missing range, multi-entry runtime arrays, `engines.runtime` path (not just `devEngines.runtime`), and the `pnpm --version` exemption. - `pnpm --filter @pnpm/engine.runtime.system-version test` — 10 passing, 100% statement coverage; unit tests for each helper and the dispatcher. - Manual end-to-end smoke tests against the rebuilt bundle for deno and bun version mismatch. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added runtime version validation for Node.js, Deno, and Bun. The system now enforces `devEngines.runtime` and `engines.runtime` declarations with configurable failure behavior (`error`, `warn`, or `ignore`). * Enhanced error messages for runtime version mismatches with helpful suggestions for overrides. * **Improvements** * Improved system runtime detection and version checking across multiple runtime environments. --------- Co-authored-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com> Co-authored-by: Zoltan Kochan <z@kochan.io>
526 lines
17 KiB
TypeScript
526 lines
17 KiB
TypeScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
import { afterEach, expect, jest, test } from '@jest/globals'
|
|
import { prepareEmpty } from '@pnpm/prepare'
|
|
|
|
import { DLX_DEFAULT_OPTS as DEFAULT_OPTS } from './utils/index.js'
|
|
|
|
const {
|
|
getSystemNodeVersion: originalGetSystemNodeVersion,
|
|
engineName: originalEngineName,
|
|
} = await import('@pnpm/engine.runtime.system-version')
|
|
// Re-export every public symbol the package surfaces so downstream
|
|
// dynamic imports (e.g. `@pnpm/deps.graph-hasher`'s use of
|
|
// `engineName` for the GVS hash) keep working under the mock. Only
|
|
// `getSystemNodeVersion` is wrapped with `jest.fn` for spy-ability;
|
|
// `engineName` delegates straight back to the original.
|
|
jest.unstable_mockModule('@pnpm/engine.runtime.system-version', () => ({
|
|
getSystemNodeVersion: jest.fn(originalGetSystemNodeVersion),
|
|
engineName: originalEngineName,
|
|
}))
|
|
const installingCommands = await import('@pnpm/installing.commands')
|
|
const { add: originalAdd } = installingCommands
|
|
jest.unstable_mockModule('@pnpm/installing.commands', () => ({
|
|
...installingCommands,
|
|
add: {
|
|
...originalAdd,
|
|
handler: jest.fn(originalAdd.handler),
|
|
},
|
|
}))
|
|
|
|
const systemNodeVersion = await import('@pnpm/engine.runtime.system-version')
|
|
const { add } = await import('@pnpm/installing.commands')
|
|
const { dlx } = await import('@pnpm/exec.commands')
|
|
const { approveBuilds } = await import('@pnpm/building.commands')
|
|
|
|
const testOnWindowsOnly = process.platform === 'win32' ? test : test.skip
|
|
|
|
function sanitizeDlxCacheComponent (cacheName: string): string {
|
|
if (cacheName === 'pkg') return cacheName
|
|
const segments = cacheName.split('-')
|
|
if (segments.length !== 2) {
|
|
throw new Error(`Unexpected name: ${cacheName}`)
|
|
}
|
|
const [date, pid] = segments
|
|
if (!/[0-9a-f]+/.test(date) && !/[0-9a-f]+/.test(pid)) {
|
|
throw new Error(`Name ${cacheName} doesn't end with 2 hex numbers`)
|
|
}
|
|
return '***********-*****'
|
|
}
|
|
|
|
const createCacheKey = (...packages: string[]): string => dlx.createCacheKey({
|
|
packages,
|
|
registries: DEFAULT_OPTS.registries,
|
|
supportedArchitectures: DEFAULT_OPTS.supportedArchitectures,
|
|
})
|
|
|
|
function verifyDlxCache (cacheName: string): void {
|
|
expect(
|
|
fs.readdirSync(path.resolve('cache', 'dlx', cacheName))
|
|
.map(sanitizeDlxCacheComponent)
|
|
.sort()
|
|
).toStrictEqual([
|
|
'pkg',
|
|
'***********-*****',
|
|
].sort())
|
|
verifyDlxCacheLink(cacheName)
|
|
}
|
|
|
|
function verifyDlxCacheLink (cacheName: string): void {
|
|
expect(
|
|
fs.readdirSync(path.resolve('cache', 'dlx', cacheName, 'pkg'))
|
|
.sort()
|
|
).toStrictEqual([
|
|
'node_modules',
|
|
'package.json',
|
|
'pnpm-lock.yaml',
|
|
].sort())
|
|
expect(
|
|
path.dirname(fs.realpathSync(path.resolve('cache', 'dlx', cacheName, 'pkg')))
|
|
).toBe(path.resolve('cache', 'dlx', cacheName))
|
|
}
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks()
|
|
})
|
|
|
|
test('dlx', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
}, ['shx@0.3.4', 'touch', 'foo'])
|
|
|
|
expect(fs.existsSync('foo')).toBeTruthy()
|
|
})
|
|
|
|
test('dlx install from git', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: process.cwd(),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
allowBuild: ['shx'],
|
|
}, ['shelljs/shx#0dcbb9d1022037268959f8b706e0f06a6fd43fde', 'touch', 'foo'])
|
|
|
|
expect(fs.existsSync('foo')).toBeTruthy()
|
|
})
|
|
|
|
test('dlx should work when the package name differs from the bin name', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
}, ['@pnpm.e2e/touch-file-one-bin'])
|
|
|
|
expect(fs.existsSync('touch.txt')).toBeTruthy()
|
|
})
|
|
|
|
test('dlx should fail when the installed package has many commands and none equals the package name', async () => {
|
|
prepareEmpty()
|
|
|
|
await expect(
|
|
dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
}, ['@pnpm.e2e/touch-file-many-bins'])
|
|
).rejects.toThrow('Could not determine executable to run. @pnpm.e2e/touch-file-many-bins has multiple binaries: t, tt')
|
|
})
|
|
|
|
test('dlx should not fail when the installed package has many commands and one equals the package name', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
}, ['@pnpm.e2e/touch-file-good-bin-name'])
|
|
|
|
expect(fs.existsSync('touch.txt')).toBeTruthy()
|
|
})
|
|
|
|
test('dlx --package <pkg1> [--package <pkg2>]', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
package: [
|
|
'@pnpm.e2e/for-testing-pnpm-dlx',
|
|
'is-positive',
|
|
],
|
|
}, ['foo'])
|
|
|
|
expect(fs.existsSync('foo')).toBeTruthy()
|
|
})
|
|
|
|
test('dlx should fail when the package has no bins', async () => {
|
|
prepareEmpty()
|
|
|
|
await expect(
|
|
dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
}, ['is-positive'])
|
|
).rejects.toThrow(/No binaries found in is-positive/)
|
|
})
|
|
|
|
test('dlx should work in shell mode', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
package: [
|
|
'is-positive',
|
|
],
|
|
shellMode: true,
|
|
}, ['echo "some text" > foo'])
|
|
|
|
expect(fs.existsSync('foo')).toBeTruthy()
|
|
})
|
|
|
|
test('dlx should work when symlink=false', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
symlink: false,
|
|
}, ['@pnpm.e2e/touch-file-good-bin-name'])
|
|
|
|
expect(fs.existsSync('touch.txt')).toBeTruthy()
|
|
})
|
|
|
|
test('dlx should return a non-zero exit code when the underlying script fails', async () => {
|
|
prepareEmpty()
|
|
|
|
const { exitCode } = await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
package: [
|
|
'touch@3.1.0',
|
|
],
|
|
}, ['nodetouch', '--bad-option'])
|
|
|
|
expect(exitCode).toBe(1)
|
|
})
|
|
|
|
testOnWindowsOnly('dlx should work when running in the root of a Windows Drive', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: 'C:\\',
|
|
storeDir: path.resolve('store'),
|
|
}, ['cowsay', 'hello'])
|
|
})
|
|
|
|
test('dlx with cache', async () => {
|
|
prepareEmpty()
|
|
|
|
const spy = jest.mocked(add.handler)
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
}, ['shx@0.3.4', 'touch', 'foo'])
|
|
|
|
expect(fs.existsSync('foo')).toBe(true)
|
|
verifyDlxCache(createCacheKey('shx@0.3.4'))
|
|
expect(spy).toHaveBeenCalled()
|
|
|
|
spy.mockClear()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
}, ['shx@0.3.4', 'touch', 'bar'])
|
|
|
|
expect(fs.existsSync('bar')).toBe(true)
|
|
verifyDlxCache(createCacheKey('shx@0.3.4'))
|
|
expect(spy).not.toHaveBeenCalled()
|
|
|
|
spy.mockClear()
|
|
|
|
// Specify a node version that shx@0.3.4 does not support. Currently supported versions are >= 6.
|
|
jest.mocked(systemNodeVersion.getSystemNodeVersion).mockReturnValue('v4.0.0')
|
|
|
|
await expect(dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
engineStrict: true,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
}, ['shx@0.3.4', 'touch', 'foo'])).rejects.toThrow('Unsupported engine for')
|
|
|
|
jest.mocked(systemNodeVersion.getSystemNodeVersion).mockImplementation(originalGetSystemNodeVersion)
|
|
})
|
|
|
|
test('dlx does not reuse expired cache', async () => {
|
|
prepareEmpty()
|
|
|
|
const now = new Date()
|
|
|
|
// first execution to initialize the cache
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
}, ['shx@0.3.4', 'echo', 'hello world'])
|
|
verifyDlxCache(createCacheKey('shx@0.3.4'))
|
|
|
|
// change the date attributes of the cache to 30 minutes older than now
|
|
const newDate = new Date(now.getTime() - 30 * 60_000)
|
|
fs.lutimesSync(path.resolve('cache', 'dlx', createCacheKey('shx@0.3.4'), 'pkg'), newDate, newDate)
|
|
|
|
const spy = jest.mocked(add.handler)
|
|
|
|
// main dlx execution
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: 10, // 10 minutes should make 30 minutes old cache expired
|
|
}, ['shx@0.3.4', 'touch', 'BAR'])
|
|
|
|
expect(fs.existsSync('BAR')).toBe(true)
|
|
expect(spy).toHaveBeenCalledWith(expect.anything(), ['shx@0.3.4'])
|
|
|
|
spy.mockClear()
|
|
|
|
expect(
|
|
fs.readdirSync(path.resolve('cache', 'dlx', createCacheKey('shx@0.3.4')))
|
|
.map(sanitizeDlxCacheComponent)
|
|
.sort()
|
|
).toStrictEqual([
|
|
'pkg',
|
|
'***********-*****',
|
|
'***********-*****',
|
|
].sort())
|
|
verifyDlxCacheLink(createCacheKey('shx@0.3.4'))
|
|
})
|
|
|
|
test('dlx still saves cache even if execution fails', async () => {
|
|
prepareEmpty()
|
|
|
|
fs.writeFileSync(path.resolve('not-a-dir'), 'to make `shx mkdir` fails')
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
}, ['shx@0.3.4', 'mkdir', path.resolve('not-a-dir')])
|
|
|
|
expect(fs.readFileSync(path.resolve('not-a-dir'), 'utf-8')).toEqual(expect.anything())
|
|
verifyDlxCache(createCacheKey('shx@0.3.4'))
|
|
})
|
|
|
|
test('dlx builds the package that is executed', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
enableGlobalVirtualStore: false,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
}, ['@pnpm.e2e/has-bin-and-needs-build'])
|
|
|
|
// The command file of the above package is created by a postinstall script
|
|
// so if it doesn't fail it means that it was built.
|
|
|
|
const dlxCacheDir = path.resolve('cache', 'dlx', createCacheKey('@pnpm.e2e/has-bin-and-needs-build@1.0.0'), 'pkg')
|
|
const builtPkg1Path = path.join(dlxCacheDir, 'node_modules/.pnpm/@pnpm.e2e+pre-and-postinstall-scripts-example@1.0.0/node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example')
|
|
expect(fs.existsSync(path.join(builtPkg1Path, 'package.json'))).toBeTruthy()
|
|
expect(fs.existsSync(path.join(builtPkg1Path, 'generated-by-preinstall.js'))).toBeFalsy()
|
|
expect(fs.existsSync(path.join(builtPkg1Path, 'generated-by-postinstall.js'))).toBeFalsy()
|
|
|
|
const builtPkg2Path = path.join(dlxCacheDir, 'node_modules/.pnpm/@pnpm.e2e+install-script-example@1.0.0/node_modules/@pnpm.e2e/install-script-example')
|
|
expect(fs.existsSync(path.join(builtPkg2Path, 'package.json'))).toBeTruthy()
|
|
expect(fs.existsSync(path.join(builtPkg2Path, 'generated-by-install.js'))).toBeFalsy()
|
|
})
|
|
|
|
test('dlx builds the packages passed via --allow-build', async () => {
|
|
prepareEmpty()
|
|
|
|
const allowBuild = ['@pnpm.e2e/install-script-example']
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
enableGlobalVirtualStore: false,
|
|
allowBuild,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
}, ['@pnpm.e2e/has-bin-and-needs-build'])
|
|
|
|
const dlxCacheDir = path.resolve('cache', 'dlx', dlx.createCacheKey({
|
|
packages: ['@pnpm.e2e/has-bin-and-needs-build@1.0.0'],
|
|
allowBuild,
|
|
registries: DEFAULT_OPTS.registries,
|
|
supportedArchitectures: DEFAULT_OPTS.supportedArchitectures,
|
|
}), 'pkg')
|
|
const builtPkg1Path = path.join(dlxCacheDir, 'node_modules/.pnpm/@pnpm.e2e+pre-and-postinstall-scripts-example@1.0.0/node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example')
|
|
expect(fs.existsSync(path.join(builtPkg1Path, 'package.json'))).toBeTruthy()
|
|
expect(fs.existsSync(path.join(builtPkg1Path, 'generated-by-preinstall.js'))).toBeFalsy()
|
|
expect(fs.existsSync(path.join(builtPkg1Path, 'generated-by-postinstall.js'))).toBeFalsy()
|
|
|
|
const builtPkg2Path = path.join(dlxCacheDir, 'node_modules/.pnpm/@pnpm.e2e+install-script-example@1.0.0/node_modules/@pnpm.e2e/install-script-example')
|
|
expect(fs.existsSync(path.join(builtPkg2Path, 'package.json'))).toBeTruthy()
|
|
expect(fs.existsSync(path.join(builtPkg2Path, 'generated-by-install.js'))).toBeTruthy()
|
|
})
|
|
|
|
// Regression test for https://github.com/pnpm/pnpm/issues/11444.
|
|
//
|
|
// dlx mirrors the global install flow: it overrides `strictDepBuilds`
|
|
// internally so the install never throws ERR_PNPM_IGNORED_BUILDS, then
|
|
// runs the same interactive `approve-builds` prompt that `pnpm add -g`
|
|
// uses when transitive deps have skipped build scripts. The user can
|
|
// opt in to the builds without retrying with `--allow-build=<pkg>`.
|
|
//
|
|
// Without a TTY (and without the test escape hatch below), the prompt is
|
|
// skipped and dlx proceeds with build scripts skipped — same behavior
|
|
// as `pnpm add -g` in CI.
|
|
test('dlx does not error on ignored builds in non-interactive mode', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
enableGlobalVirtualStore: false,
|
|
strictDepBuilds: true,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
}, ['@pnpm.e2e/has-bin-and-needs-build'])
|
|
|
|
// Cache is populated even though build scripts were skipped — the
|
|
// package is installed so the bin can run if it does not depend on
|
|
// the skipped script.
|
|
const dlxCacheDir = path.resolve('cache', 'dlx', createCacheKey('@pnpm.e2e/has-bin-and-needs-build@1.0.0'), 'pkg')
|
|
expect(fs.existsSync(path.join(dlxCacheDir, 'package.json'))).toBe(true)
|
|
})
|
|
|
|
// Regression test for https://github.com/pnpm/pnpm/issues/11444.
|
|
//
|
|
// `PNPM_AUTO_APPROVE_BUILDS_FOR_TESTS=1` lets the test drive the
|
|
// approve-builds flow non-interactively: dlx skips the TTY check and
|
|
// forwards `all: true` to approve-builds, which approves every pending
|
|
// build without prompting and re-runs install. The build artifacts must
|
|
// end up in the dlx cache.
|
|
test('dlx prompts to approve ignored builds when invoked with a commands map', async () => {
|
|
prepareEmpty()
|
|
|
|
const prevAutoApprove = process.env.PNPM_AUTO_APPROVE_BUILDS_FOR_TESTS
|
|
process.env.PNPM_AUTO_APPROVE_BUILDS_FOR_TESTS = '1'
|
|
try {
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
enableGlobalVirtualStore: false,
|
|
strictDepBuilds: true,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
}, ['@pnpm.e2e/has-bin-and-needs-build'], { 'approve-builds': approveBuilds.handler })
|
|
} finally {
|
|
if (prevAutoApprove === undefined) {
|
|
delete process.env.PNPM_AUTO_APPROVE_BUILDS_FOR_TESTS
|
|
} else {
|
|
process.env.PNPM_AUTO_APPROVE_BUILDS_FOR_TESTS = prevAutoApprove
|
|
}
|
|
}
|
|
|
|
const dlxCacheDir = path.resolve('cache', 'dlx', createCacheKey('@pnpm.e2e/has-bin-and-needs-build@1.0.0'), 'pkg')
|
|
const builtPkg2Path = path.join(dlxCacheDir, 'node_modules/.pnpm/@pnpm.e2e+install-script-example@1.0.0/node_modules/@pnpm.e2e/install-script-example')
|
|
expect(fs.existsSync(path.join(builtPkg2Path, 'generated-by-install.js'))).toBe(true)
|
|
})
|
|
|
|
test('dlx should fail when the requested package does not meet the minimum age requirement', async () => {
|
|
prepareEmpty()
|
|
|
|
await expect(
|
|
dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
minimumReleaseAge: 60 * 24 * 10000,
|
|
minimumReleaseAgeStrict: true,
|
|
registries: {
|
|
// We must use the public registry instead of verdaccio here
|
|
// because verdaccio has the "times" field in the abbreviated metadata too.
|
|
default: 'https://registry.npmjs.org/',
|
|
},
|
|
}, ['shx@0.3.4'])
|
|
).rejects.toThrow(/shx@0\.3\.4 was published.+minimumReleaseAge cutoff/)
|
|
})
|
|
|
|
test('dlx should respect minimumReleaseAgeExclude', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
minimumReleaseAge: 60 * 24 * 10000,
|
|
minimumReleaseAgeExclude: ['*'],
|
|
registries: {
|
|
// We must use the public registry instead of verdaccio here
|
|
// because verdaccio has the "times" field in the abbreviated metadata too.
|
|
default: 'https://registry.npmjs.org/',
|
|
},
|
|
}, ['shx@0.3.4', 'touch', 'foo'])
|
|
|
|
expect(fs.existsSync('foo')).toBeTruthy()
|
|
})
|
|
|
|
test('dlx with catalog', async () => {
|
|
prepareEmpty()
|
|
|
|
await dlx.handler({
|
|
...DEFAULT_OPTS,
|
|
dir: path.resolve('project'),
|
|
storeDir: path.resolve('store'),
|
|
cacheDir: path.resolve('cache'),
|
|
dlxCacheMaxAge: Infinity,
|
|
catalogs: {
|
|
default: {
|
|
shx: '^0.3.4',
|
|
},
|
|
},
|
|
}, ['shx@catalog:'])
|
|
|
|
verifyDlxCache(createCacheKey('shx@0.3.4'))
|
|
})
|