mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat(exec): check dependencies before running scripts (#8645)
* refactor: break a long line into multiple lines * feat: cache that tracks workspace structures * feat: handle hash collisions * docs(changeset): packages-list-cache * feat(packages-list-cache): store mtime * fix(packages-list-cache): JSON5 and YAML manifests * feat(packages-list-cache): add catalogs * style: sort fields alphabetically * fix: actually fix it * lint: fix * lint: fix * test(packages-list-cache): test * feat(exec): check deps before run scripts Resolves https://github.com/pnpm/pnpm/issues/8585 * style: fix eslint * feat: use a single lastValidatedTimestamp * refactor: rearrange * perf: don't do pointless comparisons * perf: optimize non-workspace * perf: optimize sharedWorkspaceLockfile=false * perf: remove unnecessary fs reads * refactor: statManifestFile * perf: skip comparing manifest to lockfile by stats * feat: add wantedLockfileDir to error message * refactor: shorten a function name * refactor: rename a function * docs: improve wordings * feat: export `linkedPackagesAreUpToDate` * feat: make sure lockfile specs satisfy manifest (wip) * docs: todo * fix: projectId * feat: skip install-related scripts * fix: type errors * refactor: use tagged union * refactor: remove unnecessary type expression * docs: todo * feat: add linkedPackagesAreUpToDate (wip) * refactor: rearrange fields * refactor: remove a temporary variable * feat: export `getWorkspacePackagesByDirectory` * feat: make workspacePackages optional * feat: complete `linkedPackagesAreUpToDate` * docs: remove unapplicable todo * docs: explain why check is skipped * feat: load allProjects and try again * refactor: remove unused dependencies * refactor: remove commented-out code * refactor: replace `else if` with `return` * feat: use-case without workspace manifest * perf: skip unnecessary work * feat: add a guard * fix: eslint * refactor: move code to new package * refactor: sort dependencies * test: outline * refactor: extract assertLockfilesEqual for testing * test: skip failing tests for now * fix: eslint * test: assertLockfilesEqual * refactor: extract statManifestFile for testing * test: todo * test: statManifestFile * test: shouldRunCheck * refactor: rename a test file * test: add * test: todo * docs: remove a commented-out code * test: create groups * test: todo * test: add * test: platform agnostic * test: remove unnecessary scripts * test: use `assert.strictEqual` instead * test: export bin locations * test: nested `pnpm run` * test: todo * test: add `cwd` option to `execPnpmSync` * test: add * fix: recursive * test: add * test: fix package names * fix: catalogs comparison * test: add * refactor: just use ramda filter * test: add * test: mutations * fix: package.json * fix: jest * feat(packages-list): debug logs * feat: add debug messages * fix: eslint * test: check debug messages in other case * docs: add next step * test: mtime updates without modification * docs: correct test description * test: mtime changes * test: check should be skipped * docs: remove fulfilled todos * fix: remove `.only` * docs: todo * docs: correct test names * test: workspace structure changes * test: packages list cache * test: add * refactor: divide a test file into 2 * docs: consistent wordings * refactor: clearer error messages * fix: ignore check in recursive nested scripts * test: no dependencies * test: print error messages on failures * test: improve stdout/stderr in error messages * docs: consistent wordings * docs: clarify what did what * test: nested script * docs: consistent test descriptions * docs(changeset): correction * fix: save catalogs to packages list * test: catalogs * test: fix * test: fix windows * refactor: remove unused option field * refactor: prefer `!= null` * feat: use `node_modules` instead * refactor: rename a package * refactor: apply suggestion * refactor: remove workspaceDir * refactor: move `shouldRunCheck` to `exec` * feat: rename config key * refactor: rename a test dir * refactor: correct grammar * refactor: make loadPackagesList sync * test: multiple lockfiles * feat: prevent deletion of `node_modules` * feat: skip checking on filtered install * fix: accidentally dropping catalogs * refactor: remove unnecessary `Promise.all` * refactor: use `virtualStoreDir` from config * refactor: split `opts` into `ctx` and `opts` * test: fix * style: fix eslint * test: fix windows * feat(exec): add `verifyDepsBeforeRun` to `exec` * refactor: sync stat * feat: stop ignoring filtered install * test: filtered install * refactor: rearrange imports * feat: rename "packages list" to "workspace state" * test: fix * fix: workspace state on failed install
This commit is contained in:
5
.changeset/angry-shrimps-lay.md
Normal file
5
.changeset/angry-shrimps-lay.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/constants": minor
|
||||
---
|
||||
|
||||
Add `MANIFEST_BASE_NAMES`
|
||||
6
.changeset/chilled-adults-sell.md
Normal file
6
.changeset/chilled-adults-sell.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/workspace.state": major
|
||||
"@pnpm/deps.status": major
|
||||
---
|
||||
|
||||
Initial Release
|
||||
5
.changeset/fuzzy-rocks-push.md
Normal file
5
.changeset/fuzzy-rocks-push.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/lockfile.verification": minor
|
||||
---
|
||||
|
||||
Export `linkedPackagesAreUpToDate` and `getWorkspacePackagesByDirectory`
|
||||
7
.changeset/orange-foxes-hope.md
Normal file
7
.changeset/orange-foxes-hope.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-script-runners": minor
|
||||
"@pnpm/config": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Add a feature to check dependencies before running scripts [#8585](https://github.com/pnpm/pnpm/issues/8585).
|
||||
5
.changeset/popular-rice-study.md
Normal file
5
.changeset/popular-rice-study.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-installation": minor
|
||||
---
|
||||
|
||||
Save a cache of packages list on every recursive install
|
||||
@@ -38,6 +38,7 @@ export interface Config {
|
||||
global?: boolean
|
||||
dir: string
|
||||
bin: string
|
||||
verifyDepsBeforeRun?: boolean
|
||||
ignoreDepScripts?: boolean
|
||||
ignoreScripts?: boolean
|
||||
ignoreCompatibilityDb?: boolean
|
||||
|
||||
15
deps/status/README.md
vendored
Normal file
15
deps/status/README.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# @pnpm/deps.status
|
||||
|
||||
> Check dependencies status
|
||||
|
||||
[](https://www.npmjs.com/package/@pnpm/deps.status)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @pnpm/deps.status
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
66
deps/status/package.json
vendored
Normal file
66
deps/status/package.json
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "@pnpm/deps.status",
|
||||
"version": "0.0.0",
|
||||
"description": "Check dependencies status",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"_test": "jest",
|
||||
"test": "pnpm run compile && pnpm run _test",
|
||||
"prepublishOnly": "pnpm run compile",
|
||||
"start": "tsc --watch",
|
||||
"compile": "tsc --build && pnpm run lint --fix"
|
||||
},
|
||||
"repository": "https://github.com/pnpm/pnpm/blob/main/deps/status",
|
||||
"keywords": [
|
||||
"pnpm10",
|
||||
"pnpm",
|
||||
"scripts"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/deps/status#readme",
|
||||
"dependencies": {
|
||||
"@pnpm/config": "workspace:*",
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/crypto.object-hasher": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/get-context": "workspace:*",
|
||||
"@pnpm/lockfile.fs": "workspace:*",
|
||||
"@pnpm/lockfile.settings-checker": "workspace:*",
|
||||
"@pnpm/lockfile.verification": "workspace:*",
|
||||
"@pnpm/parse-overrides": "workspace:*",
|
||||
"@pnpm/resolver-base": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"@pnpm/workspace.find-packages": "workspace:*",
|
||||
"@pnpm/workspace.read-manifest": "workspace:*",
|
||||
"@pnpm/workspace.state": "workspace:*",
|
||||
"ramda": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/deps.status": "workspace:*",
|
||||
"@pnpm/prepare": "workspace:*",
|
||||
"@pnpm/write-project-manifest": "workspace:*",
|
||||
"@types/ramda": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pnpm/logger": "^5.1.0"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"exports": {
|
||||
".": "./lib/index.js"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "@pnpm/jest-config"
|
||||
}
|
||||
}
|
||||
20
deps/status/src/assertLockfilesEqual.ts
vendored
Normal file
20
deps/status/src/assertLockfilesEqual.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { type Lockfile } from '@pnpm/lockfile.fs'
|
||||
import { equals } from 'ramda'
|
||||
|
||||
export function assertLockfilesEqual (currentLockfile: Lockfile | null, wantedLockfile: Lockfile, wantedLockfileDir: string): void {
|
||||
if (!currentLockfile) {
|
||||
// make sure that no importer of wantedLockfile has any dependency
|
||||
for (const [name, snapshot] of Object.entries(wantedLockfile.importers)) {
|
||||
if (!equals(snapshot.specifiers, {})) {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_NO_DEPS', `Project ${name} requires dependencies but none was installed.`, {
|
||||
hint: 'Run `pnpm install` to install dependencies',
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (!equals(currentLockfile, wantedLockfile)) {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_OUTDATED_DEPS', `The installed dependencies in the modules directory is not up-to-date with the lockfile in ${wantedLockfileDir}.`, {
|
||||
hint: 'Run `pnpm install` to update dependencies.',
|
||||
})
|
||||
}
|
||||
}
|
||||
406
deps/status/src/checkLockfilesUpToDate.ts
vendored
Normal file
406
deps/status/src/checkLockfilesUpToDate.ts
vendored
Normal file
@@ -0,0 +1,406 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import util from 'util'
|
||||
import equals from 'ramda/src/equals'
|
||||
import isEmpty from 'ramda/src/isEmpty'
|
||||
import filter from 'ramda/src/filter'
|
||||
import once from 'ramda/src/once'
|
||||
import { type Config, type OptionsFromRootManifest, getOptionsFromRootManifest } from '@pnpm/config'
|
||||
import { MANIFEST_BASE_NAMES, WANTED_LOCKFILE } from '@pnpm/constants'
|
||||
import { hashObjectNullableWithPrefix } from '@pnpm/crypto.object-hasher'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { arrayOfWorkspacePackagesToMap } from '@pnpm/get-context'
|
||||
import {
|
||||
type Lockfile,
|
||||
getLockfileImporterId,
|
||||
readCurrentLockfile,
|
||||
readWantedLockfile,
|
||||
} from '@pnpm/lockfile.fs'
|
||||
import {
|
||||
calcPatchHashes,
|
||||
createOverridesMapFromParsed,
|
||||
getOutdatedLockfileSetting,
|
||||
} from '@pnpm/lockfile.settings-checker'
|
||||
import {
|
||||
linkedPackagesAreUpToDate,
|
||||
getWorkspacePackagesByDirectory,
|
||||
satisfiesPackageManifest,
|
||||
} from '@pnpm/lockfile.verification'
|
||||
import { globalWarn, logger } from '@pnpm/logger'
|
||||
import { parseOverrides } from '@pnpm/parse-overrides'
|
||||
import { type WorkspacePackages } from '@pnpm/resolver-base'
|
||||
import {
|
||||
type DependencyManifest,
|
||||
type Project,
|
||||
type ProjectId,
|
||||
type ProjectManifest,
|
||||
} from '@pnpm/types'
|
||||
import { findWorkspacePackages } from '@pnpm/workspace.find-packages'
|
||||
import { readWorkspaceManifest } from '@pnpm/workspace.read-manifest'
|
||||
import { loadWorkspaceState, updateWorkspaceState } from '@pnpm/workspace.state'
|
||||
import { assertLockfilesEqual } from './assertLockfilesEqual'
|
||||
import { statManifestFile } from './statManifestFile'
|
||||
|
||||
export type CheckLockfilesUpToDateOptions = Pick<Config,
|
||||
| 'allProjects'
|
||||
| 'autoInstallPeers'
|
||||
| 'catalogs'
|
||||
| 'excludeLinksFromLockfile'
|
||||
| 'linkWorkspacePackages'
|
||||
| 'hooks'
|
||||
| 'peersSuffixMaxLength'
|
||||
| 'rootProjectManifest'
|
||||
| 'rootProjectManifestDir'
|
||||
| 'sharedWorkspaceLockfile'
|
||||
| 'virtualStoreDir'
|
||||
| 'workspaceDir'
|
||||
>
|
||||
|
||||
export async function checkLockfilesUpToDate (opts: CheckLockfilesUpToDateOptions): Promise<void> {
|
||||
const {
|
||||
allProjects,
|
||||
autoInstallPeers,
|
||||
catalogs,
|
||||
excludeLinksFromLockfile,
|
||||
linkWorkspacePackages,
|
||||
rootProjectManifest,
|
||||
rootProjectManifestDir,
|
||||
sharedWorkspaceLockfile,
|
||||
workspaceDir,
|
||||
} = opts
|
||||
|
||||
const rootManifestOptions = rootProjectManifest && rootProjectManifestDir
|
||||
? getOptionsFromRootManifest(rootProjectManifestDir, rootProjectManifest)
|
||||
: undefined
|
||||
|
||||
if (allProjects && workspaceDir) {
|
||||
const workspaceState = loadWorkspaceState(workspaceDir)
|
||||
if (!workspaceState) {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_NO_CACHE', 'Cannot check whether dependencies are outdated', {
|
||||
hint: 'Run `pnpm install` to create the cache',
|
||||
})
|
||||
}
|
||||
|
||||
if (!equals(
|
||||
filter(value => value != null, workspaceState.catalogs ?? {}),
|
||||
filter(value => value != null, catalogs ?? {})
|
||||
)) {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_OUTDATED', 'Catalogs cache outdated', {
|
||||
hint: 'Run `pnpm install` to update the catalogs cache',
|
||||
})
|
||||
}
|
||||
|
||||
const currentProjectRootDirs = allProjects.map(project => project.rootDir).sort()
|
||||
if (!equals(workspaceState.projectRootDirs, currentProjectRootDirs)) {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_WORKSPACE_STRUCTURE_CHANGED', 'The workspace structure has changed since last install', {
|
||||
hint: 'Run `pnpm install` to update the workspace structure and dependencies tree',
|
||||
})
|
||||
}
|
||||
|
||||
const allManifestStats = await Promise.all(allProjects.map(async project => {
|
||||
const manifestStats = await statManifestFile(project.rootDir)
|
||||
if (!manifestStats) {
|
||||
// this error should not happen
|
||||
throw new Error(`Cannot find one of ${MANIFEST_BASE_NAMES.join(', ')} in ${project.rootDir}`)
|
||||
}
|
||||
return { project, manifestStats }
|
||||
}))
|
||||
|
||||
const modifiedProjects = allManifestStats.filter(
|
||||
({ manifestStats }) =>
|
||||
manifestStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp
|
||||
)
|
||||
|
||||
if (modifiedProjects.length === 0) {
|
||||
logger.debug({ msg: 'No manifest files were modified since the last validation. Exiting check.' })
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug({ msg: 'Some manifest files were modified since the last validation. Continuing check.' })
|
||||
|
||||
let readWantedLockfileAndDir: (projectDir: string) => Promise<{
|
||||
wantedLockfile: Lockfile
|
||||
wantedLockfileDir: string
|
||||
}>
|
||||
if (sharedWorkspaceLockfile) {
|
||||
let wantedLockfileStats: fs.Stats
|
||||
try {
|
||||
wantedLockfileStats = fs.statSync(path.join(workspaceDir, WANTED_LOCKFILE))
|
||||
} catch (error) {
|
||||
if (util.types.isNativeError(error) && 'code' in error && error.code === 'ENOENT') {
|
||||
return throwLockfileNotFound(workspaceDir)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const wantedLockfilePromise = readWantedLockfile(workspaceDir, { ignoreIncompatible: false })
|
||||
if (wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp) {
|
||||
const virtualStoreDir = opts.virtualStoreDir ?? path.join(workspaceDir, 'node_modules', '.pnpm')
|
||||
const currentLockfile = await readCurrentLockfile(virtualStoreDir, { ignoreIncompatible: false })
|
||||
const wantedLockfile = (await wantedLockfilePromise) ?? throwLockfileNotFound(workspaceDir)
|
||||
assertLockfilesEqual(currentLockfile, wantedLockfile, workspaceDir)
|
||||
}
|
||||
readWantedLockfileAndDir = async () => ({
|
||||
wantedLockfile: (await wantedLockfilePromise) ?? throwLockfileNotFound(workspaceDir),
|
||||
wantedLockfileDir: workspaceDir,
|
||||
})
|
||||
} else {
|
||||
readWantedLockfileAndDir = async wantedLockfileDir => {
|
||||
const wantedLockfilePromise = readWantedLockfile(wantedLockfileDir, { ignoreIncompatible: false })
|
||||
const wantedLockfileStats = await statIfExists(path.join(wantedLockfileDir, WANTED_LOCKFILE))
|
||||
|
||||
if (!wantedLockfileStats) return throwLockfileNotFound(wantedLockfileDir)
|
||||
if (wantedLockfileStats.mtime.valueOf() > workspaceState.lastValidatedTimestamp) {
|
||||
const virtualStoreDir = opts.virtualStoreDir ?? path.join(wantedLockfileDir, 'node_modules', '.pnpm')
|
||||
const currentLockfile = await readCurrentLockfile(virtualStoreDir, { ignoreIncompatible: false })
|
||||
const wantedLockfile = (await wantedLockfilePromise) ?? throwLockfileNotFound(wantedLockfileDir)
|
||||
assertLockfilesEqual(currentLockfile, wantedLockfile, wantedLockfileDir)
|
||||
}
|
||||
|
||||
return {
|
||||
wantedLockfile: (await wantedLockfilePromise) ?? throwLockfileNotFound(wantedLockfileDir),
|
||||
wantedLockfileDir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type GetProjectId = (project: Pick<Project, 'rootDir'>) => ProjectId
|
||||
const getProjectId: GetProjectId = sharedWorkspaceLockfile
|
||||
? project => getLockfileImporterId(workspaceDir, project.rootDir)
|
||||
: () => '.' as ProjectId
|
||||
|
||||
const getWorkspacePackages = once(arrayOfWorkspacePackagesToMap.bind(null, allProjects))
|
||||
const getManifestsByDir = once(() => getWorkspacePackagesByDirectory(getWorkspacePackages()))
|
||||
|
||||
const assertCtx: AssertWantedLockfileUpToDateContext = {
|
||||
autoInstallPeers,
|
||||
config: opts,
|
||||
excludeLinksFromLockfile,
|
||||
linkWorkspacePackages,
|
||||
getManifestsByDir,
|
||||
getWorkspacePackages,
|
||||
rootDir: workspaceDir,
|
||||
rootManifestOptions,
|
||||
}
|
||||
|
||||
await Promise.all(modifiedProjects.map(async ({ project }) => {
|
||||
const { wantedLockfile, wantedLockfileDir } = await readWantedLockfileAndDir(project.rootDir)
|
||||
await assertWantedLockfileUpToDate(assertCtx, {
|
||||
projectDir: project.rootDir,
|
||||
projectId: getProjectId(project),
|
||||
projectManifest: project.manifest,
|
||||
wantedLockfile,
|
||||
wantedLockfileDir,
|
||||
})
|
||||
}))
|
||||
|
||||
// update lastValidatedTimestamp to prevent pointless repeat
|
||||
await updateWorkspaceState({
|
||||
allProjects,
|
||||
catalogs,
|
||||
lastValidatedTimestamp: Date.now(),
|
||||
workspaceDir,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!allProjects) {
|
||||
const workspaceRoot = workspaceDir ?? rootProjectManifestDir
|
||||
const workspaceManifest = await readWorkspaceManifest(workspaceRoot)
|
||||
if (workspaceManifest ?? workspaceDir) {
|
||||
const allProjects = await findWorkspacePackages(rootProjectManifestDir, {
|
||||
patterns: workspaceManifest?.packages,
|
||||
sharedWorkspaceLockfile,
|
||||
})
|
||||
return checkLockfilesUpToDate({
|
||||
...opts,
|
||||
allProjects,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// this error shouldn't happen
|
||||
throw new Error('Impossible variant: allProjects is defined but workspaceDir is undefined')
|
||||
}
|
||||
|
||||
if (rootProjectManifest && rootProjectManifestDir) {
|
||||
const virtualStoreDir = path.join(rootProjectManifestDir, 'node_modules', '.pnpm')
|
||||
const currentLockfilePromise = readCurrentLockfile(virtualStoreDir, { ignoreIncompatible: false })
|
||||
const wantedLockfilePromise = readWantedLockfile(rootProjectManifestDir, { ignoreIncompatible: false })
|
||||
const [
|
||||
currentLockfileStats,
|
||||
wantedLockfileStats,
|
||||
manifestStats,
|
||||
] = await Promise.all([
|
||||
statIfExists(path.join(virtualStoreDir, 'lock.yaml')),
|
||||
statIfExists(path.join(rootProjectManifestDir, WANTED_LOCKFILE)),
|
||||
statManifestFile(rootProjectManifestDir),
|
||||
])
|
||||
|
||||
if (!wantedLockfileStats) return throwLockfileNotFound(rootProjectManifestDir)
|
||||
|
||||
if (currentLockfileStats && wantedLockfileStats.mtime.valueOf() > currentLockfileStats.mtime.valueOf()) {
|
||||
const currentLockfile = await currentLockfilePromise
|
||||
const wantedLockfile = (await wantedLockfilePromise) ?? throwLockfileNotFound(rootProjectManifestDir)
|
||||
assertLockfilesEqual(currentLockfile, wantedLockfile, rootProjectManifestDir)
|
||||
}
|
||||
|
||||
if (!manifestStats) {
|
||||
// this error should not happen
|
||||
throw new Error(`Cannot find one of ${MANIFEST_BASE_NAMES.join(', ')} in ${rootProjectManifestDir}`)
|
||||
}
|
||||
|
||||
if (manifestStats.mtime.valueOf() > wantedLockfileStats.mtime.valueOf()) {
|
||||
logger.debug({ msg: 'The manifest is newer than the lockfile. Continuing check.' })
|
||||
await assertWantedLockfileUpToDate({
|
||||
autoInstallPeers,
|
||||
config: opts,
|
||||
excludeLinksFromLockfile,
|
||||
linkWorkspacePackages,
|
||||
getManifestsByDir: () => ({}),
|
||||
getWorkspacePackages: () => undefined,
|
||||
rootDir: rootProjectManifestDir,
|
||||
rootManifestOptions,
|
||||
}, {
|
||||
projectDir: rootProjectManifestDir,
|
||||
projectId: '.' as ProjectId,
|
||||
projectManifest: rootProjectManifest,
|
||||
wantedLockfile: (await wantedLockfilePromise) ?? throwLockfileNotFound(rootProjectManifestDir),
|
||||
wantedLockfileDir: rootProjectManifestDir,
|
||||
})
|
||||
} else if (currentLockfileStats) {
|
||||
logger.debug({ msg: 'The manifest file is not newer than the lockfile. Exiting check.' })
|
||||
} else {
|
||||
const wantedLockfile = (await wantedLockfilePromise) ?? throwLockfileNotFound(rootProjectManifestDir)
|
||||
if (!isEmpty(wantedLockfile.packages ?? {})) {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_NO_DEPS', 'The lockfile requires dependencies but none were installed', {
|
||||
hint: 'Run `pnpm install` to install dependencies',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// `opts.allProject` being `undefined` means that the run command was not run with `--recursive`.
|
||||
// `rootProjectManifest` being `undefined` means that there's no root manifest.
|
||||
// Both means that `pnpm run` would fail, so checking lockfiles here is pointless.
|
||||
globalWarn('Skipping check.')
|
||||
}
|
||||
|
||||
interface AssertWantedLockfileUpToDateContext {
|
||||
autoInstallPeers?: boolean
|
||||
config: CheckLockfilesUpToDateOptions
|
||||
excludeLinksFromLockfile?: boolean
|
||||
linkWorkspacePackages: boolean | 'deep'
|
||||
getManifestsByDir: () => Record<string, DependencyManifest>
|
||||
getWorkspacePackages: () => WorkspacePackages | undefined
|
||||
rootDir: string
|
||||
rootManifestOptions: OptionsFromRootManifest | undefined
|
||||
}
|
||||
|
||||
interface AssertWantedLockfileUpToDateOptions {
|
||||
projectDir: string
|
||||
projectId: ProjectId
|
||||
projectManifest: ProjectManifest
|
||||
wantedLockfile: Lockfile
|
||||
wantedLockfileDir: string
|
||||
}
|
||||
|
||||
async function assertWantedLockfileUpToDate (
|
||||
ctx: AssertWantedLockfileUpToDateContext,
|
||||
opts: AssertWantedLockfileUpToDateOptions
|
||||
): Promise<void> {
|
||||
const {
|
||||
autoInstallPeers,
|
||||
config,
|
||||
excludeLinksFromLockfile,
|
||||
linkWorkspacePackages,
|
||||
getManifestsByDir,
|
||||
getWorkspacePackages,
|
||||
rootDir,
|
||||
rootManifestOptions,
|
||||
} = ctx
|
||||
|
||||
const {
|
||||
projectDir,
|
||||
projectId,
|
||||
projectManifest,
|
||||
wantedLockfile,
|
||||
wantedLockfileDir,
|
||||
} = opts
|
||||
|
||||
const [
|
||||
patchedDependencies,
|
||||
pnpmfileChecksum,
|
||||
] = await Promise.all([
|
||||
calcPatchHashes(rootManifestOptions?.patchedDependencies ?? {}, rootDir),
|
||||
config.hooks?.calculatePnpmfileChecksum?.(),
|
||||
])
|
||||
|
||||
const outdatedLockfileSettingName = getOutdatedLockfileSetting(wantedLockfile, {
|
||||
autoInstallPeers: config.autoInstallPeers,
|
||||
excludeLinksFromLockfile: config.excludeLinksFromLockfile,
|
||||
peersSuffixMaxLength: config.peersSuffixMaxLength,
|
||||
overrides: createOverridesMapFromParsed(parseOverrides(rootManifestOptions?.overrides ?? {}, config.catalogs)),
|
||||
ignoredOptionalDependencies: rootManifestOptions?.ignoredOptionalDependencies?.sort(),
|
||||
packageExtensionsChecksum: hashObjectNullableWithPrefix(rootManifestOptions?.packageExtensions),
|
||||
patchedDependencies,
|
||||
pnpmfileChecksum,
|
||||
})
|
||||
|
||||
if (outdatedLockfileSettingName) {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_OUTDATED_LOCKFILE', `Setting ${outdatedLockfileSettingName} of lockfile in ${wantedLockfileDir} is outdated`, {
|
||||
hint: 'Run `pnpm install` to update the lockfile',
|
||||
})
|
||||
}
|
||||
|
||||
if (!satisfiesPackageManifest(
|
||||
{
|
||||
autoInstallPeers,
|
||||
excludeLinksFromLockfile,
|
||||
},
|
||||
wantedLockfile.importers[projectId],
|
||||
projectManifest
|
||||
).satisfies) {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST', `The lockfile in ${wantedLockfileDir} does not satisfy project of id ${projectId}`, {
|
||||
hint: 'Run `pnpm install` to update the lockfile',
|
||||
})
|
||||
}
|
||||
|
||||
if (!await linkedPackagesAreUpToDate({
|
||||
linkWorkspacePackages: !!linkWorkspacePackages,
|
||||
lockfileDir: wantedLockfileDir,
|
||||
manifestsByDir: getManifestsByDir(),
|
||||
workspacePackages: getWorkspacePackages(),
|
||||
lockfilePackages: wantedLockfile.packages,
|
||||
}, {
|
||||
dir: projectDir,
|
||||
manifest: projectManifest,
|
||||
snapshot: wantedLockfile.importers[projectId],
|
||||
})) {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_LINKED_PKGS_OUTDATED', `The linked packages by ${projectDir} is outdated`, {
|
||||
hint: 'Run `pnpm install` to update the packages',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function statIfExists (filePath: string): Promise<fs.Stats | undefined> {
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = await fs.promises.stat(filePath)
|
||||
} catch (error) {
|
||||
if (util.types.isNativeError(error) && 'code' in error && error.code === 'ENOENT') {
|
||||
return undefined
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
function throwLockfileNotFound (wantedLockfileDir: string): never {
|
||||
throw new PnpmError('RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND', `Cannot find a lockfile in ${wantedLockfileDir}`, {
|
||||
hint: 'Run `pnpm install` to create the lockfile',
|
||||
})
|
||||
}
|
||||
1
deps/status/src/index.ts
vendored
Normal file
1
deps/status/src/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { type CheckLockfilesUpToDateOptions, checkLockfilesUpToDate } from './checkLockfilesUpToDate'
|
||||
21
deps/status/src/statManifestFile.ts
vendored
Normal file
21
deps/status/src/statManifestFile.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import util from 'util'
|
||||
import { MANIFEST_BASE_NAMES } from '@pnpm/constants'
|
||||
|
||||
export async function statManifestFile (projectRootDir: string): Promise<fs.Stats | undefined> {
|
||||
const attempts = await Promise.all(MANIFEST_BASE_NAMES.map(async baseName => {
|
||||
const manifestPath = path.join(projectRootDir, baseName)
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = await fs.promises.stat(manifestPath)
|
||||
} catch (error) {
|
||||
if (util.types.isNativeError(error) && 'code' in error && error.code === 'ENOENT') {
|
||||
return undefined
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return stats
|
||||
}))
|
||||
return attempts.find(stats => stats != null)
|
||||
}
|
||||
82
deps/status/test/assertLockfilesEqual.test.ts
vendored
Normal file
82
deps/status/test/assertLockfilesEqual.test.ts
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
import { LOCKFILE_VERSION } from '@pnpm/constants'
|
||||
import { type Lockfile } from '@pnpm/lockfile.fs'
|
||||
import { type ProjectId } from '@pnpm/types'
|
||||
import { assertLockfilesEqual } from '../src/assertLockfilesEqual'
|
||||
|
||||
test('if wantedLockfile does not have any specifier, currentLockfile is allowed to be null', () => {
|
||||
assertLockfilesEqual(null, {
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
importers: {
|
||||
['.' as ProjectId]: {
|
||||
specifiers: {},
|
||||
},
|
||||
},
|
||||
}, '<LOCKFILE_DIR>')
|
||||
})
|
||||
|
||||
test('should throw if wantedLockfile has specifiers but currentLockfile is null', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
expect(() => assertLockfilesEqual(null, {
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
importers: {
|
||||
['.' as ProjectId]: {
|
||||
specifiers: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
dependencies: {
|
||||
foo: '1.0.1',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, '<LOCKFILE_DIR>')).toThrow('Project . requires dependencies but none was installed.')
|
||||
})
|
||||
|
||||
test('should not throw if wantedLockfile and currentLockfile are equal', () => {
|
||||
const lockfile = (): Lockfile => ({
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
importers: {
|
||||
['.' as ProjectId]: {
|
||||
specifiers: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
dependencies: {
|
||||
foo: '1.0.1',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assertLockfilesEqual(lockfile(), lockfile(), '<LOCKFILE_DIR>')
|
||||
})
|
||||
|
||||
test('should throw if wantedLockfile and currentLockfile are not equal', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
expect(() => assertLockfilesEqual(
|
||||
{
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
importers: {
|
||||
['.' as ProjectId]: {
|
||||
specifiers: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
dependencies: {
|
||||
foo: '1.0.1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
lockfileVersion: LOCKFILE_VERSION,
|
||||
importers: {
|
||||
['.' as ProjectId]: {
|
||||
specifiers: {
|
||||
foo: '^1.0.0',
|
||||
},
|
||||
dependencies: {
|
||||
foo: '1.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'<LOCKFILE_DIR>')
|
||||
).toThrow('The installed dependencies in the modules directory is not up-to-date with the lockfile in <LOCKFILE_DIR>.')
|
||||
})
|
||||
17
deps/status/test/statManifestFile.test.ts
vendored
Normal file
17
deps/status/test/statManifestFile.test.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { MANIFEST_BASE_NAMES } from '@pnpm/constants'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { writeProjectManifest } from '@pnpm/write-project-manifest'
|
||||
import { statManifestFile } from '../src/statManifestFile'
|
||||
|
||||
test.each(MANIFEST_BASE_NAMES)('load %s', async baseName => {
|
||||
prepareEmpty()
|
||||
await writeProjectManifest(`foo/bar/${baseName}`, { name: 'foo', version: '1.0.0' })
|
||||
const stats = await statManifestFile('foo/bar')
|
||||
expect(stats).toBeDefined()
|
||||
expect(stats?.isFile()).toBe(true)
|
||||
})
|
||||
|
||||
test('should return undefined if no manifest is found', async () => {
|
||||
prepareEmpty()
|
||||
expect(await statManifestFile('.')).toBeUndefined()
|
||||
})
|
||||
17
deps/status/test/tsconfig.json
vendored
Normal file
17
deps/status/test/tsconfig.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "../test.lib",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
61
deps/status/tsconfig.json
vendored
Normal file
61
deps/status/tsconfig.json
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../__utils__/prepare"
|
||||
},
|
||||
{
|
||||
"path": "../../config/config"
|
||||
},
|
||||
{
|
||||
"path": "../../config/parse-overrides"
|
||||
},
|
||||
{
|
||||
"path": "../../crypto/object-hasher"
|
||||
},
|
||||
{
|
||||
"path": "../../lockfile/fs"
|
||||
},
|
||||
{
|
||||
"path": "../../lockfile/settings-checker"
|
||||
},
|
||||
{
|
||||
"path": "../../lockfile/verification"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/constants"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/types"
|
||||
},
|
||||
{
|
||||
"path": "../../pkg-manager/get-context"
|
||||
},
|
||||
{
|
||||
"path": "../../pkg-manifest/write-project-manifest"
|
||||
},
|
||||
{
|
||||
"path": "../../resolving/resolver-base"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/find-packages"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/read-manifest"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/state"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
deps/status/tsconfig.lint.json
vendored
Normal file
8
deps/status/tsconfig.lint.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -51,6 +51,7 @@
|
||||
"@pnpm/config": "workspace:*",
|
||||
"@pnpm/core-loggers": "workspace:*",
|
||||
"@pnpm/crypto.hash": "workspace:*",
|
||||
"@pnpm/deps.status": "workspace:*",
|
||||
"@pnpm/env.path": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/lifecycle": "workspace:*",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { sortPackages } from '@pnpm/sort-packages'
|
||||
import { type Project, type ProjectsGraph, type ProjectRootDir, type ProjectRootDirRealPath } from '@pnpm/types'
|
||||
import execa from 'execa'
|
||||
import pLimit from 'p-limit'
|
||||
import { type CheckLockfilesUpToDateOptions, checkLockfilesUpToDate } from '@pnpm/deps.status'
|
||||
import { prependDirsToPath } from '@pnpm/env.path'
|
||||
import pick from 'ramda/src/pick'
|
||||
import renderHelp from 'render-help'
|
||||
@@ -26,6 +27,7 @@ import { PnpmError } from '@pnpm/error'
|
||||
import which from 'which'
|
||||
import writeJsonFile from 'write-json-file'
|
||||
import { getNearestProgram, getNearestScript } from './buildCommandNotFoundHint'
|
||||
import { DISABLE_DEPS_CHECK_ENV, SKIP_ENV_KEY } from './shouldRunCheck'
|
||||
|
||||
export const shorthands: Record<string, string | string[]> = {
|
||||
parallel: runShorthands.parallel,
|
||||
@@ -157,8 +159,12 @@ export type ExecOpts = Required<Pick<Config, 'selectedProjectsGraph'>> & {
|
||||
| 'recursive'
|
||||
| 'reporterHidePrefix'
|
||||
| 'userAgent'
|
||||
| 'verifyDepsBeforeRun'
|
||||
| 'workspaceDir'
|
||||
>
|
||||
> & (
|
||||
| { verifyDepsBeforeRun?: false }
|
||||
| { verifyDepsBeforeRun: true } & CheckLockfilesUpToDateOptions
|
||||
)
|
||||
|
||||
export async function handler (
|
||||
opts: ExecOpts,
|
||||
@@ -170,6 +176,10 @@ export async function handler (
|
||||
}
|
||||
const limitRun = pLimit(opts.workspaceConcurrency ?? 4)
|
||||
|
||||
if (opts.verifyDepsBeforeRun && !process.env[SKIP_ENV_KEY]) {
|
||||
await checkLockfilesUpToDate(opts)
|
||||
}
|
||||
|
||||
let chunks!: ProjectRootDir[][]
|
||||
if (opts.recursive) {
|
||||
chunks = opts.sort
|
||||
@@ -238,6 +248,7 @@ export async function handler (
|
||||
...extraEnv,
|
||||
PNPM_PACKAGE_NAME: opts.selectedProjectsGraph[prefix]?.package.manifest.name,
|
||||
...(opts.nodeOptions ? { NODE_OPTIONS: opts.nodeOptions } : {}),
|
||||
...opts.verifyDepsBeforeRun ? DISABLE_DEPS_CHECK_ENV : undefined,
|
||||
},
|
||||
prependPaths,
|
||||
userAgent: opts.userAgent,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { type CompletionFunc } from '@pnpm/command'
|
||||
import { prepareExecutionEnv } from '@pnpm/plugin-commands-env'
|
||||
import { FILTERING, UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help'
|
||||
import { type Config, types as allTypes } from '@pnpm/config'
|
||||
import { type CheckLockfilesUpToDateOptions, checkLockfilesUpToDate } from '@pnpm/deps.status'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import {
|
||||
runLifecycleHook,
|
||||
@@ -23,6 +24,7 @@ import { runRecursive, type RecursiveRunOpts, getSpecifiedScripts as getSpecifie
|
||||
import { existsInDir } from './existsInDir'
|
||||
import { handler as exec } from './exec'
|
||||
import { buildCommandNotFoundHint } from './buildCommandNotFoundHint'
|
||||
import { DISABLE_DEPS_CHECK_ENV, shouldRunCheck } from './shouldRunCheck'
|
||||
|
||||
export const IF_PRESENT_OPTION: Record<string, unknown> = {
|
||||
'if-present': Boolean,
|
||||
@@ -158,6 +160,7 @@ export type RunOpts =
|
||||
& { recursive?: boolean }
|
||||
& Pick<Config,
|
||||
| 'bin'
|
||||
| 'verifyDepsBeforeRun'
|
||||
| 'dir'
|
||||
| 'enablePrePostScripts'
|
||||
| 'engineStrict'
|
||||
@@ -181,6 +184,10 @@ export type RunOpts =
|
||||
}
|
||||
fallbackCommandUsed?: boolean
|
||||
}
|
||||
& (
|
||||
| { verifyDepsBeforeRun?: false }
|
||||
| { verifyDepsBeforeRun: true } & CheckLockfilesUpToDateOptions
|
||||
)
|
||||
|
||||
export async function handler (
|
||||
opts: RunOpts,
|
||||
@@ -188,8 +195,21 @@ export async function handler (
|
||||
): Promise<string | { exitCode: number } | undefined> {
|
||||
let dir: string
|
||||
let [scriptName, ...passedThruArgs] = params
|
||||
|
||||
// verifyDepsBeforeRun is outside of shouldRunCheck because TypeScript's tagged union
|
||||
// only works when the tag is directly placed in the condition.
|
||||
if (opts.verifyDepsBeforeRun && shouldRunCheck(process.env, scriptName)) {
|
||||
await checkLockfilesUpToDate(opts)
|
||||
}
|
||||
|
||||
if (opts.recursive) {
|
||||
if (scriptName || Object.keys(opts.selectedProjectsGraph).length > 1) {
|
||||
if (opts.verifyDepsBeforeRun) {
|
||||
opts.extraEnv = {
|
||||
...opts.extraEnv,
|
||||
...DISABLE_DEPS_CHECK_ENV,
|
||||
}
|
||||
}
|
||||
return runRecursive(params, opts) as Promise<undefined>
|
||||
}
|
||||
dir = Object.keys(opts.selectedProjectsGraph)[0]
|
||||
@@ -250,6 +270,7 @@ so you may run "pnpm -w run ${scriptName}"`,
|
||||
const extraEnv = {
|
||||
...opts.extraEnv,
|
||||
...(opts.nodeOptions ? { NODE_OPTIONS: opts.nodeOptions } : {}),
|
||||
...opts.verifyDepsBeforeRun ? DISABLE_DEPS_CHECK_ENV : undefined,
|
||||
}
|
||||
|
||||
const lifecycleOpts: RunLifecycleHookOptions = {
|
||||
|
||||
22
exec/plugin-commands-script-runners/src/shouldRunCheck.ts
Normal file
22
exec/plugin-commands-script-runners/src/shouldRunCheck.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// The scripts that `pnpm run` executes are likely to also execute other `pnpm run`.
|
||||
// We don't want this potentially expensive check to repeat.
|
||||
// The solution is to use an env key to disable the check.
|
||||
export const SKIP_ENV_KEY = 'pnpm_run_skip_deps_check'
|
||||
export const DISABLE_DEPS_CHECK_ENV = {
|
||||
[SKIP_ENV_KEY]: 'true',
|
||||
} as const satisfies Env
|
||||
|
||||
export interface Env extends NodeJS.ProcessEnv {
|
||||
[SKIP_ENV_KEY]?: string
|
||||
}
|
||||
|
||||
const SCRIPTS_TO_SKIP = [
|
||||
'preinstall',
|
||||
'install',
|
||||
'postinstall',
|
||||
'preuninstall',
|
||||
'uninstall',
|
||||
'postuninstall',
|
||||
]
|
||||
|
||||
export const shouldRunCheck = (env: Env, scriptName: string): boolean => !env[SKIP_ENV_KEY] && !SCRIPTS_TO_SKIP.includes(scriptName)
|
||||
@@ -0,0 +1,20 @@
|
||||
import { DISABLE_DEPS_CHECK_ENV, shouldRunCheck } from '../src/shouldRunCheck'
|
||||
|
||||
test('should return true if skip env is not defined and script name is not special', () => {
|
||||
expect(shouldRunCheck({}, 'start')).toBe(true)
|
||||
})
|
||||
|
||||
test('should return false if skip env is defined', () => {
|
||||
expect(shouldRunCheck({ ...DISABLE_DEPS_CHECK_ENV }, 'start')).toBe(false)
|
||||
})
|
||||
|
||||
test.each([
|
||||
'preinstall',
|
||||
'install',
|
||||
'postinstall',
|
||||
'preuninstall',
|
||||
'uninstall',
|
||||
'postuninstall',
|
||||
])('should return false if script name is %p', scriptName => {
|
||||
expect(shouldRunCheck({}, scriptName)).toBe(false)
|
||||
})
|
||||
@@ -30,6 +30,9 @@
|
||||
{
|
||||
"path": "../../crypto/hash"
|
||||
},
|
||||
{
|
||||
"path": "../../deps/status"
|
||||
},
|
||||
{
|
||||
"path": "../../env/path"
|
||||
},
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import path from 'path'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type ProjectOptions } from '@pnpm/get-context'
|
||||
import {
|
||||
type PackageSnapshot,
|
||||
type Lockfile,
|
||||
type ProjectSnapshot,
|
||||
type PackageSnapshots,
|
||||
} from '@pnpm/lockfile.types'
|
||||
import { refIsLocalDirectory, refIsLocalTarball } from '@pnpm/lockfile.utils'
|
||||
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
|
||||
import { refToRelative } from '@pnpm/dependency-path'
|
||||
import { type DirectoryResolution, type WorkspacePackages } from '@pnpm/resolver-base'
|
||||
import {
|
||||
DEPENDENCIES_FIELDS,
|
||||
DEPENDENCIES_OR_PEER_FIELDS,
|
||||
type DependencyManifest,
|
||||
type ProjectId,
|
||||
type ProjectManifest,
|
||||
} from '@pnpm/types'
|
||||
import { refIsLocalTarball } from '@pnpm/lockfile.utils'
|
||||
import { type WorkspacePackages } from '@pnpm/resolver-base'
|
||||
import { DEPENDENCIES_FIELDS, type ProjectId } from '@pnpm/types'
|
||||
import pEvery from 'p-every'
|
||||
import any from 'ramda/src/any'
|
||||
import isEmpty from 'ramda/src/isEmpty'
|
||||
import semver from 'semver'
|
||||
import getVersionSelectorType from 'version-selector-type'
|
||||
import { allCatalogsAreUpToDate } from './allCatalogsAreUpToDate'
|
||||
import { getWorkspacePackagesByDirectory } from './getWorkspacePackagesByDirectory'
|
||||
import { linkedPackagesAreUpToDate } from './linkedPackagesAreUpToDate'
|
||||
import { satisfiesPackageManifest } from './satisfiesPackageManifest'
|
||||
|
||||
export async function allProjectsAreUpToDate (
|
||||
@@ -73,138 +62,6 @@ export async function allProjectsAreUpToDate (
|
||||
})
|
||||
}
|
||||
|
||||
function getWorkspacePackagesByDirectory (workspacePackages: WorkspacePackages): Record<string, DependencyManifest> {
|
||||
const workspacePackagesByDirectory: Record<string, DependencyManifest> = {}
|
||||
if (workspacePackages) {
|
||||
for (const pkgVersions of workspacePackages.values()) {
|
||||
for (const { rootDir, manifest } of pkgVersions.values()) {
|
||||
workspacePackagesByDirectory[rootDir] = manifest
|
||||
}
|
||||
}
|
||||
}
|
||||
return workspacePackagesByDirectory
|
||||
}
|
||||
|
||||
async function linkedPackagesAreUpToDate (
|
||||
{
|
||||
linkWorkspacePackages,
|
||||
manifestsByDir,
|
||||
workspacePackages,
|
||||
lockfilePackages,
|
||||
lockfileDir,
|
||||
}: {
|
||||
linkWorkspacePackages: boolean
|
||||
manifestsByDir: Record<string, DependencyManifest>
|
||||
workspacePackages: WorkspacePackages
|
||||
lockfilePackages?: PackageSnapshots
|
||||
lockfileDir: string
|
||||
},
|
||||
project: {
|
||||
dir: string
|
||||
manifest: ProjectManifest
|
||||
snapshot: ProjectSnapshot
|
||||
}
|
||||
): Promise<boolean> {
|
||||
return pEvery(
|
||||
DEPENDENCIES_FIELDS,
|
||||
(depField) => {
|
||||
const lockfileDeps = project.snapshot[depField]
|
||||
const manifestDeps = project.manifest[depField]
|
||||
if ((lockfileDeps == null) || (manifestDeps == null)) return true
|
||||
const depNames = Object.keys(lockfileDeps)
|
||||
return pEvery(
|
||||
depNames,
|
||||
async (depName) => {
|
||||
const currentSpec = manifestDeps[depName]
|
||||
if (!currentSpec) return true
|
||||
const lockfileRef = lockfileDeps[depName]
|
||||
if (refIsLocalDirectory(project.snapshot.specifiers[depName])) {
|
||||
const depPath = refToRelative(lockfileRef, depName)
|
||||
return depPath != null && isLocalFileDepUpdated(lockfileDir, lockfilePackages?.[depPath])
|
||||
}
|
||||
const isLinked = lockfileRef.startsWith('link:')
|
||||
if (
|
||||
isLinked &&
|
||||
(
|
||||
currentSpec.startsWith('link:') ||
|
||||
currentSpec.startsWith('file:') ||
|
||||
currentSpec.startsWith('workspace:.')
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
// https://github.com/pnpm/pnpm/issues/6592
|
||||
// if the dependency is linked and the specified version type is tag, we consider it to be up-to-date to skip full resolution.
|
||||
if (isLinked && getVersionSelectorType(currentSpec)?.type === 'tag') {
|
||||
return true
|
||||
}
|
||||
const linkedDir = isLinked
|
||||
? path.join(project.dir, lockfileRef.slice(5))
|
||||
: workspacePackages?.get(depName)?.get(lockfileRef)?.rootDir
|
||||
if (!linkedDir) return true
|
||||
if (!linkWorkspacePackages && !currentSpec.startsWith('workspace:')) {
|
||||
// we found a linked dir, but we don't want to use it, because it's not specified as a
|
||||
// workspace:x.x.x dependency
|
||||
return true
|
||||
}
|
||||
const linkedPkg = manifestsByDir[linkedDir] ?? await safeReadPackageJsonFromDir(linkedDir)
|
||||
const availableRange = getVersionRange(currentSpec)
|
||||
// This should pass the same options to semver as @pnpm/npm-resolver
|
||||
const localPackageSatisfiesRange = availableRange === '*' || availableRange === '^' || availableRange === '~' ||
|
||||
linkedPkg && semver.satisfies(linkedPkg.version, availableRange, { loose: true })
|
||||
if (isLinked !== localPackageSatisfiesRange) return false
|
||||
return true
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function isLocalFileDepUpdated (lockfileDir: string, pkgSnapshot: PackageSnapshot | undefined): Promise<boolean> {
|
||||
if (!pkgSnapshot) return false
|
||||
const localDepDir = path.join(lockfileDir, (pkgSnapshot.resolution as DirectoryResolution).directory)
|
||||
const manifest = await safeReadPackageJsonFromDir(localDepDir)
|
||||
if (!manifest) return false
|
||||
for (const depField of DEPENDENCIES_OR_PEER_FIELDS) {
|
||||
if (depField === 'devDependencies') continue
|
||||
const manifestDeps = manifest[depField] ?? {}
|
||||
const lockfileDeps = pkgSnapshot[depField] ?? {}
|
||||
|
||||
// Lock file has more dependencies than the current manifest, e.g. some dependencies are removed.
|
||||
if (Object.keys(lockfileDeps).some(depName => !manifestDeps[depName])) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const depName of Object.keys(manifestDeps)) {
|
||||
// If a dependency does not exist in the lock file, e.g. a new dependency is added to the current manifest.
|
||||
// We need to do full resolution again.
|
||||
if (!lockfileDeps[depName]) {
|
||||
return false
|
||||
}
|
||||
const currentSpec = manifestDeps[depName]
|
||||
// We do not care about the link dependencies of local dependency.
|
||||
if (currentSpec.startsWith('file:') || currentSpec.startsWith('link:') || currentSpec.startsWith('workspace:')) continue
|
||||
if (semver.satisfies(lockfileDeps[depName], getVersionRange(currentSpec), { loose: true })) {
|
||||
continue
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function getVersionRange (spec: string): string {
|
||||
if (spec.startsWith('workspace:')) return spec.slice(10)
|
||||
if (spec.startsWith('npm:')) {
|
||||
spec = spec.slice(4)
|
||||
const index = spec.indexOf('@', 1)
|
||||
if (index === -1) return '*'
|
||||
return spec.slice(index + 1) || '*'
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
||||
function hasLocalTarballDepsInRoot (importer: ProjectSnapshot): boolean {
|
||||
return any(refIsLocalTarball, Object.values(importer.dependencies ?? {})) ||
|
||||
any(refIsLocalTarball, Object.values(importer.devDependencies ?? {})) ||
|
||||
|
||||
14
lockfile/verification/src/getWorkspacePackagesByDirectory.ts
Normal file
14
lockfile/verification/src/getWorkspacePackagesByDirectory.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type WorkspacePackages } from '@pnpm/resolver-base'
|
||||
import { type DependencyManifest } from '@pnpm/types'
|
||||
|
||||
export function getWorkspacePackagesByDirectory (workspacePackages: WorkspacePackages): Record<string, DependencyManifest> {
|
||||
const workspacePackagesByDirectory: Record<string, DependencyManifest> = {}
|
||||
if (workspacePackages) {
|
||||
for (const pkgVersions of workspacePackages.values()) {
|
||||
for (const { rootDir, manifest } of pkgVersions.values()) {
|
||||
workspacePackagesByDirectory[rootDir] = manifest
|
||||
}
|
||||
}
|
||||
}
|
||||
return workspacePackagesByDirectory
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export { allProjectsAreUpToDate } from './allProjectsAreUpToDate'
|
||||
export { getWorkspacePackagesByDirectory } from './getWorkspacePackagesByDirectory'
|
||||
export { linkedPackagesAreUpToDate } from './linkedPackagesAreUpToDate'
|
||||
export { satisfiesPackageManifest } from './satisfiesPackageManifest'
|
||||
|
||||
139
lockfile/verification/src/linkedPackagesAreUpToDate.ts
Normal file
139
lockfile/verification/src/linkedPackagesAreUpToDate.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
type PackageSnapshot,
|
||||
type ProjectSnapshot,
|
||||
type PackageSnapshots,
|
||||
} from '@pnpm/lockfile.types'
|
||||
import { refIsLocalDirectory } from '@pnpm/lockfile.utils'
|
||||
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
|
||||
import { refToRelative } from '@pnpm/dependency-path'
|
||||
import { type DirectoryResolution, type WorkspacePackages } from '@pnpm/resolver-base'
|
||||
import {
|
||||
DEPENDENCIES_FIELDS,
|
||||
DEPENDENCIES_OR_PEER_FIELDS,
|
||||
type DependencyManifest,
|
||||
type ProjectManifest,
|
||||
} from '@pnpm/types'
|
||||
import pEvery from 'p-every'
|
||||
import semver from 'semver'
|
||||
import getVersionSelectorType from 'version-selector-type'
|
||||
|
||||
export async function linkedPackagesAreUpToDate (
|
||||
{
|
||||
linkWorkspacePackages,
|
||||
manifestsByDir,
|
||||
workspacePackages,
|
||||
lockfilePackages,
|
||||
lockfileDir,
|
||||
}: {
|
||||
linkWorkspacePackages: boolean
|
||||
manifestsByDir: Record<string, DependencyManifest>
|
||||
workspacePackages?: WorkspacePackages
|
||||
lockfilePackages?: PackageSnapshots
|
||||
lockfileDir: string
|
||||
},
|
||||
project: {
|
||||
dir: string
|
||||
manifest: ProjectManifest
|
||||
snapshot: ProjectSnapshot
|
||||
}
|
||||
): Promise<boolean> {
|
||||
return pEvery(
|
||||
DEPENDENCIES_FIELDS,
|
||||
(depField) => {
|
||||
const lockfileDeps = project.snapshot[depField]
|
||||
const manifestDeps = project.manifest[depField]
|
||||
if ((lockfileDeps == null) || (manifestDeps == null)) return true
|
||||
const depNames = Object.keys(lockfileDeps)
|
||||
return pEvery(
|
||||
depNames,
|
||||
async (depName) => {
|
||||
const currentSpec = manifestDeps[depName]
|
||||
if (!currentSpec) return true
|
||||
const lockfileRef = lockfileDeps[depName]
|
||||
if (refIsLocalDirectory(project.snapshot.specifiers[depName])) {
|
||||
const depPath = refToRelative(lockfileRef, depName)
|
||||
return depPath != null && isLocalFileDepUpdated(lockfileDir, lockfilePackages?.[depPath])
|
||||
}
|
||||
const isLinked = lockfileRef.startsWith('link:')
|
||||
if (
|
||||
isLinked &&
|
||||
(
|
||||
currentSpec.startsWith('link:') ||
|
||||
currentSpec.startsWith('file:') ||
|
||||
currentSpec.startsWith('workspace:.')
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
// https://github.com/pnpm/pnpm/issues/6592
|
||||
// if the dependency is linked and the specified version type is tag, we consider it to be up-to-date to skip full resolution.
|
||||
if (isLinked && getVersionSelectorType(currentSpec)?.type === 'tag') {
|
||||
return true
|
||||
}
|
||||
const linkedDir = isLinked
|
||||
? path.join(project.dir, lockfileRef.slice(5))
|
||||
: workspacePackages?.get(depName)?.get(lockfileRef)?.rootDir
|
||||
if (!linkedDir) return true
|
||||
if (!linkWorkspacePackages && !currentSpec.startsWith('workspace:')) {
|
||||
// we found a linked dir, but we don't want to use it, because it's not specified as a
|
||||
// workspace:x.x.x dependency
|
||||
return true
|
||||
}
|
||||
const linkedPkg = manifestsByDir[linkedDir] ?? await safeReadPackageJsonFromDir(linkedDir)
|
||||
const availableRange = getVersionRange(currentSpec)
|
||||
// This should pass the same options to semver as @pnpm/npm-resolver
|
||||
const localPackageSatisfiesRange = availableRange === '*' || availableRange === '^' || availableRange === '~' ||
|
||||
linkedPkg && semver.satisfies(linkedPkg.version, availableRange, { loose: true })
|
||||
if (isLinked !== localPackageSatisfiesRange) return false
|
||||
return true
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function isLocalFileDepUpdated (lockfileDir: string, pkgSnapshot: PackageSnapshot | undefined): Promise<boolean> {
|
||||
if (!pkgSnapshot) return false
|
||||
const localDepDir = path.join(lockfileDir, (pkgSnapshot.resolution as DirectoryResolution).directory)
|
||||
const manifest = await safeReadPackageJsonFromDir(localDepDir)
|
||||
if (!manifest) return false
|
||||
for (const depField of DEPENDENCIES_OR_PEER_FIELDS) {
|
||||
if (depField === 'devDependencies') continue
|
||||
const manifestDeps = manifest[depField] ?? {}
|
||||
const lockfileDeps = pkgSnapshot[depField] ?? {}
|
||||
|
||||
// Lock file has more dependencies than the current manifest, e.g. some dependencies are removed.
|
||||
if (Object.keys(lockfileDeps).some(depName => !manifestDeps[depName])) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const depName of Object.keys(manifestDeps)) {
|
||||
// If a dependency does not exist in the lock file, e.g. a new dependency is added to the current manifest.
|
||||
// We need to do full resolution again.
|
||||
if (!lockfileDeps[depName]) {
|
||||
return false
|
||||
}
|
||||
const currentSpec = manifestDeps[depName]
|
||||
// We do not care about the link dependencies of local dependency.
|
||||
if (currentSpec.startsWith('file:') || currentSpec.startsWith('link:') || currentSpec.startsWith('workspace:')) continue
|
||||
if (semver.satisfies(lockfileDeps[depName], getVersionRange(currentSpec), { loose: true })) {
|
||||
continue
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function getVersionRange (spec: string): string {
|
||||
if (spec.startsWith('workspace:')) return spec.slice(10)
|
||||
if (spec.startsWith('npm:')) {
|
||||
spec = spec.slice(4)
|
||||
const index = spec.indexOf('@', 1)
|
||||
if (index === -1) return '*'
|
||||
return spec.slice(index + 1) || '*'
|
||||
}
|
||||
return spec
|
||||
}
|
||||
@@ -3,6 +3,8 @@ export const LOCKFILE_MAJOR_VERSION = '9'
|
||||
export const LOCKFILE_VERSION = `${LOCKFILE_MAJOR_VERSION}.0`
|
||||
export const LOCKFILE_VERSION_V6 = '6.0'
|
||||
|
||||
export const MANIFEST_BASE_NAMES = ['package.json', 'package.json5', 'package.yaml'] as const
|
||||
|
||||
export const ENGINE_NAME = `${process.platform};${process.arch};node${process.version.split('.')[0].substring(1)}`
|
||||
export const LAYOUT_VERSION = 5
|
||||
export const STORE_VERSION = 'v10'
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
"@pnpm/types": "workspace:*",
|
||||
"@pnpm/workspace.find-packages": "workspace:*",
|
||||
"@pnpm/workspace.pkgs-graph": "workspace:*",
|
||||
"@pnpm/workspace.state": "workspace:*",
|
||||
"@pnpm/write-project-manifest": "workspace:*",
|
||||
"@yarnpkg/core": "catalog:",
|
||||
"@yarnpkg/lockfile": "catalog:",
|
||||
|
||||
@@ -22,11 +22,20 @@ import {
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { sequenceGraph } from '@pnpm/sort-packages'
|
||||
import { createPkgGraph } from '@pnpm/workspace.pkgs-graph'
|
||||
import { updateWorkspaceState } from '@pnpm/workspace.state'
|
||||
import isSubdir from 'is-subdir'
|
||||
import { getPinnedVersion } from './getPinnedVersion'
|
||||
import { getSaveType } from './getSaveType'
|
||||
import { getNodeExecPath } from './nodeExecPath'
|
||||
import { recursive, createMatcher, matchDependencies, makeIgnorePatterns, type UpdateDepsMatcher } from './recursive'
|
||||
import {
|
||||
type CommandFullName,
|
||||
type RecursiveOptions,
|
||||
type UpdateDepsMatcher,
|
||||
createMatcher,
|
||||
matchDependencies,
|
||||
makeIgnorePatterns,
|
||||
recursive,
|
||||
} from './recursive'
|
||||
import { createWorkspaceSpecs, updateToWorkspacePackagesFromManifest } from './updateWorkspaceDependencies'
|
||||
|
||||
const OVERWRITE_UPDATE_OPTIONS = {
|
||||
@@ -40,6 +49,7 @@ export type InstallDepsOptions = Pick<Config,
|
||||
| 'autoInstallPeers'
|
||||
| 'bail'
|
||||
| 'bin'
|
||||
| 'catalogs'
|
||||
| 'cliOptions'
|
||||
| 'dedupePeerDependents'
|
||||
| 'depth'
|
||||
@@ -191,7 +201,7 @@ when running add/update with the --workspace option')
|
||||
}
|
||||
}
|
||||
}
|
||||
await recursive(allProjects,
|
||||
await recursiveInstallThenUpdateWorkspaceState(allProjects,
|
||||
params,
|
||||
{
|
||||
...opts,
|
||||
@@ -316,7 +326,7 @@ when running add/update with the --workspace option')
|
||||
], {
|
||||
workspaceDir: opts.workspaceDir,
|
||||
})
|
||||
await recursive(allProjects, [], {
|
||||
await recursiveInstallThenUpdateWorkspaceState(allProjects, [], {
|
||||
...opts,
|
||||
...OVERWRITE_UPDATE_OPTIONS,
|
||||
allProjectsGraph: opts.allProjectsGraph!,
|
||||
@@ -349,3 +359,19 @@ function selectProjectByDir (projects: Project[], searchedDir: string): Projects
|
||||
if (project == null) return undefined
|
||||
return { [searchedDir]: { dependencies: [], package: project } }
|
||||
}
|
||||
|
||||
async function recursiveInstallThenUpdateWorkspaceState (
|
||||
allProjects: Project[],
|
||||
params: string[],
|
||||
opts: RecursiveOptions & Pick<InstallDepsOptions, 'catalogs' | 'workspaceDir'>,
|
||||
cmdFullName: CommandFullName
|
||||
): Promise<boolean | string> {
|
||||
const recursiveResult = await recursive(allProjects, params, opts, cmdFullName)
|
||||
await updateWorkspaceState({
|
||||
allProjects,
|
||||
catalogs: opts.catalogs,
|
||||
lastValidatedTimestamp: Date.now(),
|
||||
workspaceDir: opts.workspaceDir,
|
||||
})
|
||||
return recursiveResult
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ import { getSaveType } from './getSaveType'
|
||||
import { getPinnedVersion } from './getPinnedVersion'
|
||||
import { type PreferredVersions } from '@pnpm/resolver-base'
|
||||
|
||||
type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
|
||||
export type RecursiveOptions = CreateStoreControllerOptions & Pick<Config,
|
||||
| 'bail'
|
||||
| 'dedupePeerDependents'
|
||||
| 'depth'
|
||||
@@ -98,11 +98,13 @@ Pick<Config,
|
||||
Pick<Config, 'workspaceDir'>
|
||||
>
|
||||
|
||||
export type CommandFullName = 'install' | 'add' | 'remove' | 'update' | 'import'
|
||||
|
||||
export async function recursive (
|
||||
allProjects: Project[],
|
||||
params: string[],
|
||||
opts: RecursiveOptions,
|
||||
cmdFullName: 'install' | 'add' | 'remove' | 'update' | 'import'
|
||||
cmdFullName: CommandFullName
|
||||
): Promise<boolean | string> {
|
||||
if (allProjects.length === 0) {
|
||||
// It might make sense to throw an exception in this case
|
||||
|
||||
@@ -108,6 +108,9 @@
|
||||
{
|
||||
"path": "../../workspace/sort-packages"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/state"
|
||||
},
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
|
||||
95
pnpm-lock.yaml
generated
95
pnpm-lock.yaml
generated
@@ -1784,6 +1784,70 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
deps/status:
|
||||
dependencies:
|
||||
'@pnpm/config':
|
||||
specifier: workspace:*
|
||||
version: link:../../config/config
|
||||
'@pnpm/constants':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/constants
|
||||
'@pnpm/crypto.object-hasher':
|
||||
specifier: workspace:*
|
||||
version: link:../../crypto/object-hasher
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/error
|
||||
'@pnpm/get-context':
|
||||
specifier: workspace:*
|
||||
version: link:../../pkg-manager/get-context
|
||||
'@pnpm/lockfile.fs':
|
||||
specifier: workspace:*
|
||||
version: link:../../lockfile/fs
|
||||
'@pnpm/lockfile.settings-checker':
|
||||
specifier: workspace:*
|
||||
version: link:../../lockfile/settings-checker
|
||||
'@pnpm/lockfile.verification':
|
||||
specifier: workspace:*
|
||||
version: link:../../lockfile/verification
|
||||
'@pnpm/logger':
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
'@pnpm/parse-overrides':
|
||||
specifier: workspace:*
|
||||
version: link:../../config/parse-overrides
|
||||
'@pnpm/resolver-base':
|
||||
specifier: workspace:*
|
||||
version: link:../../resolving/resolver-base
|
||||
'@pnpm/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
'@pnpm/workspace.find-packages':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/find-packages
|
||||
'@pnpm/workspace.read-manifest':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/read-manifest
|
||||
'@pnpm/workspace.state':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/state
|
||||
ramda:
|
||||
specifier: 'catalog:'
|
||||
version: '@pnpm/ramda@0.28.1'
|
||||
devDependencies:
|
||||
'@pnpm/deps.status':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
'@pnpm/prepare':
|
||||
specifier: workspace:*
|
||||
version: link:../../__utils__/prepare
|
||||
'@pnpm/write-project-manifest':
|
||||
specifier: workspace:*
|
||||
version: link:../../pkg-manifest/write-project-manifest
|
||||
'@types/ramda':
|
||||
specifier: 'catalog:'
|
||||
version: 0.29.12
|
||||
|
||||
env/node.fetcher:
|
||||
dependencies:
|
||||
'@pnpm/create-cafs-store':
|
||||
@@ -2287,6 +2351,9 @@ importers:
|
||||
'@pnpm/crypto.hash':
|
||||
specifier: workspace:*
|
||||
version: link:../../crypto/hash
|
||||
'@pnpm/deps.status':
|
||||
specifier: workspace:*
|
||||
version: link:../../deps/status
|
||||
'@pnpm/env.path':
|
||||
specifier: workspace:*
|
||||
version: link:../../env/path
|
||||
@@ -5064,6 +5131,9 @@ importers:
|
||||
'@pnpm/workspace.pkgs-graph':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/pkgs-graph
|
||||
'@pnpm/workspace.state':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/state
|
||||
'@pnpm/write-project-manifest':
|
||||
specifier: workspace:*
|
||||
version: link:../../pkg-manifest/write-project-manifest
|
||||
@@ -5778,6 +5848,9 @@ importers:
|
||||
'@pnpm/workspace.pkgs-graph':
|
||||
specifier: workspace:*
|
||||
version: link:../workspace/pkgs-graph
|
||||
'@pnpm/workspace.state':
|
||||
specifier: workspace:*
|
||||
version: link:../workspace/state
|
||||
'@pnpm/write-project-manifest':
|
||||
specifier: workspace:*
|
||||
version: link:../pkg-manifest/write-project-manifest
|
||||
@@ -7800,6 +7873,28 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
workspace/state:
|
||||
dependencies:
|
||||
'@pnpm/catalogs.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../catalogs/types
|
||||
'@pnpm/logger':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/logger
|
||||
'@pnpm/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
ramda:
|
||||
specifier: 'catalog:'
|
||||
version: '@pnpm/ramda@0.28.1'
|
||||
devDependencies:
|
||||
'@pnpm/prepare':
|
||||
specifier: workspace:*
|
||||
version: link:../../__utils__/prepare
|
||||
'@pnpm/workspace.state':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
|
||||
packages:
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"@pnpm/worker": "workspace:*",
|
||||
"@pnpm/workspace.find-packages": "workspace:*",
|
||||
"@pnpm/workspace.pkgs-graph": "workspace:*",
|
||||
"@pnpm/workspace.state": "workspace:*",
|
||||
"@pnpm/write-project-manifest": "workspace:*",
|
||||
"@types/cross-spawn": "catalog:",
|
||||
"@types/is-windows": "catalog:",
|
||||
|
||||
@@ -4,9 +4,9 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
|
||||
import isWindows from 'is-windows'
|
||||
import crossSpawn from 'cross-spawn'
|
||||
|
||||
const binDir = path.join(__dirname, '../..', isWindows() ? 'dist' : 'bin')
|
||||
const pnpmBinLocation = path.join(binDir, 'pnpm.cjs')
|
||||
const pnpxBinLocation = path.join(__dirname, '../../bin/pnpx.cjs')
|
||||
export const binDir = path.join(__dirname, '../..', isWindows() ? 'dist' : 'bin')
|
||||
export const pnpmBinLocation = path.join(binDir, 'pnpm.cjs')
|
||||
export const pnpxBinLocation = path.join(__dirname, '../../bin/pnpx.cjs')
|
||||
|
||||
// The default timeout for tests is 4 minutes. Set a timeout for execPnpm calls
|
||||
// for 3 minutes to make it more clear what specific part of a test is timing
|
||||
@@ -93,12 +93,15 @@ export interface ChildProcess {
|
||||
export function execPnpmSync (
|
||||
args: string[],
|
||||
opts?: {
|
||||
env: Record<string, string>
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
expectSuccess?: boolean // similar to expect(status).toBe(0), but also prints error messages, which makes it easier to debug failed tests
|
||||
stdio?: StdioOptions
|
||||
timeout?: number
|
||||
}
|
||||
): ChildProcess {
|
||||
const execResult = crossSpawn.sync(process.execPath, [pnpmBinLocation, ...args], {
|
||||
cwd: opts?.cwd,
|
||||
env: {
|
||||
...createEnv(),
|
||||
...opts?.env,
|
||||
@@ -108,6 +111,14 @@ export function execPnpmSync (
|
||||
})
|
||||
if (execResult.error) throw execResult.error
|
||||
if (execResult.signal) throw new Error(`Process terminated with signal ${execResult.signal}`)
|
||||
if (execResult.status !== 0 && opts?.expectSuccess) {
|
||||
const stdout = execResult.stdout?.toString().split('\n').map(line => `\t${line}`).join('\n')
|
||||
const stderr = execResult.stderr?.toString().split('\n').map(line => `\t${line}`).join('\n')
|
||||
let message = `Process exits with code ${execResult.status}`
|
||||
if (stdout.trim()) message += `\nSTDOUT:\n${stdout}`
|
||||
if (stderr.trim()) message += `\nSTDOUT:\n${stderr}`
|
||||
throw new Error(message)
|
||||
}
|
||||
return execResult as ChildProcess
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { add as addDistTag } from './distTags'
|
||||
import {
|
||||
binDir,
|
||||
execPnpm,
|
||||
execPnpmSync,
|
||||
execPnpx,
|
||||
execPnpxSync,
|
||||
pnpmBinLocation,
|
||||
pnpxBinLocation,
|
||||
spawnPnpm,
|
||||
spawnPnpx,
|
||||
} from './execPnpm'
|
||||
@@ -14,10 +17,13 @@ export { retryLoadJsonFile } from './retryLoadJsonFile'
|
||||
export {
|
||||
pathToLocalPkg,
|
||||
testDefaults,
|
||||
binDir,
|
||||
execPnpm,
|
||||
execPnpmSync,
|
||||
execPnpx,
|
||||
execPnpxSync,
|
||||
pnpmBinLocation,
|
||||
pnpxBinLocation,
|
||||
spawnPnpm,
|
||||
spawnPnpx,
|
||||
addDistTag,
|
||||
|
||||
344
pnpm/test/verifyDepsBeforeRun/exec.ts
Normal file
344
pnpm/test/verifyDepsBeforeRun/exec.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { prepare, preparePackages } from '@pnpm/prepare'
|
||||
import { type ProjectManifest } from '@pnpm/types'
|
||||
import { loadWorkspaceState } from '@pnpm/workspace.state'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
import { execPnpm, execPnpmSync } from '../utils'
|
||||
|
||||
const CONFIG = ['--config.verify-deps-before-run=true'] as const
|
||||
|
||||
test('single package workspace', async () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: 'root',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '100.0.0',
|
||||
},
|
||||
}
|
||||
|
||||
const project = prepare(manifest)
|
||||
|
||||
const EXEC = ['exec', 'echo', 'hello from exec'] as const
|
||||
|
||||
// attempting to execute a command without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// installing dependencies on a single package workspace should not create a packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toBeUndefined()
|
||||
}
|
||||
|
||||
// should be able to execute a command after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from exec')
|
||||
}
|
||||
|
||||
project.writePackageJson(manifest)
|
||||
|
||||
// should be able to execute a command after the mtime of the manifest change but the content doesn't
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from exec')
|
||||
}
|
||||
|
||||
project.writePackageJson({
|
||||
...manifest,
|
||||
dependencies: {
|
||||
...manifest.dependencies,
|
||||
'@pnpm.e2e/foo': '100.1.0',
|
||||
},
|
||||
})
|
||||
|
||||
// attempting to execute a command with outdated dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// should be able to execute a command after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from exec')
|
||||
}
|
||||
|
||||
project.writePackageJson({
|
||||
...manifest,
|
||||
dependencies: {}, // delete all dependencies
|
||||
})
|
||||
|
||||
// attempting to execute a command with redundant dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// should be able to execute a command without dependencies
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from exec')
|
||||
}
|
||||
|
||||
// should set env.pnpm_run_skip_deps_check for the script
|
||||
await execPnpm([...CONFIG, 'exec', 'node', '--eval', 'assert.strictEqual(process.env.pnpm_run_skip_deps_check, "true")'])
|
||||
})
|
||||
|
||||
test('multi-project workspace', async () => {
|
||||
const manifests: Record<string, ProjectManifest> = {
|
||||
root: {
|
||||
name: 'root',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
name: 'foo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const projects = preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: manifests.root,
|
||||
},
|
||||
manifests.foo,
|
||||
manifests.bar,
|
||||
])
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
const EXEC = ['exec', 'node', '--print', '"hello from exec: " + process.cwd()'] as const
|
||||
|
||||
// attempting to execute a command in root without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
// attempting to execute a command in a workspace package without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC], {
|
||||
cwd: projects.foo.dir(),
|
||||
})
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
// attempting to execute a command recursively without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--recursive', ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
// attempting to execute a command with filter without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--filter=foo', ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// pnpm install should create a packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toStrictEqual({
|
||||
catalogs: {},
|
||||
lastValidatedTimestamp: expect.any(Number),
|
||||
projectRootDirs: [
|
||||
path.resolve('.'),
|
||||
path.resolve('foo'),
|
||||
path.resolve('bar'),
|
||||
].sort(),
|
||||
})
|
||||
}
|
||||
|
||||
// should be able to execute a command in root after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${process.cwd()}`)
|
||||
}
|
||||
// should be able to execute a command in a workspace package after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], {
|
||||
cwd: projects.foo.dir(),
|
||||
expectSuccess: true,
|
||||
})
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('foo')}`)
|
||||
}
|
||||
// should be able to execute a command recursively after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--recursive', ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('foo')}`)
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('bar')}`)
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('.')}`)
|
||||
}
|
||||
// should be able to execute a command with filter after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--filter=foo', ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('foo')}`)
|
||||
}
|
||||
|
||||
projects.foo.writePackageJson(manifests.foo)
|
||||
|
||||
// if the mtime of one manifest file changes but its content doesn't, pnpm run should update the packages list then run the script normally
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${process.cwd()}`)
|
||||
}
|
||||
// should skip check after pnpm has updated the packages list
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${process.cwd()}`)
|
||||
}
|
||||
|
||||
projects.foo.writePackageJson({
|
||||
...manifests.foo,
|
||||
dependencies: {
|
||||
...manifests.foo.dependencies,
|
||||
'@pnpm.e2e/foo': '=100.1.0',
|
||||
},
|
||||
} as ProjectManifest)
|
||||
|
||||
// attempting to execute a command in root without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
}
|
||||
// attempting to execute a command in any workspace package without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC], {
|
||||
cwd: projects.foo.dir(),
|
||||
})
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
}
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC], {
|
||||
cwd: projects.bar.dir(),
|
||||
})
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
}
|
||||
// attempting to execute a command recursively without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--recursive', ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
}
|
||||
// attempting to execute a command with filter without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--filter=foo', ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// should be able to execute a command in root after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${process.cwd()}`)
|
||||
}
|
||||
// should be able to execute a command in any workspace package after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], {
|
||||
cwd: projects.foo.dir(),
|
||||
expectSuccess: true,
|
||||
})
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('foo')}`)
|
||||
}
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], {
|
||||
cwd: projects.bar.dir(),
|
||||
expectSuccess: true,
|
||||
})
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('bar')}`)
|
||||
}
|
||||
// should be able to execute a command recursively after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--recursive', ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('foo')}`)
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('bar')}`)
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('.')}`)
|
||||
}
|
||||
// should be able to execute a command with filter after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--filter=foo', ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${path.resolve('foo')}`)
|
||||
}
|
||||
|
||||
manifests.baz = {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
}
|
||||
fs.mkdirSync(path.resolve('baz'), { recursive: true })
|
||||
fs.writeFileSync(path.resolve('baz/package.json'), JSON.stringify(manifests.baz, undefined, 2) + '\n')
|
||||
|
||||
// attempting to execute a command without updating projects list should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, ...EXEC])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_WORKSPACE_STRUCTURE_CHANGED')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// pnpm install should update the packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toStrictEqual({
|
||||
catalogs: {},
|
||||
lastValidatedTimestamp: expect.any(Number),
|
||||
projectRootDirs: [
|
||||
path.resolve('.'),
|
||||
path.resolve('foo'),
|
||||
path.resolve('bar'),
|
||||
path.resolve('baz'),
|
||||
].sort(),
|
||||
})
|
||||
}
|
||||
|
||||
// should be able to execute a command after projects list have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, ...EXEC], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain(`hello from exec: ${process.cwd()}`)
|
||||
}
|
||||
|
||||
// should set env.pnpm_run_skip_deps_check for all the scripts
|
||||
await execPnpm([...CONFIG, 'exec', 'node', '--eval', 'assert.strictEqual(process.env.pnpm_run_skip_deps_check, "true")'])
|
||||
})
|
||||
959
pnpm/test/verifyDepsBeforeRun/multiProjectWorkspace.ts
Normal file
959
pnpm/test/verifyDepsBeforeRun/multiProjectWorkspace.ts
Normal file
@@ -0,0 +1,959 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { preparePackages } from '@pnpm/prepare'
|
||||
import { type ProjectManifest } from '@pnpm/types'
|
||||
import { loadWorkspaceState } from '@pnpm/workspace.state'
|
||||
import { sync as writeYamlFile } from 'write-yaml-file'
|
||||
import { execPnpm, execPnpmSync, pnpmBinLocation } from '../utils'
|
||||
|
||||
const CONFIG = ['--config.verify-deps-before-run=true'] as const
|
||||
|
||||
test('single dependency', async () => {
|
||||
const checkEnv = 'node --eval "assert.strictEqual(process.env.pnpm_run_skip_deps_check, \'true\')"'
|
||||
|
||||
const manifests: Record<string, ProjectManifest> = {
|
||||
root: {
|
||||
name: 'root',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from root',
|
||||
checkEnv,
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
name: 'foo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from foo',
|
||||
checkEnv,
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from bar',
|
||||
checkEnv,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const projects = preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: manifests.root,
|
||||
},
|
||||
manifests.foo,
|
||||
manifests.bar,
|
||||
])
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
// attempting to execute a script in root without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
// attempting to execute a script in a workspace package without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'], {
|
||||
cwd: projects.foo.dir(),
|
||||
})
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
// attempting to execute a script recursively without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
// attempting to execute a script with filter without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// pnpm install should create a packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toStrictEqual({
|
||||
catalogs: {},
|
||||
lastValidatedTimestamp: expect.any(Number),
|
||||
projectRootDirs: [
|
||||
path.resolve('.'),
|
||||
path.resolve('foo'),
|
||||
path.resolve('bar'),
|
||||
].sort(),
|
||||
})
|
||||
}
|
||||
|
||||
// should be able to execute a script in root after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
expect(stdout.toString()).toContain('No manifest files were modified since the last validation. Exiting check.')
|
||||
expect(stdout.toString()).not.toContain('Some manifest files were modified since the last validation. Continuing check.')
|
||||
}
|
||||
// should be able to execute a script in a workspace package after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], {
|
||||
cwd: projects.foo.dir(),
|
||||
expectSuccess: true,
|
||||
})
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
// should be able to execute a script recursively after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
expect(stdout.toString()).toContain('hello from bar')
|
||||
}
|
||||
// should be able to execute a script with filter after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
|
||||
projects.foo.writePackageJson(manifests.foo)
|
||||
|
||||
// if the mtime of one manifest file changes but its content doesn't, pnpm run should update the packages list then run the script normally
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
expect(stdout.toString()).not.toContain('No manifest files were modified since the last validation. Exiting check.')
|
||||
expect(stdout.toString()).toContain('Some manifest files were modified since the last validation. Continuing check.')
|
||||
expect(stdout.toString()).toContain('updating workspace state')
|
||||
}
|
||||
// should skip check after pnpm has updated the packages list
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
expect(stdout.toString()).toContain('No manifest files were modified since the last validation. Exiting check.')
|
||||
expect(stdout.toString()).not.toContain('Some manifest files were modified since the last validation. Continuing check.')
|
||||
expect(stdout.toString()).not.toContain('updating workspace state')
|
||||
}
|
||||
|
||||
projects.foo.writePackageJson({
|
||||
...manifests.foo,
|
||||
dependencies: {
|
||||
...manifests.foo.dependencies,
|
||||
'@pnpm.e2e/foo': '=100.1.0',
|
||||
},
|
||||
} as ProjectManifest)
|
||||
|
||||
// attempting to execute a script in root without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
expect(stdout.toString()).not.toContain('No manifest files were modified since the last validation. Exiting check.')
|
||||
expect(stdout.toString()).toContain('Some manifest files were modified since the last validation. Continuing check.')
|
||||
}
|
||||
// attempting to execute a script in any workspace package without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'], {
|
||||
cwd: projects.foo.dir(),
|
||||
})
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
}
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'], {
|
||||
cwd: projects.bar.dir(),
|
||||
})
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
}
|
||||
// attempting to execute a script recursively without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
}
|
||||
// attempting to execute a script with filter without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain('project of id foo')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// should be able to execute a script in root after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
expect(stdout.toString()).toContain('No manifest files were modified since the last validation. Exiting check.')
|
||||
expect(stdout.toString()).not.toContain('Some manifest files were modified since the last validation. Continuing check.')
|
||||
}
|
||||
// should be able to execute a script in any workspace package after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], {
|
||||
cwd: projects.foo.dir(),
|
||||
expectSuccess: true,
|
||||
})
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], {
|
||||
cwd: projects.bar.dir(),
|
||||
expectSuccess: true,
|
||||
})
|
||||
expect(stdout.toString()).toContain('hello from bar')
|
||||
}
|
||||
// should be able to execute a script recursively after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
expect(stdout.toString()).toContain('hello from bar')
|
||||
}
|
||||
// should be able to execute a script with filter after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
|
||||
manifests.baz = {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from baz',
|
||||
checkEnv,
|
||||
},
|
||||
}
|
||||
fs.mkdirSync(path.resolve('baz'), { recursive: true })
|
||||
fs.writeFileSync(path.resolve('baz/package.json'), JSON.stringify(manifests.baz, undefined, 2) + '\n')
|
||||
|
||||
// attempting to execute a script without updating projects list should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_WORKSPACE_STRUCTURE_CHANGED')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// pnpm install should update the packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toStrictEqual({
|
||||
catalogs: {},
|
||||
lastValidatedTimestamp: expect.any(Number),
|
||||
projectRootDirs: [
|
||||
path.resolve('.'),
|
||||
path.resolve('foo'),
|
||||
path.resolve('bar'),
|
||||
path.resolve('baz'),
|
||||
].sort(),
|
||||
})
|
||||
}
|
||||
|
||||
// should be able to execute a script after projects list have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
}
|
||||
|
||||
// should set env.pnpm_run_skip_deps_check for all the scripts
|
||||
await execPnpm([...CONFIG, '--recursive', 'run', 'checkEnv'])
|
||||
})
|
||||
|
||||
test('multiple lockfiles', async () => {
|
||||
const manifests: Record<string, ProjectManifest> = {
|
||||
root: {
|
||||
name: 'root',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from root',
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
name: 'foo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from foo',
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from bar',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const projects = preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: manifests.root,
|
||||
},
|
||||
manifests.foo,
|
||||
manifests.bar,
|
||||
])
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
const config = [
|
||||
...CONFIG,
|
||||
'--config.shared-workspace-lockfile=false',
|
||||
]
|
||||
|
||||
// attempting to execute a script in root without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
// attempting to execute a script in a workspace package without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, 'start'], {
|
||||
cwd: projects.foo.dir(),
|
||||
})
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
// attempting to execute a script recursively without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, '--recursive', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
// attempting to execute a script with filter without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, '--filter=foo', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
|
||||
await execPnpm([...config, 'install'])
|
||||
|
||||
// pnpm install should create a packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toStrictEqual({
|
||||
catalogs: {},
|
||||
lastValidatedTimestamp: expect.any(Number),
|
||||
projectRootDirs: [
|
||||
path.resolve('.'),
|
||||
path.resolve('foo'),
|
||||
path.resolve('bar'),
|
||||
].sort(),
|
||||
})
|
||||
}
|
||||
|
||||
// should be able to execute a script in root after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, '--reporter=ndjson', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
expect(stdout.toString()).toContain('No manifest files were modified since the last validation. Exiting check.')
|
||||
expect(stdout.toString()).not.toContain('Some manifest files were modified since the last validation. Continuing check.')
|
||||
}
|
||||
// should be able to execute a script in a workspace package after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, 'start'], {
|
||||
cwd: projects.foo.dir(),
|
||||
expectSuccess: true,
|
||||
})
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
// should be able to execute a script recursively after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, '--recursive', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
expect(stdout.toString()).toContain('hello from bar')
|
||||
}
|
||||
// should be able to execute a script with filter after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, '--filter=foo', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
|
||||
projects.foo.writePackageJson(manifests.foo)
|
||||
|
||||
// if the mtime of one manifest file changes but its content doesn't, pnpm run should update the packages list then run the script normally
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, '--reporter=ndjson', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
expect(stdout.toString()).not.toContain('No manifest files were modified since the last validation. Exiting check.')
|
||||
expect(stdout.toString()).toContain('Some manifest files were modified since the last validation. Continuing check.')
|
||||
expect(stdout.toString()).toContain('updating workspace state')
|
||||
}
|
||||
// should skip check after pnpm has updated the packages list
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, '--reporter=ndjson', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
expect(stdout.toString()).toContain('No manifest files were modified since the last validation. Exiting check.')
|
||||
expect(stdout.toString()).not.toContain('Some manifest files were modified since the last validation. Continuing check.')
|
||||
expect(stdout.toString()).not.toContain('updating workspace state')
|
||||
}
|
||||
|
||||
projects.foo.writePackageJson({
|
||||
...manifests.foo,
|
||||
dependencies: {
|
||||
...manifests.foo.dependencies,
|
||||
'@pnpm.e2e/foo': '=100.1.0',
|
||||
},
|
||||
} as ProjectManifest)
|
||||
|
||||
// attempting to execute a script in root without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
|
||||
}
|
||||
// attempting to execute a script in any workspace package without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, 'start'], {
|
||||
cwd: projects.foo.dir(),
|
||||
})
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
|
||||
}
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, 'start'], {
|
||||
cwd: projects.bar.dir(),
|
||||
})
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
|
||||
}
|
||||
// attempting to execute a script recursively without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, '--recursive', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
|
||||
}
|
||||
// attempting to execute a script with filter without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, '--filter=foo', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).toContain(`The lockfile in ${path.resolve('foo')} does not satisfy project of id .`)
|
||||
}
|
||||
|
||||
await execPnpm([...config, 'install'])
|
||||
|
||||
// should be able to execute a script in root after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
}
|
||||
// should be able to execute a script in any workspace package after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, 'start'], {
|
||||
cwd: projects.foo.dir(),
|
||||
expectSuccess: true,
|
||||
})
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, 'start'], {
|
||||
cwd: projects.bar.dir(),
|
||||
expectSuccess: true,
|
||||
})
|
||||
expect(stdout.toString()).toContain('hello from bar')
|
||||
}
|
||||
// should be able to execute a script recursively after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, '--recursive', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
expect(stdout.toString()).toContain('hello from bar')
|
||||
}
|
||||
// should be able to execute a script with filter after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, '--filter=foo', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
|
||||
manifests.baz = {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from baz',
|
||||
},
|
||||
}
|
||||
fs.mkdirSync(path.resolve('baz'), { recursive: true })
|
||||
fs.writeFileSync(path.resolve('baz/package.json'), JSON.stringify(manifests.baz, undefined, 2) + '\n')
|
||||
|
||||
// attempting to execute a script without updating projects list should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...config, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_WORKSPACE_STRUCTURE_CHANGED')
|
||||
}
|
||||
|
||||
await execPnpm([...config, 'install'])
|
||||
|
||||
// pnpm install should update the packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toStrictEqual({
|
||||
catalogs: {},
|
||||
lastValidatedTimestamp: expect.any(Number),
|
||||
projectRootDirs: [
|
||||
path.resolve('.'),
|
||||
path.resolve('foo'),
|
||||
path.resolve('bar'),
|
||||
path.resolve('baz'),
|
||||
].sort(),
|
||||
})
|
||||
}
|
||||
|
||||
// should be able to execute a script after projects list have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...config, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
}
|
||||
})
|
||||
|
||||
test('filtered install', async () => {
|
||||
const manifests: Record<string, ProjectManifest> = {
|
||||
root: {
|
||||
name: 'root',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from root',
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
name: 'foo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from foo',
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from bar',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const projects = preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: manifests.root,
|
||||
},
|
||||
manifests.foo,
|
||||
manifests.bar,
|
||||
])
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
// attempting to execute a script without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, '--filter=foo', 'install'])
|
||||
|
||||
// should be able to execute a script after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
|
||||
manifests.foo.dependencies!['@pnpm.e2e/foo'] = '=100.1.0'
|
||||
projects.foo.writePackageJson(manifests.foo)
|
||||
|
||||
// attempt to execute a script without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, '--filter=foo', 'install'])
|
||||
|
||||
// should be able to execute a script after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--filter=foo', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from foo')
|
||||
}
|
||||
})
|
||||
|
||||
test('no dependencies', async () => {
|
||||
const manifests: Record<string, ProjectManifest> = {
|
||||
root: {
|
||||
name: 'root',
|
||||
private: true,
|
||||
scripts: {
|
||||
start: 'echo hello from root',
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
name: 'foo',
|
||||
private: true,
|
||||
scripts: {
|
||||
start: 'echo hello from foo',
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
scripts: {
|
||||
start: 'echo hello from bar',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: manifests.root,
|
||||
},
|
||||
manifests.foo,
|
||||
manifests.bar,
|
||||
])
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
// attempting to execute a script without `pnpm install` should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// pnpm install should create a packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toStrictEqual({
|
||||
catalogs: {},
|
||||
lastValidatedTimestamp: expect.any(Number),
|
||||
projectRootDirs: [
|
||||
path.resolve('.'),
|
||||
path.resolve('foo'),
|
||||
path.resolve('bar'),
|
||||
].sort(),
|
||||
})
|
||||
}
|
||||
|
||||
// should be able to execute a script after `pnpm install`
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
}
|
||||
})
|
||||
|
||||
test('nested `pnpm run` should not check for mutated manifest', async () => {
|
||||
const manifests: Record<string, ProjectManifest> = {
|
||||
foo: {
|
||||
name: 'foo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
nestedScript: 'echo hello from nested script of foo',
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
nestedScript: 'echo hello from nested script of bar',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const projects = preparePackages([
|
||||
manifests.foo,
|
||||
manifests.bar,
|
||||
])
|
||||
|
||||
for (const name in projects) {
|
||||
const scriptPath = path.join(projects[name].dir(), 'mutate-manifest.js')
|
||||
fs.writeFileSync(scriptPath, `
|
||||
const fs = require('fs')
|
||||
const manifest = require('./package.json')
|
||||
manifest.dependencies['@pnpm.e2e/foo'] = '100.1.0'
|
||||
const jsonText = JSON.stringify(manifest, undefined, 2)
|
||||
fs.writeFileSync(require.resolve('./package.json'), jsonText)
|
||||
console.log('manifest mutated: ${name}')
|
||||
`)
|
||||
}
|
||||
|
||||
// add to every manifest file a script named `start` which would inherit `config` and invoke `nestedScript`
|
||||
for (const name in projects) {
|
||||
manifests[name].scripts!.start =
|
||||
`node mutate-manifest.js && node ${pnpmBinLocation} ${CONFIG.join(' ')} run nestedScript`
|
||||
projects[name].writePackageJson(manifests[name])
|
||||
}
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
// attempting to execute a script without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// mutating the manifest should not cause nested `pnpm run nestedScript` to fail
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('manifest mutated: foo')
|
||||
expect(stdout.toString()).toContain('hello from nested script of foo')
|
||||
expect(stdout.toString()).toContain('manifest mutated: bar')
|
||||
expect(stdout.toString()).toContain('hello from nested script of bar')
|
||||
}
|
||||
|
||||
// non nested script (`start`) should still fail (after `nestedScript` modified the manifests)
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// it shouldn't fail after the dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('manifest mutated: foo')
|
||||
expect(stdout.toString()).toContain('hello from nested script of foo')
|
||||
expect(stdout.toString()).toContain('manifest mutated: bar')
|
||||
expect(stdout.toString()).toContain('hello from nested script of bar')
|
||||
}
|
||||
|
||||
// it shouldn't fail after the manifests have been rewritten with the same content (by `nestedScript`)
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--recursive', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('manifest mutated: foo')
|
||||
expect(stdout.toString()).toContain('hello from nested script of foo')
|
||||
expect(stdout.toString()).toContain('manifest mutated: bar')
|
||||
expect(stdout.toString()).toContain('hello from nested script of bar')
|
||||
}
|
||||
})
|
||||
|
||||
test('should check for outdated catalogs', async () => {
|
||||
const manifests: Record<string, ProjectManifest> = {
|
||||
root: {
|
||||
name: 'root',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from root',
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
name: 'foo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from foo',
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': 'catalog:',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from bar',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: manifests.root,
|
||||
},
|
||||
manifests.foo,
|
||||
manifests.bar,
|
||||
])
|
||||
|
||||
const workspaceManifest = {
|
||||
catalog: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
} as Record<string, string>,
|
||||
packages: ['**', '!store/**'],
|
||||
}
|
||||
writeYamlFile('pnpm-workspace.yaml', workspaceManifest)
|
||||
|
||||
// attempting to execute a script without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_CACHE')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// pnpm install should create a packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toStrictEqual({
|
||||
catalogs: {
|
||||
default: workspaceManifest.catalog,
|
||||
},
|
||||
lastValidatedTimestamp: expect.any(Number),
|
||||
projectRootDirs: [
|
||||
path.resolve('.'),
|
||||
path.resolve('foo'),
|
||||
path.resolve('bar'),
|
||||
].sort(),
|
||||
})
|
||||
}
|
||||
|
||||
// should be able to execute a script after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
}
|
||||
|
||||
workspaceManifest.catalog.foo = '=100.1.0'
|
||||
writeYamlFile('pnpm-workspace.yaml', workspaceManifest)
|
||||
|
||||
// attempting to execute a script without updating dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_OUTDATED')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// should be able to execute a script after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
}
|
||||
})
|
||||
|
||||
test('failed to install dependencies', async () => {
|
||||
const manifests: Record<string, ProjectManifest> = {
|
||||
root: {
|
||||
name: 'root',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from root',
|
||||
},
|
||||
},
|
||||
foo: {
|
||||
name: 'foo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from foo',
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
name: 'bar',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '=100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from bar',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const projects = preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: manifests.root,
|
||||
},
|
||||
manifests.foo,
|
||||
manifests.bar,
|
||||
])
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// should be able to execute a script after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from root')
|
||||
}
|
||||
|
||||
// modify a manifest file to require an impossible version
|
||||
manifests.foo.dependencies!['@pnpm.e2e/foo'] = '=9999.9999.9999' // this version does not exist
|
||||
projects.foo.writePackageJson(manifests.foo)
|
||||
|
||||
// should fail to install dependencies
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'install'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_NO_MATCHING_VERSION')
|
||||
}
|
||||
|
||||
// attempting to execute a script without successfully updating the dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
}
|
||||
})
|
||||
257
pnpm/test/verifyDepsBeforeRun/singleProjectWorkspace.ts
Normal file
257
pnpm/test/verifyDepsBeforeRun/singleProjectWorkspace.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { prepare } from '@pnpm/prepare'
|
||||
import { type ProjectManifest } from '@pnpm/types'
|
||||
import { loadWorkspaceState } from '@pnpm/workspace.state'
|
||||
import { execPnpm, execPnpmSync, pnpmBinLocation } from '../utils'
|
||||
|
||||
const CONFIG = ['--config.verify-deps-before-run=true'] as const
|
||||
|
||||
test('single dependency', async () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: 'root',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from script',
|
||||
checkEnv: 'node --eval "assert.strictEqual(process.env.pnpm_run_skip_deps_check, \'true\')"',
|
||||
},
|
||||
}
|
||||
|
||||
const project = prepare(manifest)
|
||||
|
||||
// attempting to execute a script without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// installing dependencies on a single package workspace should not create a packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toBeUndefined()
|
||||
}
|
||||
|
||||
// should be able to execute a script after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from script')
|
||||
}
|
||||
|
||||
project.writePackageJson(manifest)
|
||||
|
||||
// should be able to execute a script after the mtime of the manifest change but the content doesn't
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from script')
|
||||
expect(stdout.toString()).not.toContain('The manifest file is not newer than the lockfile. Exiting check.')
|
||||
expect(stdout.toString()).toContain('The manifest is newer than the lockfile. Continuing check.')
|
||||
}
|
||||
|
||||
project.writePackageJson({
|
||||
...manifest,
|
||||
dependencies: {
|
||||
...manifest.dependencies,
|
||||
'@pnpm.e2e/foo': '100.1.0',
|
||||
},
|
||||
})
|
||||
|
||||
// attempting to execute a script with outdated dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
expect(stdout.toString()).not.toContain('The manifest file is not newer than the lockfile. Exiting check.')
|
||||
expect(stdout.toString()).toContain('The manifest is newer than the lockfile. Continuing check.')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// should be able to execute a script after dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, '--reporter=ndjson', 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from script')
|
||||
expect(stdout.toString()).toContain('The manifest file is not newer than the lockfile. Exiting check.')
|
||||
expect(stdout.toString()).not.toContain('The manifest is newer than the lockfile. Continuing check.')
|
||||
}
|
||||
|
||||
project.writePackageJson({
|
||||
...manifest,
|
||||
dependencies: {}, // delete all dependencies
|
||||
})
|
||||
|
||||
// attempting to execute a script with redundant dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// should be able to execute a script without dependencies
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from script')
|
||||
}
|
||||
|
||||
// should set env.pnpm_run_skip_deps_check for the script
|
||||
await execPnpm([...CONFIG, 'run', 'checkEnv'])
|
||||
})
|
||||
|
||||
test('deleting node_modules after install', async () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: 'root',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '100.0.0',
|
||||
},
|
||||
scripts: {
|
||||
start: 'echo hello from script',
|
||||
},
|
||||
}
|
||||
|
||||
prepare(manifest)
|
||||
|
||||
// attempting to execute a script without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// installing dependencies on a single package workspace should not create a packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toBeUndefined()
|
||||
}
|
||||
|
||||
// should be able to execute a script after dependencies have been installed
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from script')
|
||||
}
|
||||
|
||||
fs.rmSync('node_modules', { recursive: true })
|
||||
|
||||
// attempting to execute a script after node_modules has been deleted should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_NO_DEPS')
|
||||
}
|
||||
})
|
||||
|
||||
test('no dependencies', async () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: 'root',
|
||||
private: true,
|
||||
scripts: {
|
||||
start: 'echo hello from script',
|
||||
},
|
||||
}
|
||||
|
||||
prepare(manifest)
|
||||
|
||||
// attempting to execute a script without the lockfile should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// installing dependencies on a single package workspace should not create a packages list cache
|
||||
{
|
||||
const workspaceState = loadWorkspaceState(process.cwd())
|
||||
expect(workspaceState).toBeUndefined()
|
||||
}
|
||||
|
||||
// should be able to execute a script after the lockfile has been created
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('hello from script')
|
||||
}
|
||||
})
|
||||
|
||||
test('nested `pnpm run` should not check for mutated manifest', async () => {
|
||||
const manifest: ProjectManifest = {
|
||||
name: 'root',
|
||||
private: true,
|
||||
scripts: {
|
||||
nestedScript: 'echo hello from the nested script',
|
||||
},
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '100.0.0',
|
||||
},
|
||||
}
|
||||
|
||||
const project = prepare(manifest)
|
||||
|
||||
fs.writeFileSync('mutate-manifest.js', `
|
||||
const fs = require('fs')
|
||||
const manifest = require('./package.json')
|
||||
manifest.dependencies['@pnpm.e2e/foo'] = '100.1.0'
|
||||
const jsonText = JSON.stringify(manifest, undefined, 2)
|
||||
fs.writeFileSync(require.resolve('./package.json'), jsonText)
|
||||
console.log('manifest mutated')
|
||||
`)
|
||||
|
||||
const cacheDir = path.resolve('cache')
|
||||
const config = [
|
||||
CONFIG,
|
||||
`--config.cache-dir=${cacheDir}`,
|
||||
]
|
||||
|
||||
// add a script named `start` which would inherit `config` and invoke `nestedScript`
|
||||
manifest.scripts!.start =
|
||||
`node mutate-manifest.js && node ${pnpmBinLocation} ${config.join(' ')} run nestedScript`
|
||||
project.writePackageJson(manifest)
|
||||
|
||||
// attempting to execute a script without installing dependencies should fail
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// mutating the manifest should not cause nested `pnpm run nestedScript` to fail
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('manifest mutated')
|
||||
expect(stdout.toString()).toContain('hello from the nested script')
|
||||
}
|
||||
|
||||
// non nested script (`start`) should still fail (after `nestedScript` modified the manifest)
|
||||
{
|
||||
const { status, stdout } = execPnpmSync([...CONFIG, 'start'])
|
||||
expect(status).not.toBe(0)
|
||||
expect(stdout.toString()).toContain('ERR_PNPM_RUN_CHECK_DEPS_UNSATISFIED_PKG_MANIFEST')
|
||||
}
|
||||
|
||||
await execPnpm([...CONFIG, 'install'])
|
||||
|
||||
// it shouldn't fail after the dependencies have been updated
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('manifest mutated')
|
||||
expect(stdout.toString()).toContain('hello from the nested script')
|
||||
}
|
||||
|
||||
// it shouldn't fail after the manifest has been rewritten with the same content (by `nestedScript`)
|
||||
{
|
||||
const { stdout } = execPnpmSync([...CONFIG, 'start'], { expectSuccess: true })
|
||||
expect(stdout.toString()).toContain('manifest mutated')
|
||||
expect(stdout.toString()).toContain('hello from the nested script')
|
||||
}
|
||||
})
|
||||
@@ -176,6 +176,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../workspace/pkgs-graph"
|
||||
},
|
||||
{
|
||||
"path": "../workspace/state"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
17
workspace/state/README.md
Normal file
17
workspace/state/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# @pnpm/workspace.state
|
||||
|
||||
> Track the list of actual paths of workspace packages in a cache.
|
||||
|
||||
<!--@shields('npm')-->
|
||||
[](https://www.npmjs.com/package/@pnpm/workspace.state)
|
||||
<!--/@-->
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @pnpm/workspace.state
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
49
workspace/state/package.json
Normal file
49
workspace/state/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@pnpm/workspace.state",
|
||||
"version": "0.0.0",
|
||||
"description": "Track the list of actual paths of workspace packages in a cache",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "pnpm run compile && pnpm run _test",
|
||||
"_test": "jest",
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"prepublishOnly": "pnpm run compile",
|
||||
"compile": "tsc --build && pnpm run lint --fix"
|
||||
},
|
||||
"repository": "https://github.com/pnpm/pnpm/blob/main/workspace/state",
|
||||
"keywords": [
|
||||
"pnpm10",
|
||||
"pnpm",
|
||||
"mtime"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/workspace/state#readme",
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"dependencies": {
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/logger": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"ramda": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/prepare": "workspace:*",
|
||||
"@pnpm/workspace.state": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
".": "./lib/index.js"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "@pnpm/jest-config"
|
||||
}
|
||||
}
|
||||
14
workspace/state/src/createWorkspaceState.ts
Normal file
14
workspace/state/src/createWorkspaceState.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type WorkspaceState, type ProjectsList } from './types'
|
||||
|
||||
export interface CreateWorkspaceStateOptions {
|
||||
allProjects: ProjectsList
|
||||
catalogs: Catalogs | undefined
|
||||
lastValidatedTimestamp: number
|
||||
}
|
||||
|
||||
export const createWorkspaceState = (opts: CreateWorkspaceStateOptions): WorkspaceState => ({
|
||||
catalogs: opts.catalogs,
|
||||
lastValidatedTimestamp: opts.lastValidatedTimestamp,
|
||||
projectRootDirs: opts.allProjects.map(project => project.rootDir).sort(),
|
||||
})
|
||||
4
workspace/state/src/filePath.ts
Normal file
4
workspace/state/src/filePath.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import path from 'path'
|
||||
|
||||
export const getFilePath = (workspaceDir: string): string =>
|
||||
path.join(workspaceDir, 'node_modules', '.pnpm-workspace-state.json')
|
||||
3
workspace/state/src/index.ts
Normal file
3
workspace/state/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { loadWorkspaceState } from './loadWorkspaceState'
|
||||
export { type UpdateWorkspaceStateOptions, updateWorkspaceState } from './updateWorkspaceState'
|
||||
export { type WorkspaceState, type ProjectsList } from './types'
|
||||
20
workspace/state/src/loadWorkspaceState.ts
Normal file
20
workspace/state/src/loadWorkspaceState.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import fs from 'fs'
|
||||
import util from 'util'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { getFilePath } from './filePath'
|
||||
import { type WorkspaceState } from './types'
|
||||
|
||||
export function loadWorkspaceState (workspaceDir: string): WorkspaceState | undefined {
|
||||
logger.debug({ msg: 'loading workspace state' })
|
||||
const cacheFile = getFilePath(workspaceDir)
|
||||
let cacheFileContent: string
|
||||
try {
|
||||
cacheFileContent = fs.readFileSync(cacheFile, 'utf-8')
|
||||
} catch (error) {
|
||||
if (util.types.isNativeError(error) && 'code' in error && error.code === 'ENOENT') {
|
||||
return undefined
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return JSON.parse(cacheFileContent) as WorkspaceState
|
||||
}
|
||||
10
workspace/state/src/types.ts
Normal file
10
workspace/state/src/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { type Project, type ProjectRootDir } from '@pnpm/types'
|
||||
|
||||
export type ProjectsList = Array<Pick<Project, 'rootDir'>>
|
||||
|
||||
export interface WorkspaceState {
|
||||
catalogs: Catalogs | undefined
|
||||
lastValidatedTimestamp: number
|
||||
projectRootDirs: ProjectRootDir[]
|
||||
}
|
||||
23
workspace/state/src/updateWorkspaceState.ts
Normal file
23
workspace/state/src/updateWorkspaceState.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { type Catalogs } from '@pnpm/catalogs.types'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { getFilePath } from './filePath'
|
||||
import { createWorkspaceState } from './createWorkspaceState'
|
||||
import { type ProjectsList } from './types'
|
||||
|
||||
export interface UpdateWorkspaceStateOptions {
|
||||
allProjects: ProjectsList
|
||||
catalogs: Catalogs | undefined
|
||||
lastValidatedTimestamp: number
|
||||
workspaceDir: string
|
||||
}
|
||||
|
||||
export async function updateWorkspaceState (opts: UpdateWorkspaceStateOptions): Promise<void> {
|
||||
logger.debug({ msg: 'updating workspace state' })
|
||||
const workspaceState = createWorkspaceState(opts)
|
||||
const workspaceStateJSON = JSON.stringify(workspaceState, undefined, 2) + '\n'
|
||||
const cacheFile = getFilePath(opts.workspaceDir)
|
||||
await fs.promises.mkdir(path.dirname(cacheFile), { recursive: true })
|
||||
await fs.promises.writeFile(cacheFile, workspaceStateJSON)
|
||||
}
|
||||
59
workspace/state/test/createWorkspaceState.test.ts
Normal file
59
workspace/state/test/createWorkspaceState.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import path from 'path'
|
||||
import { prepareEmpty, preparePackages } from '@pnpm/prepare'
|
||||
import { type ProjectRootDir } from '@pnpm/types'
|
||||
import { createWorkspaceState } from '../src/createWorkspaceState'
|
||||
|
||||
const lastValidatedTimestamp = Date.now()
|
||||
|
||||
test('createWorkspaceState() on empty list', () => {
|
||||
prepareEmpty()
|
||||
|
||||
expect(
|
||||
createWorkspaceState({
|
||||
allProjects: [],
|
||||
catalogs: undefined,
|
||||
lastValidatedTimestamp,
|
||||
})
|
||||
).toStrictEqual({
|
||||
catalogs: undefined,
|
||||
lastValidatedTimestamp,
|
||||
projectRootDirs: [],
|
||||
})
|
||||
})
|
||||
|
||||
test('createWorkspaceState() on non-empty list', () => {
|
||||
preparePackages(['a', 'b', 'c', 'd'].map(name => ({
|
||||
location: `./packages/${name}`,
|
||||
package: { name },
|
||||
})))
|
||||
|
||||
expect(
|
||||
createWorkspaceState({
|
||||
allProjects: [
|
||||
{ rootDir: path.resolve('packages/c') as ProjectRootDir },
|
||||
{ rootDir: path.resolve('packages/b') as ProjectRootDir },
|
||||
{ rootDir: path.resolve('packages/a') as ProjectRootDir },
|
||||
{ rootDir: path.resolve('packages/d') as ProjectRootDir },
|
||||
],
|
||||
lastValidatedTimestamp,
|
||||
catalogs: {
|
||||
default: {
|
||||
foo: '0.1.2',
|
||||
},
|
||||
},
|
||||
})
|
||||
).toStrictEqual({
|
||||
catalogs: {
|
||||
default: {
|
||||
foo: '0.1.2',
|
||||
},
|
||||
},
|
||||
lastValidatedTimestamp,
|
||||
projectRootDirs: [
|
||||
path.resolve('packages/a'),
|
||||
path.resolve('packages/b'),
|
||||
path.resolve('packages/c'),
|
||||
path.resolve('packages/d'),
|
||||
],
|
||||
})
|
||||
})
|
||||
12
workspace/state/test/filePath.test.ts
Normal file
12
workspace/state/test/filePath.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import path from 'path'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { getFilePath } from '../src/filePath'
|
||||
|
||||
test('getFilePath()', () => {
|
||||
prepareEmpty()
|
||||
expect(
|
||||
getFilePath(process.cwd())
|
||||
).toStrictEqual(
|
||||
path.resolve(path.resolve('node_modules/.pnpm-workspace-state.json'))
|
||||
)
|
||||
})
|
||||
60
workspace/state/test/loadWorkspaceState.test.ts
Normal file
60
workspace/state/test/loadWorkspaceState.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { type ProjectRootDir } from '@pnpm/types'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { getFilePath } from '../src/filePath'
|
||||
import { type WorkspaceState, loadWorkspaceState } from '../src/index'
|
||||
|
||||
const lastValidatedTimestamp = Date.now()
|
||||
|
||||
const originalLoggerDebug = logger.debug
|
||||
beforeEach(() => {
|
||||
logger.debug = jest.fn(originalLoggerDebug)
|
||||
})
|
||||
afterEach(() => {
|
||||
logger.debug = originalLoggerDebug
|
||||
})
|
||||
|
||||
const expectedLoggerCalls = [[{ msg: 'loading workspace state' }]]
|
||||
|
||||
test('loadWorkspaceState() when cache dir does not exist', async () => {
|
||||
prepareEmpty()
|
||||
const workspaceDir = process.cwd()
|
||||
expect(loadWorkspaceState(workspaceDir)).toBeUndefined()
|
||||
expect((logger.debug as jest.Mock).mock.calls).toStrictEqual(expectedLoggerCalls)
|
||||
})
|
||||
|
||||
test('loadWorkspaceState() when cache dir exists but not the file', async () => {
|
||||
prepareEmpty()
|
||||
const workspaceDir = process.cwd()
|
||||
const cacheFile = getFilePath(workspaceDir)
|
||||
fs.mkdirSync(path.dirname(cacheFile), { recursive: true })
|
||||
expect(loadWorkspaceState(workspaceDir)).toBeUndefined()
|
||||
expect((logger.debug as jest.Mock).mock.calls).toStrictEqual(expectedLoggerCalls)
|
||||
})
|
||||
|
||||
test('loadWorkspaceState() when cache file exists and is correct', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const workspaceDir = process.cwd()
|
||||
const cacheFile = getFilePath(workspaceDir)
|
||||
fs.mkdirSync(path.dirname(cacheFile), { recursive: true })
|
||||
const workspaceState: WorkspaceState = {
|
||||
catalogs: {
|
||||
default: {
|
||||
foo: '0.1.2',
|
||||
},
|
||||
},
|
||||
lastValidatedTimestamp,
|
||||
projectRootDirs: [
|
||||
path.resolve('packages/a') as ProjectRootDir,
|
||||
path.resolve('packages/b') as ProjectRootDir,
|
||||
path.resolve('packages/c') as ProjectRootDir,
|
||||
path.resolve('packages/d') as ProjectRootDir,
|
||||
],
|
||||
}
|
||||
fs.writeFileSync(cacheFile, JSON.stringify(workspaceState))
|
||||
expect(loadWorkspaceState(workspaceDir)).toStrictEqual(workspaceState)
|
||||
expect((logger.debug as jest.Mock).mock.calls).toStrictEqual(expectedLoggerCalls)
|
||||
})
|
||||
17
workspace/state/test/tsconfig.json
Normal file
17
workspace/state/test/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "../test.lib",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
68
workspace/state/test/updatePackagesList.test.ts
Normal file
68
workspace/state/test/updatePackagesList.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import path from 'path'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { preparePackages } from '@pnpm/prepare'
|
||||
import { type ProjectRootDir } from '@pnpm/types'
|
||||
import { loadWorkspaceState, updateWorkspaceState } from '../src/index'
|
||||
|
||||
const lastValidatedTimestamp = Date.now()
|
||||
|
||||
const originalLoggerDebug = logger.debug
|
||||
afterEach(() => {
|
||||
logger.debug = originalLoggerDebug
|
||||
})
|
||||
|
||||
test('updateWorkspaceState()', async () => {
|
||||
preparePackages(['a', 'b', 'c', 'd'].map(name => ({
|
||||
location: `./packages/${name}`,
|
||||
package: { name },
|
||||
})))
|
||||
|
||||
const workspaceDir = process.cwd()
|
||||
|
||||
expect(loadWorkspaceState(workspaceDir)).toBeUndefined()
|
||||
|
||||
logger.debug = jest.fn(originalLoggerDebug)
|
||||
await updateWorkspaceState({
|
||||
lastValidatedTimestamp,
|
||||
workspaceDir,
|
||||
catalogs: undefined,
|
||||
allProjects: [],
|
||||
})
|
||||
expect((logger.debug as jest.Mock).mock.calls).toStrictEqual([[{ msg: 'updating workspace state' }]])
|
||||
expect(loadWorkspaceState(workspaceDir)).toStrictEqual({
|
||||
lastValidatedTimestamp,
|
||||
projectRootDirs: [],
|
||||
})
|
||||
|
||||
logger.debug = jest.fn(originalLoggerDebug)
|
||||
await updateWorkspaceState({
|
||||
lastValidatedTimestamp,
|
||||
workspaceDir,
|
||||
catalogs: {
|
||||
default: {
|
||||
foo: '0.1.2',
|
||||
},
|
||||
},
|
||||
allProjects: [
|
||||
{ rootDir: path.resolve('packages/c') as ProjectRootDir },
|
||||
{ rootDir: path.resolve('packages/a') as ProjectRootDir },
|
||||
{ rootDir: path.resolve('packages/d') as ProjectRootDir },
|
||||
{ rootDir: path.resolve('packages/b') as ProjectRootDir },
|
||||
],
|
||||
})
|
||||
expect((logger.debug as jest.Mock).mock.calls).toStrictEqual([[{ msg: 'updating workspace state' }]])
|
||||
expect(loadWorkspaceState(workspaceDir)).toStrictEqual({
|
||||
catalogs: {
|
||||
default: {
|
||||
foo: '0.1.2',
|
||||
},
|
||||
},
|
||||
lastValidatedTimestamp,
|
||||
projectRootDirs: [
|
||||
path.resolve('packages/a'),
|
||||
path.resolve('packages/b'),
|
||||
path.resolve('packages/c'),
|
||||
path.resolve('packages/d'),
|
||||
],
|
||||
})
|
||||
})
|
||||
25
workspace/state/tsconfig.json
Normal file
25
workspace/state/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../__utils__/prepare"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/types"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/logger"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/types"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
workspace/state/tsconfig.lint.json
Normal file
8
workspace/state/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user