mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-24 07:38:12 -05:00
feat(deploy): create dedicated lockfile (#8828)
* feat(deploy): create dedicated lockfile
Closes https://github.com/pnpm/pnpm/issues/8778
* chore: remove a leftover `console.log`
* fix: hoisted should also use dedicated lockfile
* feat: inherit more keys
* docs(changeset): more details
* refactor: remove a variable
* refactor: use `selectedProject.rootDir`
* fix: manifest files
* fix: update lockfile
* fix: accidentally skipped normal dependencies
* test: update
* fix: meta
* fix: remove links to nowhere
* docs: remove the false todo
* fix: transitive workspace dependencies
* fix: package snapshot names
* fix: dependencies that depend on deployed package
* perf: do not repeat computation
* fix: compile error
* refactor: base on allProjects
* fix: add missing `link:` prefix
* test: add some tests
* fix: revert stupid refactor
This reverts commit 000788127c.
* test: more assertions
* test: more assertions
* test: https://github.com/pnpm/pnpm/issues/8778
* test: exact paths
* refactor: use `toBe`
* refactor: divide section
* fix: eslint
* test: fix assertions
* fix: dependencies that depend on deploy package
* perf: cheap operation first
* test: remove `.only`
* test: add assertions
* test: remove unnecessary assertions
* test: remove unnecessary details
* fix: deployed package depends on itself
* docs: remove the other todo
* fix: self-referential dependencies
* test: fix
* test: more assertions
* feat: convert fallbacks to programmer errors
* fix: `file:` protocol
* refactor: more types
* refactor: remove unused variables
* refactor: fix regex
* feat: force-legacy-deploy
* feat: suggest reporting bug and using workaround
* feat: overrides, patchedDependencies, packageExtensions (wip)
* test: fix
* feat: handle `packageExtensions` in a smarter way
* fix: pnpmfile
* docs: change wording
* fix: `packageExtensions` with internal dependencies
* fix: directory resolution location
* refactor: use `rootProjectManifestDir`
* feat: set `overrides` to `undefined` instead
* refactor: remove `as ProjectRootDirRealPath`
* test: packageExtensions
* test: use regex string matchers
* refactor: move new tests to its own file
* fix: patchedDependencies
* fix: eslint
* test: patchedDependencies
* test: fix windows
* fix: pnpmfile checksum
* docs: change wording
* fix: peer dependencies
* docs: omission of peers
* docs: more detailed explanation
* fix: preserve unique peer dependencies suffix
* refactor: code rearrange
* refactor: shorten lines of code
* feat: add `dedupeInjectedDeps` to `InstallCommandOptions`
* test: peer dependencies suffix
* docs(changeset): config -> force-legacy-deploy
* docs(changeset): merge
* docs(changeset): add missing period
This commit is contained in:
7
.changeset/shy-countries-exercise.md
Normal file
7
.changeset/shy-countries-exercise.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@pnpm/config": minor
|
||||
"@pnpm/plugin-commands-deploy": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
`pnpm deploy` now tries creating a dedicated lockfile from a shared lockfile for deployment. It will fallback to deployment without a lockfile if there is no shared lockfile or `force-legacy-deploy` is set to `true`.
|
||||
6
.changeset/swift-shirts-brush.md
Normal file
6
.changeset/swift-shirts-brush.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-deploy": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Fix an issue in which `pnpm deploy --prod` fails due to missing `devDependencies` [#8778](https://github.com/pnpm/pnpm/issues/8778).
|
||||
5
.changeset/tough-eyes-care.md
Normal file
5
.changeset/tough-eyes-care.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-installation": minor
|
||||
---
|
||||
|
||||
Add `dedupeInjectedDeps` to `InstallCommandOptions`.
|
||||
@@ -102,6 +102,7 @@ export interface Config {
|
||||
failedToLoadBuiltInConfig: boolean
|
||||
resolvePeersFromWorkspaceRoot?: boolean
|
||||
deployAllFiles?: boolean
|
||||
forceLegacyDeploy?: boolean
|
||||
reporterHidePrefix?: boolean
|
||||
|
||||
// proxy
|
||||
|
||||
@@ -132,6 +132,7 @@ export async function getConfig (opts: {
|
||||
'fetch-retry-maxtimeout': 60000,
|
||||
'fetch-retry-mintimeout': 10000,
|
||||
'fetch-timeout': 60000,
|
||||
'force-legacy-deploy': false,
|
||||
'git-shallow-hosts': [
|
||||
// Follow https://github.com/npm/git/blob/1e1dbd26bd5b87ca055defecc3679777cb480e2a/lib/clone.js#L13-L19
|
||||
'github.com',
|
||||
|
||||
@@ -24,6 +24,7 @@ export const types = Object.assign({
|
||||
'fetching-concurrency': Number,
|
||||
filter: [String, Array],
|
||||
'filter-prod': [String, Array],
|
||||
'force-legacy-deploy': Boolean,
|
||||
'frozen-lockfile': Boolean,
|
||||
'git-checks': Boolean,
|
||||
'git-shallow-hosts': Array,
|
||||
|
||||
@@ -254,6 +254,7 @@ export type InstallCommandOptions = Pick<Config,
|
||||
| 'bin'
|
||||
| 'catalogs'
|
||||
| 'cliOptions'
|
||||
| 'dedupeInjectedDeps'
|
||||
| 'dedupeDirectDeps'
|
||||
| 'dedupePeerDependents'
|
||||
| 'deployAllFiles'
|
||||
|
||||
73
pnpm-lock.yaml
generated
73
pnpm-lock.yaml
generated
@@ -6126,6 +6126,9 @@ importers:
|
||||
'@pnpm/common-cli-options-help':
|
||||
specifier: workspace:*
|
||||
version: link:../../cli/common-cli-options-help
|
||||
'@pnpm/config':
|
||||
specifier: workspace:*
|
||||
version: link:../../config/config
|
||||
'@pnpm/directory-fetcher':
|
||||
specifier: workspace:*
|
||||
version: link:../../fetching/directory-fetcher
|
||||
@@ -6138,15 +6141,30 @@ importers:
|
||||
'@pnpm/fs.is-empty-dir-or-nothing':
|
||||
specifier: workspace:*
|
||||
version: link:../../fs/is-empty-dir-or-nothing
|
||||
'@pnpm/lockfile.fs':
|
||||
specifier: workspace:*
|
||||
version: link:../../lockfile/fs
|
||||
'@pnpm/lockfile.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../lockfile/types
|
||||
'@pnpm/plugin-commands-installation':
|
||||
specifier: workspace:*
|
||||
version: link:../../pkg-manager/plugin-commands-installation
|
||||
'@pnpm/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
'@types/normalize-path':
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.2
|
||||
'@zkochan/rimraf':
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.2
|
||||
normalize-path:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.0
|
||||
ramda:
|
||||
specifier: 'catalog:'
|
||||
version: '@pnpm/ramda@0.28.1'
|
||||
render-help:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.3
|
||||
@@ -6154,9 +6172,6 @@ importers:
|
||||
'@pnpm/assert-project':
|
||||
specifier: workspace:*
|
||||
version: link:../../__utils__/assert-project
|
||||
'@pnpm/lockfile.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../lockfile/types
|
||||
'@pnpm/logger':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/logger
|
||||
@@ -6169,9 +6184,15 @@ importers:
|
||||
'@pnpm/registry-mock':
|
||||
specifier: 'catalog:'
|
||||
version: 3.46.0(encoding@0.1.13)(typanion@3.14.0)
|
||||
'@pnpm/test-fixtures':
|
||||
specifier: workspace:*
|
||||
version: link:../../__utils__/test-fixtures
|
||||
'@pnpm/workspace.filter-packages-from-dir':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/filter-packages-from-dir
|
||||
'@types/ramda':
|
||||
specifier: 'catalog:'
|
||||
version: 0.29.12
|
||||
|
||||
releasing/plugin-commands-publishing:
|
||||
dependencies:
|
||||
@@ -8715,7 +8736,6 @@ packages:
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
deprecated: Use @eslint/config-array instead
|
||||
|
||||
'@humanwhocodes/module-importer@1.0.1':
|
||||
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
|
||||
@@ -8723,7 +8743,6 @@ packages:
|
||||
|
||||
'@humanwhocodes/object-schema@2.0.3':
|
||||
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
|
||||
deprecated: Use @eslint/object-schema instead
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
@@ -9584,10 +9603,6 @@ packages:
|
||||
peerDependencies:
|
||||
'@yarnpkg/core': ^4.0.5
|
||||
|
||||
'@yarnpkg/fslib@3.1.0':
|
||||
resolution: {integrity: sha512-wsj7/sUVSdXOIX/qwaON/Ky5GsP5gs9ry9DKwgLbWT7k3qw4/EcHAtfTtPhBYu33UibzBFI+fgB4wBRVH2XVaw==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
'@yarnpkg/fslib@3.1.1':
|
||||
resolution: {integrity: sha512-NpeecISQEuDnmipElGa0cOC7DnlPf3+FXnuwwJTciJgt+S/BDb8VFBvXSE5UirGmsFWlf4mfZuuAC7e8Pmhh4g==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
@@ -9626,11 +9641,6 @@ packages:
|
||||
engines: {node: '>=18.12.0'}
|
||||
hasBin: true
|
||||
|
||||
'@yarnpkg/shell@4.0.2':
|
||||
resolution: {integrity: sha512-DLZSx06OoEbPY1uePt7pKEgpWDk96PldrCdWBPqI5Np5/YAEo6+toVcjz+6fORMOE8PS3Bsep1Nfm2mUrY1Oxg==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
hasBin: true
|
||||
|
||||
'@yarnpkg/shell@4.1.1':
|
||||
resolution: {integrity: sha512-0aS71iJrNQ4cezU5BJ5JpBTXkFQPKkzOEpDtMQm8E2H3g9PLxUe/5VdA60bZq/4N/qazLLYEOngcFZ6QRpraVQ==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
@@ -10794,7 +10804,6 @@ packages:
|
||||
|
||||
eslint-config-standard-with-typescript@39.1.1:
|
||||
resolution: {integrity: sha512-t6B5Ep8E4I18uuoYeYxINyqcXb2UbC0SOOTxRtBSt2JUs+EzeXbfe2oaiPs71AIdnoWhXDO2fYOHz8df3kV84A==}
|
||||
deprecated: Please use eslint-config-love, instead.
|
||||
peerDependencies:
|
||||
'@typescript-eslint/eslint-plugin': ^6.4.0
|
||||
eslint: ^8.0.1
|
||||
@@ -10899,7 +10908,6 @@ packages:
|
||||
eslint@8.57.0:
|
||||
resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
|
||||
hasBin: true
|
||||
|
||||
esm@3.2.25:
|
||||
@@ -11316,7 +11324,6 @@ packages:
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
@@ -11582,7 +11589,6 @@ packages:
|
||||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
@@ -16687,10 +16693,10 @@ snapshots:
|
||||
'@arcanis/slice-ansi': 1.1.1
|
||||
'@types/semver': 7.5.8
|
||||
'@types/treeify': 1.0.3
|
||||
'@yarnpkg/fslib': 3.1.0
|
||||
'@yarnpkg/libzip': 3.1.0(@yarnpkg/fslib@3.1.0)
|
||||
'@yarnpkg/fslib': 3.1.1
|
||||
'@yarnpkg/libzip': 3.1.0(@yarnpkg/fslib@3.1.1)
|
||||
'@yarnpkg/parsers': 3.0.2
|
||||
'@yarnpkg/shell': 4.0.2(typanion@3.14.0)
|
||||
'@yarnpkg/shell': 4.1.1(typanion@3.14.0)
|
||||
camelcase: 5.3.1
|
||||
chalk: 3.0.0
|
||||
ci-info: 3.9.0
|
||||
@@ -16748,20 +16754,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@yarnpkg/core': 4.0.5(typanion@3.14.0)
|
||||
|
||||
'@yarnpkg/fslib@3.1.0':
|
||||
dependencies:
|
||||
tslib: 2.7.0
|
||||
|
||||
'@yarnpkg/fslib@3.1.1':
|
||||
dependencies:
|
||||
tslib: 2.7.0
|
||||
|
||||
'@yarnpkg/libzip@3.1.0(@yarnpkg/fslib@3.1.0)':
|
||||
dependencies:
|
||||
'@types/emscripten': 1.39.13
|
||||
'@yarnpkg/fslib': 3.1.0
|
||||
tslib: 2.7.0
|
||||
|
||||
'@yarnpkg/libzip@3.1.0(@yarnpkg/fslib@3.1.1)':
|
||||
dependencies:
|
||||
'@types/emscripten': 1.39.13
|
||||
@@ -16791,7 +16787,7 @@ snapshots:
|
||||
'@yarnpkg/pnp@4.0.6':
|
||||
dependencies:
|
||||
'@types/node': 18.19.49
|
||||
'@yarnpkg/fslib': 3.1.0
|
||||
'@yarnpkg/fslib': 3.1.1
|
||||
|
||||
'@yarnpkg/pnp@4.0.7':
|
||||
dependencies:
|
||||
@@ -16811,19 +16807,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typanion
|
||||
|
||||
'@yarnpkg/shell@4.0.2(typanion@3.14.0)':
|
||||
dependencies:
|
||||
'@yarnpkg/fslib': 3.1.0
|
||||
'@yarnpkg/parsers': 3.0.2
|
||||
chalk: 3.0.0
|
||||
clipanion: 3.2.0-rc.6(typanion@3.14.0)
|
||||
cross-spawn: 7.0.5
|
||||
fast-glob: 3.3.2
|
||||
micromatch: 4.0.8
|
||||
tslib: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- typanion
|
||||
|
||||
'@yarnpkg/shell@4.1.1(typanion@3.14.0)':
|
||||
dependencies:
|
||||
'@yarnpkg/fslib': 3.1.1
|
||||
|
||||
@@ -35,25 +35,32 @@
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/releasing/plugin-commands-deploy#readme",
|
||||
"devDependencies": {
|
||||
"@pnpm/assert-project": "workspace:*",
|
||||
"@pnpm/lockfile.types": "workspace:*",
|
||||
"@pnpm/logger": "workspace:*",
|
||||
"@pnpm/plugin-commands-deploy": "workspace:*",
|
||||
"@pnpm/prepare": "workspace:*",
|
||||
"@pnpm/registry-mock": "catalog:",
|
||||
"@pnpm/workspace.filter-packages-from-dir": "workspace:*"
|
||||
"@pnpm/test-fixtures": "workspace:*",
|
||||
"@pnpm/workspace.filter-packages-from-dir": "workspace:*",
|
||||
"@types/ramda": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/catalogs.resolver": "workspace:*",
|
||||
"@pnpm/catalogs.types": "workspace:*",
|
||||
"@pnpm/cli-utils": "workspace:*",
|
||||
"@pnpm/config": "workspace:*",
|
||||
"@pnpm/common-cli-options-help": "workspace:*",
|
||||
"@pnpm/directory-fetcher": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/fs.indexed-pkg-importer": "workspace:*",
|
||||
"@pnpm/fs.is-empty-dir-or-nothing": "workspace:*",
|
||||
"@pnpm/lockfile.fs": "workspace:*",
|
||||
"@pnpm/lockfile.types": "workspace:*",
|
||||
"@pnpm/plugin-commands-installation": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"@types/normalize-path": "catalog:",
|
||||
"@zkochan/rimraf": "catalog:",
|
||||
"normalize-path": "catalog:",
|
||||
"ramda": "catalog:",
|
||||
"render-help": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
308
releasing/plugin-commands-deploy/src/createDeployFiles.ts
Normal file
308
releasing/plugin-commands-deploy/src/createDeployFiles.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import path from 'path'
|
||||
import url from 'url'
|
||||
import normalizePath from 'normalize-path'
|
||||
import pick from 'ramda/src/pick'
|
||||
import {
|
||||
type DirectoryResolution,
|
||||
type LockfileObject,
|
||||
type LockfileResolution,
|
||||
type PackageSnapshot,
|
||||
type PackageSnapshots,
|
||||
type ProjectSnapshot,
|
||||
type ResolvedDependencies,
|
||||
} from '@pnpm/lockfile.types'
|
||||
import {
|
||||
type DependenciesField,
|
||||
type DepPath,
|
||||
type Project,
|
||||
type ProjectId,
|
||||
type ProjectManifest,
|
||||
} from '@pnpm/types'
|
||||
|
||||
const DEPENDENCIES_FIELD = ['dependencies', 'devDependencies', 'optionalDependencies'] as const satisfies DependenciesField[]
|
||||
|
||||
const INHERITED_MANIFEST_KEYS = [
|
||||
'name',
|
||||
'description',
|
||||
'version',
|
||||
'private',
|
||||
'author',
|
||||
'bin',
|
||||
'scripts',
|
||||
'packageManager',
|
||||
'dependenciesMeta',
|
||||
'peerDependenciesMeta',
|
||||
] as const satisfies Array<keyof ProjectManifest>
|
||||
|
||||
export type DeployManifest = Pick<ProjectManifest, typeof INHERITED_MANIFEST_KEYS[number] | DependenciesField | 'pnpm'>
|
||||
|
||||
export interface CreateDeployFilesOptions {
|
||||
allProjects: Array<Pick<Project, 'manifest' | 'rootDirRealPath'>>
|
||||
deployDir: string
|
||||
lockfile: LockfileObject
|
||||
lockfileDir: string
|
||||
manifest: DeployManifest
|
||||
projectId: ProjectId
|
||||
rootProjectManifestDir: string
|
||||
}
|
||||
|
||||
export interface DeployFiles {
|
||||
lockfile: LockfileObject
|
||||
manifest: DeployManifest
|
||||
}
|
||||
|
||||
export function createDeployFiles ({
|
||||
allProjects,
|
||||
deployDir,
|
||||
lockfile,
|
||||
lockfileDir,
|
||||
manifest,
|
||||
projectId,
|
||||
rootProjectManifestDir,
|
||||
}: CreateDeployFilesOptions): DeployFiles {
|
||||
const deployedProjectRealPath = path.resolve(lockfileDir, projectId)
|
||||
const inputSnapshot = lockfile.importers[projectId]
|
||||
|
||||
const targetSnapshot: ProjectSnapshot = {
|
||||
...inputSnapshot,
|
||||
specifiers: {},
|
||||
dependencies: {},
|
||||
devDependencies: {},
|
||||
optionalDependencies: {},
|
||||
}
|
||||
|
||||
const targetPackageSnapshots: PackageSnapshots = {}
|
||||
for (const name in lockfile.packages) {
|
||||
const inputDepPath = name as DepPath
|
||||
const inputSnapshot = lockfile.packages[inputDepPath]
|
||||
const resolveResult = resolveLinkOrFile(inputDepPath, {
|
||||
lockfileDir,
|
||||
projectRootDirRealPath: rootProjectManifestDir,
|
||||
})
|
||||
const outputDepPath = resolveResult
|
||||
? createFileUrlDepPath(resolveResult, allProjects)
|
||||
: inputDepPath
|
||||
targetPackageSnapshots[outputDepPath] = convertPackageSnapshot(inputSnapshot, {
|
||||
allProjects,
|
||||
deployDir,
|
||||
deployedProjectRealPath,
|
||||
lockfileDir,
|
||||
projectRootDirRealPath: rootProjectManifestDir,
|
||||
})
|
||||
}
|
||||
|
||||
for (const importerPath in lockfile.importers) {
|
||||
if (importerPath === projectId) continue
|
||||
const projectSnapshot = lockfile.importers[importerPath as ProjectId]
|
||||
const projectRootDirRealPath = path.resolve(lockfileDir, importerPath)
|
||||
const packageSnapshot = convertProjectSnapshotToPackageSnapshot(projectSnapshot, {
|
||||
allProjects,
|
||||
deployDir,
|
||||
lockfileDir,
|
||||
deployedProjectRealPath,
|
||||
projectRootDirRealPath,
|
||||
})
|
||||
const depPath = createFileUrlDepPath({ resolvedPath: projectRootDirRealPath }, allProjects)
|
||||
targetPackageSnapshots[depPath] = packageSnapshot
|
||||
}
|
||||
|
||||
for (const field of DEPENDENCIES_FIELD) {
|
||||
const targetDependencies = targetSnapshot[field] ?? {}
|
||||
const targetSpecifiers = targetSnapshot.specifiers
|
||||
const inputDependencies = inputSnapshot[field] ?? {}
|
||||
for (const name in inputDependencies) {
|
||||
const spec = inputDependencies[name]
|
||||
const resolveResult = resolveLinkOrFile(spec, {
|
||||
lockfileDir,
|
||||
projectRootDirRealPath: path.resolve(lockfileDir, projectId),
|
||||
})
|
||||
|
||||
if (!resolveResult) {
|
||||
targetSpecifiers[name] = targetDependencies[name] = spec
|
||||
continue
|
||||
}
|
||||
|
||||
targetSpecifiers[name] = targetDependencies[name] =
|
||||
resolveResult.resolvedPath === deployedProjectRealPath ? 'link:.' : createFileUrlDepPath(resolveResult, allProjects)
|
||||
}
|
||||
}
|
||||
|
||||
const result: DeployFiles = {
|
||||
lockfile: {
|
||||
...lockfile,
|
||||
overrides: undefined, // the effects of package overrides should already be part of the package snapshots
|
||||
patchedDependencies: undefined,
|
||||
packageExtensionsChecksum: undefined, // the effects of the package extensions should already be part of the package snapshots
|
||||
pnpmfileChecksum: undefined, // the effects of the pnpmfile should already be part of the package snapshots
|
||||
importers: {
|
||||
['.' as ProjectId]: targetSnapshot,
|
||||
},
|
||||
packages: targetPackageSnapshots,
|
||||
},
|
||||
manifest: {
|
||||
...pick(INHERITED_MANIFEST_KEYS, manifest),
|
||||
dependencies: targetSnapshot.dependencies,
|
||||
devDependencies: targetSnapshot.devDependencies,
|
||||
optionalDependencies: targetSnapshot.optionalDependencies,
|
||||
pnpm: {
|
||||
...manifest.pnpm,
|
||||
overrides: undefined, // the effects of package overrides should already be part of the package snapshots
|
||||
patchedDependencies: undefined,
|
||||
packageExtensions: undefined, // the effects of the package extensions should already be part of the package snapshots
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if (lockfile.patchedDependencies) {
|
||||
result.lockfile.patchedDependencies = {}
|
||||
result.manifest.pnpm!.patchedDependencies = {}
|
||||
|
||||
for (const name in lockfile.patchedDependencies) {
|
||||
const patchInfo = lockfile.patchedDependencies[name]
|
||||
const resolvedPath = path.resolve(rootProjectManifestDir, patchInfo.path)
|
||||
const relativePath = normalizePath(path.relative(deployDir, resolvedPath))
|
||||
result.manifest.pnpm!.patchedDependencies[name] = relativePath
|
||||
result.lockfile.patchedDependencies[name] = {
|
||||
hash: patchInfo.hash,
|
||||
path: relativePath,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
interface ConvertOptions {
|
||||
allProjects: CreateDeployFilesOptions['allProjects']
|
||||
deployDir: string
|
||||
deployedProjectRealPath: string
|
||||
projectRootDirRealPath: string
|
||||
lockfileDir: string
|
||||
}
|
||||
|
||||
function convertPackageSnapshot (inputSnapshot: PackageSnapshot, opts: ConvertOptions): PackageSnapshot {
|
||||
const inputResolution = inputSnapshot.resolution
|
||||
let outputResolution: LockfileResolution
|
||||
if ('integrity' in inputResolution) {
|
||||
outputResolution = inputResolution
|
||||
} else if ('tarball' in inputResolution) {
|
||||
outputResolution = { ...inputResolution }
|
||||
if (inputResolution.tarball.startsWith('file:')) {
|
||||
const inputPath = inputResolution.tarball.slice('file:'.length)
|
||||
const resolvedPath = path.resolve(opts.lockfileDir, inputPath)
|
||||
const outputPath = normalizePath(path.relative(opts.deployDir, resolvedPath))
|
||||
outputResolution.tarball = `file:${outputPath}`
|
||||
if (inputResolution.path) outputResolution.path = outputPath
|
||||
}
|
||||
} else if (inputResolution.type === 'directory') {
|
||||
const resolvedPath = path.resolve(opts.lockfileDir, inputResolution.directory)
|
||||
const directory = normalizePath(path.relative(opts.deployDir, resolvedPath))
|
||||
outputResolution = { ...inputResolution, directory }
|
||||
} else if (inputResolution.type === 'git') {
|
||||
outputResolution = inputResolution
|
||||
} else {
|
||||
const resolution: never = inputResolution // `never` is the type guard to force fixing this code when adding new type of resolution
|
||||
throw new Error(`Unknown resolution type: ${JSON.stringify(resolution)}`)
|
||||
}
|
||||
|
||||
return {
|
||||
...inputSnapshot,
|
||||
resolution: outputResolution,
|
||||
dependencies: convertResolvedDependencies(inputSnapshot.dependencies, opts),
|
||||
optionalDependencies: convertResolvedDependencies(inputSnapshot.optionalDependencies, opts),
|
||||
}
|
||||
}
|
||||
|
||||
function convertProjectSnapshotToPackageSnapshot (projectSnapshot: ProjectSnapshot, opts: ConvertOptions): PackageSnapshot {
|
||||
const resolution: DirectoryResolution = {
|
||||
type: 'directory',
|
||||
directory: normalizePath(path.relative(opts.deployDir, opts.projectRootDirRealPath)),
|
||||
}
|
||||
const dependencies = convertResolvedDependencies(projectSnapshot.dependencies, opts)
|
||||
const optionalDependencies = convertResolvedDependencies(projectSnapshot.optionalDependencies, opts)
|
||||
return {
|
||||
dependencies,
|
||||
optionalDependencies,
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
|
||||
function convertResolvedDependencies (
|
||||
input: ResolvedDependencies | undefined,
|
||||
opts: Pick<ConvertOptions, 'allProjects' | 'deployedProjectRealPath' | 'lockfileDir' | 'projectRootDirRealPath'>
|
||||
): ResolvedDependencies | undefined {
|
||||
if (!input) return undefined
|
||||
const output: ResolvedDependencies = {}
|
||||
|
||||
for (const key in input) {
|
||||
const spec = input[key]
|
||||
const resolveResult = resolveLinkOrFile(spec, opts)
|
||||
if (!resolveResult) {
|
||||
output[key] = spec
|
||||
continue
|
||||
}
|
||||
|
||||
if (resolveResult.resolvedPath === opts.deployedProjectRealPath) {
|
||||
output[key] = 'link:.' // the path is relative to the lockfile dir, which means '.' would reference the deploy dir
|
||||
continue
|
||||
}
|
||||
|
||||
output[key] = createFileUrlDepPath(resolveResult, opts.allProjects)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
interface ResolveLinkOrFileResult {
|
||||
scheme: 'link:' | 'file:'
|
||||
resolvedPath: string
|
||||
suffix?: `(${string})`
|
||||
}
|
||||
|
||||
function resolveLinkOrFile (spec: string, opts: Pick<ConvertOptions, 'lockfileDir' | 'projectRootDirRealPath'>): ResolveLinkOrFileResult | undefined {
|
||||
// try parsing `spec` as `spec(peers)`
|
||||
const hasPeers = /^(?<spec>[^()]+)(?<peers>\(.+\))$/.exec(spec)
|
||||
if (hasPeers) {
|
||||
const result = resolveLinkOrFile(hasPeers.groups!.spec, opts)
|
||||
if (!result) return undefined
|
||||
if (result.suffix) {
|
||||
throw new Error(`Something goes wrong, suffix is not undefined: ${result.suffix}`)
|
||||
}
|
||||
result.suffix = hasPeers.groups!.peers as `(${string})`
|
||||
return result
|
||||
}
|
||||
|
||||
// try parsing `spec` as either @scope/name@pref or name@pref
|
||||
const renamed = /^@(?<scope>[^@]+)\/(?<name>[^@]+)@(?<pref>.+)$/.exec(spec) ?? /^(?<name>[^@]+)@(?<pref>.+)$/.exec(spec)
|
||||
if (renamed) return resolveLinkOrFile(renamed.groups!.pref, opts)
|
||||
|
||||
const { lockfileDir, projectRootDirRealPath } = opts
|
||||
|
||||
if (spec.startsWith('link:')) {
|
||||
const targetPath = spec.slice('link:'.length)
|
||||
return {
|
||||
scheme: 'link:',
|
||||
resolvedPath: path.resolve(projectRootDirRealPath, targetPath),
|
||||
}
|
||||
}
|
||||
|
||||
if (spec.startsWith('file:')) {
|
||||
const targetPath = spec.slice('file:'.length)
|
||||
return {
|
||||
scheme: 'file:',
|
||||
resolvedPath: path.resolve(lockfileDir, targetPath),
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function createFileUrlDepPath (
|
||||
{ resolvedPath, suffix }: Pick<ResolveLinkOrFileResult, 'resolvedPath' | 'suffix'>,
|
||||
allProjects: CreateDeployFilesOptions['allProjects']
|
||||
): DepPath {
|
||||
const depFileUrl = url.pathToFileURL(resolvedPath).toString()
|
||||
const project = allProjects.find(project => project.rootDirRealPath === resolvedPath)
|
||||
const name = project?.manifest.name ?? path.basename(resolvedPath)
|
||||
return `${name}@${depFileUrl}${suffix ?? ''}` as DepPath
|
||||
}
|
||||
@@ -1,26 +1,45 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import pick from 'ramda/src/pick'
|
||||
import { docsUrl } from '@pnpm/cli-utils'
|
||||
import { type Config, types as configTypes } from '@pnpm/config'
|
||||
import { fetchFromDir } from '@pnpm/directory-fetcher'
|
||||
import { createIndexedPkgImporter } from '@pnpm/fs.indexed-pkg-importer'
|
||||
import { isEmptyDirOrNothing } from '@pnpm/fs.is-empty-dir-or-nothing'
|
||||
import { install } from '@pnpm/plugin-commands-installation'
|
||||
import { FILTERING } from '@pnpm/common-cli-options-help'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { readWantedLockfile, writeWantedLockfile } from '@pnpm/lockfile.fs'
|
||||
import rimraf from '@zkochan/rimraf'
|
||||
import renderHelp from 'render-help'
|
||||
import { deployHook } from './deployHook'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { logger, globalWarn } from '@pnpm/logger'
|
||||
import { type Project, type ProjectId } from '@pnpm/types'
|
||||
import normalizePath from 'normalize-path'
|
||||
import { createDeployFiles } from './createDeployFiles'
|
||||
import { deployCatalogHook } from './deployCatalogHook'
|
||||
|
||||
export const shorthands = install.shorthands
|
||||
const FORCE_LEGACY_DEPLOY = 'force-legacy-deploy' satisfies keyof typeof configTypes
|
||||
|
||||
export const shorthands = {
|
||||
...install.shorthands,
|
||||
legacy: [`--config.${FORCE_LEGACY_DEPLOY}=true`],
|
||||
}
|
||||
|
||||
const DEPLOY_OWN_OPTIONS = pick([FORCE_LEGACY_DEPLOY], configTypes)
|
||||
|
||||
export function rcOptionsTypes (): Record<string, unknown> {
|
||||
return install.rcOptionsTypes()
|
||||
return {
|
||||
...install.rcOptionsTypes(),
|
||||
...DEPLOY_OWN_OPTIONS,
|
||||
}
|
||||
}
|
||||
|
||||
export function cliOptionsTypes (): Record<string, unknown> {
|
||||
return install.cliOptionsTypes()
|
||||
return {
|
||||
...install.cliOptionsTypes(),
|
||||
...DEPLOY_OWN_OPTIONS,
|
||||
}
|
||||
}
|
||||
|
||||
export const commandNames = ['deploy']
|
||||
@@ -48,6 +67,10 @@ export function help (): string {
|
||||
description: '`optionalDependencies` are not installed',
|
||||
name: '--no-optional',
|
||||
},
|
||||
{
|
||||
description: 'Force legacy deploy implementation',
|
||||
name: '--legacy',
|
||||
},
|
||||
],
|
||||
},
|
||||
FILTERING,
|
||||
@@ -55,27 +78,28 @@ export function help (): string {
|
||||
})
|
||||
}
|
||||
|
||||
export async function handler (
|
||||
opts: Omit<install.InstallCommandOptions, 'useLockfile'>,
|
||||
params: string[]
|
||||
): Promise<void> {
|
||||
export type DeployOptions =
|
||||
& Omit<install.InstallCommandOptions, 'useLockfile'>
|
||||
& Pick<Config, 'forceLegacyDeploy'>
|
||||
|
||||
export async function handler (opts: DeployOptions, params: string[]): Promise<void> {
|
||||
if (!opts.workspaceDir) {
|
||||
throw new PnpmError('CANNOT_DEPLOY', 'A deploy is only possible from inside a workspace')
|
||||
}
|
||||
if (!opts.injectWorkspacePackages) {
|
||||
throw new PnpmError('DEPLOY_NONINJECTED_WORKSPACE', 'We only support deploy from workspaces that use the inject-workspace-packages=true setting')
|
||||
}
|
||||
const selectedDirs = Object.keys(opts.selectedProjectsGraph ?? {})
|
||||
if (selectedDirs.length === 0) {
|
||||
const selectedProjects = Object.values(opts.selectedProjectsGraph ?? {})
|
||||
if (selectedProjects.length === 0) {
|
||||
throw new PnpmError('NOTHING_TO_DEPLOY', 'No project was selected for deployment')
|
||||
}
|
||||
if (selectedDirs.length > 1) {
|
||||
if (selectedProjects.length > 1) {
|
||||
throw new PnpmError('CANNOT_DEPLOY_MANY', 'Cannot deploy more than 1 project')
|
||||
}
|
||||
if (params.length !== 1) {
|
||||
throw new PnpmError('INVALID_DEPLOY_TARGET', 'This command requires one parameter')
|
||||
}
|
||||
const deployedDir = selectedDirs[0]
|
||||
const selectedProject = selectedProjects[0].package
|
||||
const deployDirParam = params[0]
|
||||
const deployDir = path.isAbsolute(deployDirParam) ? deployDirParam : path.join(opts.dir, deployDirParam)
|
||||
|
||||
@@ -90,10 +114,22 @@ export async function handler (
|
||||
await rimraf(deployDir)
|
||||
await fs.promises.mkdir(deployDir, { recursive: true })
|
||||
const includeOnlyPackageFiles = !opts.deployAllFiles
|
||||
await copyProject(deployedDir, deployDir, { includeOnlyPackageFiles })
|
||||
const deployedProject = opts.allProjects?.find(({ rootDir }) => rootDir === deployedDir)
|
||||
await copyProject(selectedProject.rootDir, deployDir, { includeOnlyPackageFiles })
|
||||
|
||||
if (opts.sharedWorkspaceLockfile) {
|
||||
const warning = opts.forceLegacyDeploy
|
||||
? 'Shared workspace lockfile detected but configuration forces legacy deploy implementation.'
|
||||
: await deployFromSharedLockfile(opts, selectedProject, deployDir)
|
||||
if (warning) {
|
||||
globalWarn(warning)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const deployedProject = opts.allProjects?.find(({ rootDir }) => rootDir === selectedProject.rootDir)
|
||||
if (deployedProject) {
|
||||
deployedProject.modulesDir = path.relative(deployedDir, path.join(deployDir, 'node_modules'))
|
||||
deployedProject.modulesDir = path.relative(selectedProject.rootDir, path.join(deployDir, 'node_modules'))
|
||||
}
|
||||
await install.handler({
|
||||
...opts,
|
||||
@@ -134,3 +170,85 @@ async function copyProject (src: string, dest: string, opts: { includeOnlyPackag
|
||||
const importPkg = createIndexedPkgImporter('clone-or-copy')
|
||||
importPkg(dest, { filesMap: filesIndex, force: true, resolvedFrom: 'local-dir' })
|
||||
}
|
||||
|
||||
async function deployFromSharedLockfile (
|
||||
opts: DeployOptions,
|
||||
selectedProject: Pick<Project, 'rootDir'> & {
|
||||
manifest: Pick<Project['manifest'], 'name' | 'version'>
|
||||
},
|
||||
deployDir: string
|
||||
): Promise<string | undefined> {
|
||||
const {
|
||||
allProjects,
|
||||
lockfileDir,
|
||||
rootProjectManifestDir,
|
||||
workspaceDir,
|
||||
} = opts
|
||||
|
||||
// The following errors should not be possible. It is a programmer error if they are reached.
|
||||
if (!allProjects) throw new Error('opts.allProjects is undefined.')
|
||||
if (!lockfileDir) throw new Error('opts.lockfileDir is undefined.')
|
||||
if (!workspaceDir) throw new Error('opts.workspaceDir is undefined.')
|
||||
|
||||
const lockfile = await readWantedLockfile(lockfileDir, { ignoreIncompatible: false })
|
||||
if (!lockfile) {
|
||||
return 'Shared lockfile not found. Falling back to installing without a lockfile.'
|
||||
}
|
||||
|
||||
const projectId = normalizePath(path.relative(workspaceDir, selectedProject.rootDir)) as ProjectId
|
||||
|
||||
const deployFiles = createDeployFiles({
|
||||
allProjects,
|
||||
deployDir,
|
||||
lockfile,
|
||||
lockfileDir,
|
||||
manifest: selectedProject.manifest,
|
||||
projectId,
|
||||
rootProjectManifestDir,
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
fs.promises.writeFile(
|
||||
path.join(deployDir, 'package.json'),
|
||||
JSON.stringify(deployFiles.manifest, undefined, 2) + '\n'
|
||||
),
|
||||
writeWantedLockfile(deployDir, deployFiles.lockfile),
|
||||
])
|
||||
|
||||
try {
|
||||
await install.handler({
|
||||
...opts,
|
||||
allProjects: undefined,
|
||||
allProjectsGraph: undefined,
|
||||
selectedProjectsGraph: undefined,
|
||||
rootProjectManifest: deployFiles.manifest,
|
||||
rootProjectManifestDir: deployDir,
|
||||
dir: deployDir,
|
||||
lockfileDir: deployDir,
|
||||
workspaceDir: undefined,
|
||||
virtualStoreDir: undefined,
|
||||
modulesDir: undefined,
|
||||
confirmModulesPurge: false,
|
||||
frozenLockfile: true,
|
||||
hooks: {
|
||||
...opts.hooks,
|
||||
readPackage: [
|
||||
...(opts.hooks?.readPackage ?? []),
|
||||
deployHook,
|
||||
deployCatalogHook.bind(null, opts.catalogs ?? {}),
|
||||
],
|
||||
calculatePnpmfileChecksum: undefined, // the effects of the pnpmfile should already be part of the package snapshots
|
||||
},
|
||||
rawLocalConfig: {
|
||||
...opts.rawLocalConfig,
|
||||
'frozen-lockfile': true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
globalWarn('Deployment with a shared lockfile has failed. If this is a bug, please report it at <https://github.com/pnpm/pnpm/issues>.')
|
||||
globalWarn(`As a workaround, you may add ${FORCE_LEGACY_DEPLOY}=true to .npmrc.`)
|
||||
throw error
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -3,11 +3,20 @@ import path from 'path'
|
||||
import { deploy } from '@pnpm/plugin-commands-deploy'
|
||||
import { assertProject } from '@pnpm/assert-project'
|
||||
import { preparePackages } from '@pnpm/prepare'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { logger, globalWarn } from '@pnpm/logger'
|
||||
import { filterPackagesFromDir } from '@pnpm/workspace.filter-packages-from-dir'
|
||||
import { DEFAULT_OPTS } from './utils'
|
||||
|
||||
test('deploy', async () => {
|
||||
beforeEach(async () => {
|
||||
const logger = await import('@pnpm/logger')
|
||||
jest.spyOn(logger, 'globalWarn')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
test('deploy without existing lockfile', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'project-1',
|
||||
@@ -62,6 +71,8 @@ test('deploy', async () => {
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['deploy'])
|
||||
|
||||
expect(globalWarn).toHaveBeenCalledWith('Shared lockfile not found. Falling back to installing without a lockfile.')
|
||||
|
||||
const project = assertProject(path.resolve('deploy'))
|
||||
project.has('project-2')
|
||||
project.has('is-positive')
|
||||
|
||||
19
releasing/plugin-commands-deploy/test/fixtures/is-positive.patch
vendored
Normal file
19
releasing/plugin-commands-deploy/test/fixtures/is-positive.patch
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
diff --git a/PATCH.txt b/PATCH.txt
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e6d52b1e6644678337868f7b5d6cc6dc0d040891
|
||||
--- /dev/null
|
||||
+++ b/PATCH.txt
|
||||
@@ -0,0 +1 @@
|
||||
+added by pnpm patch-commit
|
||||
diff --git a/package.json b/package.json
|
||||
index 5feb15ba194c74ad48a2ee15abec9887ec1f9e83..27e24feff1bbfc20b3735c23b332bfdc16803362 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -16,6 +16,7 @@
|
||||
"test": "xo && ava"
|
||||
},
|
||||
"files": [
|
||||
+ "PATCH.txt",
|
||||
"index.js"
|
||||
],
|
||||
"keywords": [
|
||||
962
releasing/plugin-commands-deploy/test/shared-lockfile.test.ts
Normal file
962
releasing/plugin-commands-deploy/test/shared-lockfile.test.ts
Normal file
@@ -0,0 +1,962 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import url from 'url'
|
||||
import { deploy } from '@pnpm/plugin-commands-deploy'
|
||||
import { install } from '@pnpm/plugin-commands-installation'
|
||||
import { assertProject } from '@pnpm/assert-project'
|
||||
import { preparePackages } from '@pnpm/prepare'
|
||||
import { type PatchFile, type LockfileFile, type LockfilePackageSnapshot } from '@pnpm/lockfile.types'
|
||||
import { globalWarn } from '@pnpm/logger'
|
||||
import { filterPackagesFromDir } from '@pnpm/workspace.filter-packages-from-dir'
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { type ProjectManifest } from '@pnpm/types'
|
||||
import { DEFAULT_OPTS } from './utils'
|
||||
|
||||
const f = fixtures(__dirname)
|
||||
|
||||
const resolvePathAsUrl = (...paths: string[]): string => url.pathToFileURL(path.resolve(...paths)).toString()
|
||||
|
||||
beforeEach(async () => {
|
||||
const logger = await import('@pnpm/logger')
|
||||
jest.spyOn(logger, 'globalWarn')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
function readPackageJson (manifestDir: string): unknown {
|
||||
const manifestPath = path.resolve(manifestDir, 'package.json')
|
||||
const manifestText = fs.readFileSync(manifestPath, 'utf-8')
|
||||
return JSON.parse(manifestText)
|
||||
}
|
||||
|
||||
test('deploy with a shared lockfile after full install', async () => {
|
||||
const projectNames = ['project-1', 'project-2', 'project-3', 'project-4', 'project-5'] as const
|
||||
|
||||
const preparedManifests: Record<typeof projectNames[number], ProjectManifest> = {
|
||||
'project-1': {
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
files: ['index.js'],
|
||||
dependencies: {
|
||||
'project-2': 'workspace:*',
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'project-3': 'workspace:*',
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
},
|
||||
'project-2': {
|
||||
name: 'project-2',
|
||||
version: '2.0.0',
|
||||
files: ['index.js'],
|
||||
dependencies: {
|
||||
'project-3': 'workspace:*',
|
||||
'project-4': 'workspace:*',
|
||||
'renamed-project-2': 'workspace:project-2@*',
|
||||
'is-odd': '1.0.0',
|
||||
},
|
||||
},
|
||||
'project-3': {
|
||||
name: 'project-3',
|
||||
version: '2.0.0',
|
||||
files: ['index.js'],
|
||||
dependencies: {
|
||||
'project-3': 'workspace:*',
|
||||
'project-5': 'workspace:*',
|
||||
'is-odd': '1.0.0',
|
||||
},
|
||||
},
|
||||
'project-4': {
|
||||
name: 'project-4',
|
||||
version: '0.0.0',
|
||||
},
|
||||
'project-5': {
|
||||
name: 'project-5',
|
||||
version: '0.0.0',
|
||||
},
|
||||
}
|
||||
|
||||
preparePackages(projectNames.map(name => preparedManifests[name]))
|
||||
|
||||
for (const name of projectNames) {
|
||||
fs.writeFileSync(`${name}/test.js`, '', 'utf8')
|
||||
fs.writeFileSync(`${name}/index.js`, '', 'utf8')
|
||||
}
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-1' }])
|
||||
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph: allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
expect(fs.existsSync('pnpm-lock.yaml')).toBeTruthy()
|
||||
|
||||
const expectedDeployManifest: ProjectManifest = {
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'project-2': expect.stringMatching(/^project-2@file:/),
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'project-3': expect.stringMatching(/^project-3@file:/),
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
optionalDependencies: {},
|
||||
pnpm: {},
|
||||
}
|
||||
|
||||
// deploy prod only
|
||||
{
|
||||
fs.rmSync('deploy', { recursive: true, force: true })
|
||||
await deploy.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
dev: false,
|
||||
production: true,
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
sharedWorkspaceLockfile: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['deploy'])
|
||||
|
||||
const project = assertProject(path.resolve('deploy'))
|
||||
project.has('project-2')
|
||||
project.has('is-positive')
|
||||
project.hasNot('project-3')
|
||||
project.hasNot('is-negative')
|
||||
project.hasNot('project-4')
|
||||
project.hasNot('project-5')
|
||||
expect(readPackageJson('deploy')).toStrictEqual(expectedDeployManifest)
|
||||
expect(fs.existsSync('deploy/pnpm-lock.yaml'))
|
||||
expect(fs.existsSync('deploy/index.js')).toBeTruthy()
|
||||
expect(fs.existsSync('deploy/test.js')).toBeFalsy()
|
||||
expect(fs.existsSync('deploy/node_modules/.modules.yaml')).toBeTruthy()
|
||||
const project2Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.startsWith('project-2@'))
|
||||
expect(project2Name).toBeDefined()
|
||||
expect(fs.realpathSync('deploy/node_modules/project-2')).toBe(path.resolve(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-2`))
|
||||
expect(fs.existsSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-2/index.js`)).toBeTruthy()
|
||||
expect(fs.existsSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-2/test.js`)).toBeFalsy()
|
||||
expect(fs.readdirSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules`).sort()).toStrictEqual([
|
||||
'is-odd',
|
||||
'project-2',
|
||||
'project-3',
|
||||
'project-4',
|
||||
'renamed-project-2',
|
||||
])
|
||||
expect(readPackageJson(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-2`)).toStrictEqual(preparedManifests['project-2'])
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules/renamed-project-2`)).toBe(
|
||||
path.resolve(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-2`)
|
||||
)
|
||||
const project3Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.startsWith('project-3@'))
|
||||
expect(project3Name).toBeDefined()
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-3`)).toBe(
|
||||
path.resolve(`deploy/node_modules/.pnpm/${project3Name}/node_modules/project-3`)
|
||||
)
|
||||
expect(fs.readdirSync(`deploy/node_modules/.pnpm/${project3Name}/node_modules`).sort()).toStrictEqual([
|
||||
'is-odd',
|
||||
'project-3',
|
||||
'project-5',
|
||||
])
|
||||
expect(readPackageJson(`deploy/node_modules/.pnpm/${project3Name}/node_modules/project-3`)).toStrictEqual(preparedManifests['project-3'])
|
||||
const project4Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.startsWith('project-4@'))
|
||||
expect(project4Name).toBeDefined()
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-4`)).toBe(
|
||||
path.resolve(`deploy/node_modules/.pnpm/${project4Name}/node_modules/project-4`)
|
||||
)
|
||||
expect(readPackageJson(`deploy/node_modules/.pnpm/${project4Name}/node_modules/project-4`)).toStrictEqual(preparedManifests['project-4'])
|
||||
const project5Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.startsWith('project-5@'))
|
||||
expect(project5Name).toBeDefined()
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project3Name}/node_modules/project-5`)).toBe(
|
||||
path.resolve(`deploy/node_modules/.pnpm/${project5Name}/node_modules/project-5`)
|
||||
)
|
||||
expect(readPackageJson(`deploy/node_modules/.pnpm/${project5Name}/node_modules/project-5`)).toStrictEqual(preparedManifests['project-5'])
|
||||
expect(globalWarn).not.toHaveBeenCalledWith(expect.stringContaining('Falling back to installing without a lockfile'))
|
||||
}
|
||||
|
||||
// deploy all
|
||||
{
|
||||
fs.rmSync('deploy', { recursive: true, force: true })
|
||||
await deploy.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
sharedWorkspaceLockfile: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['deploy'])
|
||||
|
||||
const project = assertProject(path.resolve('deploy'))
|
||||
project.has('project-2')
|
||||
project.has('is-positive')
|
||||
project.has('project-3')
|
||||
project.has('is-negative')
|
||||
project.hasNot('project-4')
|
||||
project.hasNot('project-5')
|
||||
expect(readPackageJson('deploy')).toStrictEqual(expectedDeployManifest)
|
||||
expect(fs.existsSync('deploy/pnpm-lock.yaml'))
|
||||
expect(fs.existsSync('deploy/index.js')).toBeTruthy()
|
||||
expect(fs.existsSync('deploy/test.js')).toBeFalsy()
|
||||
expect(fs.existsSync('deploy/node_modules/.modules.yaml')).toBeTruthy()
|
||||
const project2Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.startsWith('project-2@'))
|
||||
expect(project2Name).toBeDefined()
|
||||
expect(fs.realpathSync('deploy/node_modules/project-2')).toBe(path.resolve(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-2`))
|
||||
expect(fs.existsSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-2/index.js`)).toBeTruthy()
|
||||
expect(fs.existsSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-2/test.js`)).toBeFalsy()
|
||||
expect(fs.readdirSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules`).sort()).toStrictEqual([
|
||||
'is-odd',
|
||||
'project-2',
|
||||
'project-3',
|
||||
'project-4',
|
||||
'renamed-project-2',
|
||||
])
|
||||
const project3Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.startsWith('project-3@'))
|
||||
expect(project3Name).toBeDefined()
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-3`)).toBe(
|
||||
path.resolve(`deploy/node_modules/.pnpm/${project3Name}/node_modules/project-3`)
|
||||
)
|
||||
expect(project3Name).toBeDefined()
|
||||
expect(fs.existsSync(`deploy/node_modules/.pnpm/${project3Name}/node_modules/project-3/index.js`)).toBeTruthy()
|
||||
expect(fs.existsSync(`deploy/node_modules/.pnpm/${project3Name}/node_modules/project-3/test.js`)).toBeFalsy()
|
||||
expect(fs.readdirSync(`deploy/node_modules/.pnpm/${project3Name}/node_modules`).sort()).toStrictEqual([
|
||||
'is-odd',
|
||||
'project-3',
|
||||
'project-5',
|
||||
])
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project3Name}/node_modules/project-3`)).toContain(project3Name)
|
||||
const project4Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.startsWith('project-4@'))
|
||||
expect(project4Name).toBeDefined()
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project2Name}/node_modules/project-4`)).toBe(
|
||||
path.resolve(`deploy/node_modules/.pnpm/${project4Name}/node_modules/project-4`)
|
||||
)
|
||||
const project5Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.startsWith('project-5@'))
|
||||
expect(project5Name).toBeDefined()
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project3Name}/node_modules/project-5`)).toBe(
|
||||
path.resolve(`deploy/node_modules/.pnpm/${project5Name}/node_modules/project-5`)
|
||||
)
|
||||
expect(globalWarn).not.toHaveBeenCalledWith(expect.stringContaining('Falling back to installing without a lockfile'))
|
||||
}
|
||||
})
|
||||
|
||||
test('deploy with a shared lockfile and --prod filter should not fail even if dev workspace package does not exist (#8778)', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'prod-0',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'prod-1': 'workspace:*',
|
||||
},
|
||||
devDependencies: {
|
||||
'dev-0': 'workspace:*',
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'prod-1',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'dev-1': 'workspace:*',
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dev-0',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dev-1',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
},
|
||||
])
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'prod-0' }])
|
||||
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph: allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
expect(fs.existsSync('pnpm-lock.yaml')).toBeTruthy()
|
||||
|
||||
fs.rmSync('dev-0', { recursive: true })
|
||||
fs.rmSync('dev-1', { recursive: true })
|
||||
|
||||
await deploy.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
production: true,
|
||||
dev: false,
|
||||
selectedProjectsGraph,
|
||||
sharedWorkspaceLockfile: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['deploy'])
|
||||
|
||||
const project = assertProject(path.resolve('deploy'))
|
||||
project.has('prod-1')
|
||||
project.hasNot('dev-0')
|
||||
project.hasNot('dev-1')
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.importers).toStrictEqual({
|
||||
'.': {
|
||||
dependencies: {
|
||||
'prod-1': {
|
||||
version: expect.stringMatching(/^prod-1@file:/),
|
||||
specifier: expect.stringMatching(/^prod-1@file:/),
|
||||
},
|
||||
},
|
||||
devDependencies: {
|
||||
'dev-0': {
|
||||
version: expect.stringMatching(/^dev-0@file:/),
|
||||
specifier: expect.stringMatching(/^dev-0@file:/),
|
||||
},
|
||||
'is-negative': {
|
||||
version: '1.0.0',
|
||||
specifier: '1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as LockfileFile['importers'])
|
||||
|
||||
const manifest = readPackageJson('deploy') as ProjectManifest
|
||||
expect(manifest).toStrictEqual({
|
||||
name: 'prod-0',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'prod-1': expect.stringMatching(/^prod-1@file:/),
|
||||
},
|
||||
devDependencies: {
|
||||
'dev-0': expect.stringMatching(/^dev-0@file:/),
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
optionalDependencies: {},
|
||||
pnpm: {},
|
||||
} as ProjectManifest)
|
||||
|
||||
const prod1Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.includes('prod-1@'))
|
||||
expect(prod1Name).toBeDefined()
|
||||
expect(fs.readdirSync(`deploy/node_modules/.pnpm/${prod1Name}/node_modules`).sort()).toStrictEqual(['is-positive', 'prod-1'])
|
||||
expect(fs.realpathSync('deploy/node_modules/prod-1')).toBe(path.resolve(`deploy/node_modules/.pnpm/${prod1Name}/node_modules/prod-1`))
|
||||
})
|
||||
|
||||
test('deploy with a shared lockfile should correctly handle workspace dependencies that depend on the deployed project', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'project-1': 'workspace:*',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'project-0': 'workspace:*',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-0' }])
|
||||
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph: allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
expect(fs.existsSync('pnpm-lock.yaml')).toBeTruthy()
|
||||
|
||||
await deploy.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
sharedWorkspaceLockfile: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['deploy'])
|
||||
|
||||
const project = assertProject(path.resolve('deploy'))
|
||||
project.has('project-1')
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.importers).toStrictEqual({
|
||||
'.': {
|
||||
dependencies: {
|
||||
'project-1': {
|
||||
version: expect.stringMatching(/^project-1@file:/),
|
||||
specifier: expect.stringMatching(/^project-1@file:/),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as LockfileFile['importers'])
|
||||
|
||||
const manifest = readPackageJson('deploy') as ProjectManifest
|
||||
expect(manifest).toStrictEqual({
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'project-1': expect.stringMatching(/^project-1@file:/),
|
||||
},
|
||||
devDependencies: {},
|
||||
optionalDependencies: {},
|
||||
pnpm: {},
|
||||
} as ProjectManifest)
|
||||
|
||||
const project1Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.includes('project-1@'))
|
||||
expect(project1Name).toBeDefined()
|
||||
expect(fs.readdirSync(`deploy/node_modules/.pnpm/${project1Name}/node_modules`).sort()).toStrictEqual(['project-0', 'project-1'])
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project1Name}/node_modules/project-0`)).toBe(path.resolve('deploy'))
|
||||
expect(fs.realpathSync('deploy/node_modules/project-1')).toBe(path.resolve(`deploy/node_modules/.pnpm/${project1Name}/node_modules/project-1`))
|
||||
})
|
||||
|
||||
test('deploy with a shared lockfile should correctly handle package that depends on itself', async () => {
|
||||
preparePackages([
|
||||
{
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'project-0': 'workspace:*',
|
||||
'renamed-workspace': 'workspace:project-0@*',
|
||||
'renamed-linked': 'link:.',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-0' }])
|
||||
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph: allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
expect(fs.existsSync('pnpm-lock.yaml')).toBeTruthy()
|
||||
|
||||
await deploy.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
sharedWorkspaceLockfile: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['deploy'])
|
||||
|
||||
const project = assertProject(path.resolve('deploy'))
|
||||
project.has('project-0')
|
||||
project.has('renamed-workspace')
|
||||
project.has('renamed-linked')
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.importers).toStrictEqual({
|
||||
'.': {
|
||||
dependencies: {
|
||||
'project-0': {
|
||||
version: 'link:.',
|
||||
specifier: 'link:.',
|
||||
},
|
||||
'renamed-workspace': {
|
||||
version: 'link:.',
|
||||
specifier: 'link:.',
|
||||
},
|
||||
'renamed-linked': {
|
||||
version: 'link:.',
|
||||
specifier: 'link:.',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as LockfileFile['importers'])
|
||||
|
||||
const manifest = readPackageJson('deploy') as ProjectManifest
|
||||
expect(manifest).toStrictEqual({
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'project-0': 'link:.',
|
||||
'renamed-workspace': 'link:.',
|
||||
'renamed-linked': 'link:.',
|
||||
},
|
||||
devDependencies: {},
|
||||
optionalDependencies: {},
|
||||
pnpm: {},
|
||||
} as ProjectManifest)
|
||||
|
||||
expect(fs.realpathSync('deploy/node_modules/project-0')).toBe(path.resolve('deploy'))
|
||||
expect(fs.realpathSync('deploy/node_modules/renamed-workspace')).toBe(path.resolve('deploy'))
|
||||
expect(fs.realpathSync('deploy/node_modules/renamed-linked')).toBe(path.resolve('deploy'))
|
||||
})
|
||||
|
||||
test('deploy with a shared lockfile should correctly handle packageExtensions', async () => {
|
||||
const preparedManifests: Record<string, ProjectManifest> = {
|
||||
root: {
|
||||
name: 'root',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
pnpm: {
|
||||
packageExtensions: {
|
||||
'is-positive': {
|
||||
dependencies: {
|
||||
'is-odd': '1.0.0',
|
||||
'link-to-project-0': 'link:project-0',
|
||||
'link-to-project-1': 'link:project-1',
|
||||
'project-0': 'workspace:*',
|
||||
'project-1': 'workspace:*',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'project-0': {
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'project-1': 'workspace:*',
|
||||
},
|
||||
},
|
||||
'project-1': {
|
||||
name: 'project-1',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: preparedManifests.root,
|
||||
},
|
||||
preparedManifests['project-0'],
|
||||
preparedManifests['project-1'],
|
||||
])
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-0' }])
|
||||
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph: allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
expect(fs.existsSync('pnpm-lock.yaml')).toBeTruthy()
|
||||
|
||||
await deploy.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
sharedWorkspaceLockfile: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['deploy'])
|
||||
|
||||
const project = assertProject(path.resolve('deploy'))
|
||||
project.has('project-1')
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile).toHaveProperty(['snapshots', 'is-positive@1.0.0'], {
|
||||
dependencies: {
|
||||
'is-odd': '1.0.0',
|
||||
'link-to-project-0': 'link:.',
|
||||
'link-to-project-1': expect.stringMatching(/^project-1@file:/),
|
||||
'project-0': 'link:.',
|
||||
'project-1': expect.stringMatching(/^project-1@file:/),
|
||||
},
|
||||
} as LockfilePackageSnapshot)
|
||||
|
||||
const manifest = readPackageJson('deploy') as ProjectManifest
|
||||
expect(manifest).toStrictEqual({
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'project-1': expect.stringMatching(/^project-1@file:/),
|
||||
},
|
||||
devDependencies: {},
|
||||
optionalDependencies: {},
|
||||
pnpm: {},
|
||||
} as ProjectManifest)
|
||||
|
||||
const project1Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.includes('project-1@'))
|
||||
expect(project1Name).toBeDefined()
|
||||
|
||||
expect(fs.realpathSync('deploy/node_modules/.pnpm/is-positive@1.0.0/node_modules/is-odd'))
|
||||
.toBe(path.resolve('deploy/node_modules/.pnpm/is-odd@1.0.0/node_modules/is-odd'))
|
||||
expect(fs.realpathSync('deploy/node_modules/.pnpm/is-positive@1.0.0/node_modules/link-to-project-0')).toBe(path.resolve('deploy'))
|
||||
expect(fs.realpathSync('deploy/node_modules/.pnpm/is-positive@1.0.0/node_modules/link-to-project-1'))
|
||||
.toBe(path.resolve(`deploy/node_modules/.pnpm/${project1Name}/node_modules/project-1`))
|
||||
expect(fs.realpathSync('deploy/node_modules/.pnpm/is-positive@1.0.0/node_modules/project-0')).toBe(path.resolve('deploy'))
|
||||
expect(fs.realpathSync('deploy/node_modules/.pnpm/is-positive@1.0.0/node_modules/project-1'))
|
||||
.toBe(path.resolve(`deploy/node_modules/.pnpm/${project1Name}/node_modules/project-1`))
|
||||
|
||||
expect(readPackageJson('deploy/node_modules/.pnpm/is-positive@1.0.0/node_modules/link-to-project-0')).toStrictEqual(manifest)
|
||||
expect(readPackageJson('deploy/node_modules/.pnpm/is-positive@1.0.0/node_modules/link-to-project-1')).toStrictEqual(preparedManifests['project-1'])
|
||||
expect(readPackageJson('deploy/node_modules/.pnpm/is-positive@1.0.0/node_modules/project-0')).toStrictEqual(manifest)
|
||||
expect(readPackageJson('deploy/node_modules/.pnpm/is-positive@1.0.0/node_modules/project-1')).toStrictEqual(preparedManifests['project-1'])
|
||||
})
|
||||
|
||||
test('deploy with a shared lockfile should correctly handle patchedDependencies', async () => {
|
||||
const preparedManifests: Record<string, ProjectManifest> = {
|
||||
root: {
|
||||
name: 'root',
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
pnpm: {
|
||||
patchedDependencies: {
|
||||
'is-positive': '__patches__/is-positive.patch',
|
||||
},
|
||||
},
|
||||
},
|
||||
'project-0': {
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'project-1': 'workspace:*',
|
||||
},
|
||||
},
|
||||
'project-1': {
|
||||
name: 'project-1',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
preparePackages([
|
||||
{
|
||||
location: '.',
|
||||
package: preparedManifests.root,
|
||||
},
|
||||
preparedManifests['project-0'],
|
||||
preparedManifests['project-1'],
|
||||
])
|
||||
|
||||
f.copy('is-positive.patch', '__patches__/is-positive.patch')
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-0' }])
|
||||
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph: allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
expect(fs.existsSync('pnpm-lock.yaml')).toBeTruthy()
|
||||
|
||||
await deploy.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
sharedWorkspaceLockfile: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['deploy'])
|
||||
|
||||
const project = assertProject(path.resolve('deploy'))
|
||||
project.has('project-1')
|
||||
|
||||
const lockfile = project.readLockfile()
|
||||
expect(lockfile.patchedDependencies).toStrictEqual({
|
||||
'is-positive': {
|
||||
hash: expect.any(String),
|
||||
path: '../__patches__/is-positive.patch',
|
||||
},
|
||||
} as Record<string, PatchFile>)
|
||||
|
||||
const patchFile = lockfile.patchedDependencies['is-positive']
|
||||
|
||||
const manifest = readPackageJson('deploy') as ProjectManifest
|
||||
expect(manifest).toStrictEqual({
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'project-1': expect.stringMatching(/^project-1@file:/),
|
||||
},
|
||||
devDependencies: {},
|
||||
optionalDependencies: {},
|
||||
pnpm: {
|
||||
patchedDependencies: {
|
||||
'is-positive': '../__patches__/is-positive.patch',
|
||||
},
|
||||
},
|
||||
} as ProjectManifest)
|
||||
|
||||
const project1Name = fs.readdirSync('deploy/node_modules/.pnpm').find(name => name.includes('project-1@'))
|
||||
expect(project1Name).toBeDefined()
|
||||
if (process.platform !== 'win32') {
|
||||
expect(fs.realpathSync(`deploy/node_modules/.pnpm/${project1Name}/node_modules/is-positive`)).toBe(
|
||||
path.resolve(`deploy/node_modules/.pnpm/is-positive@1.0.0_patch_hash=${patchFile.hash}/node_modules/is-positive`)
|
||||
)
|
||||
}
|
||||
expect(
|
||||
fs.readFileSync(`deploy/node_modules/.pnpm/${project1Name}/node_modules/is-positive/PATCH.txt`, 'utf-8')
|
||||
.trim()
|
||||
).toBe('added by pnpm patch-commit')
|
||||
})
|
||||
|
||||
test('deploy with a shared lockfile that has peer dependencies suffix in workspace package dependency paths', async () => {
|
||||
const preparedManifests: Record<string, ProjectManifest> = {
|
||||
'project-0': {
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'project-1': 'workspace:*',
|
||||
},
|
||||
peerDependencies: {
|
||||
'project-1': '*',
|
||||
'project-2': '*',
|
||||
},
|
||||
},
|
||||
'project-1': {
|
||||
name: 'project-1',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
'project-2': 'workspace:*',
|
||||
},
|
||||
peerDependencies: {
|
||||
'is-negative': '>=1.0.0',
|
||||
'project-2': '*',
|
||||
},
|
||||
},
|
||||
'project-2': {
|
||||
name: 'project-2',
|
||||
version: '0.0.0',
|
||||
peerDependencies: {
|
||||
'is-positive': '>=1.0.0',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
preparePackages(['project-0', 'project-1', 'project-2'].map(name => ({
|
||||
location: `packages/${name}`,
|
||||
package: preparedManifests[name],
|
||||
})))
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(process.cwd(), [{ namePattern: 'project-0' }])
|
||||
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph: allProjectsGraph,
|
||||
dedupeInjectedDeps: false,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
expect(assertProject('.').readLockfile()).toMatchObject({
|
||||
importers: {
|
||||
'packages/project-0': {
|
||||
dependencies: {
|
||||
'project-1': {
|
||||
version: 'file:packages/project-1(is-negative@1.0.0)(project-2@file:packages/project-2(is-positive@1.0.0))',
|
||||
},
|
||||
'project-2': {
|
||||
version: 'file:packages/project-2(is-positive@1.0.0)',
|
||||
},
|
||||
},
|
||||
},
|
||||
'packages/project-1': {
|
||||
dependencies: {
|
||||
'project-2': {
|
||||
version: 'file:packages/project-2(is-positive@1.0.0)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
'project-1@file:packages/project-1': {
|
||||
resolution: {
|
||||
type: 'directory',
|
||||
directory: 'packages/project-1',
|
||||
},
|
||||
},
|
||||
'project-2@file:packages/project-2': {
|
||||
resolution: {
|
||||
type: 'directory',
|
||||
directory: 'packages/project-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
snapshots: {
|
||||
'project-1@file:packages/project-1(is-negative@1.0.0)(project-2@file:packages/project-2(is-positive@1.0.0))': {
|
||||
dependencies: {
|
||||
'project-2': 'file:packages/project-2(is-positive@1.0.0)',
|
||||
},
|
||||
},
|
||||
'project-2@file:packages/project-2(is-positive@1.0.0)': {},
|
||||
},
|
||||
})
|
||||
|
||||
await deploy.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
selectedProjectsGraph,
|
||||
sharedWorkspaceLockfile: true,
|
||||
lockfileDir: process.cwd(),
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['deploy'])
|
||||
|
||||
const project = assertProject(path.resolve('deploy'))
|
||||
project.has('project-1')
|
||||
project.has('project-2')
|
||||
|
||||
expect(project.readLockfile()).toMatchObject({
|
||||
importers: {
|
||||
'.': {
|
||||
dependencies: {
|
||||
'project-1': {
|
||||
specifier: `project-1@${resolvePathAsUrl('packages/project-1')}(is-negative@1.0.0)(project-2@file:packages/project-2(is-positive@1.0.0))`,
|
||||
version: `project-1@${resolvePathAsUrl('packages/project-1')}(is-negative@1.0.0)(project-2@file:packages/project-2(is-positive@1.0.0))`,
|
||||
},
|
||||
'project-2': {
|
||||
specifier: `project-2@${resolvePathAsUrl('packages/project-2')}(is-positive@1.0.0)`,
|
||||
version: `project-2@${resolvePathAsUrl('packages/project-2')}(is-positive@1.0.0)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
packages: {
|
||||
[`project-1@${resolvePathAsUrl('packages/project-1')}`]: {
|
||||
resolution: {
|
||||
type: 'directory',
|
||||
directory: '../packages/project-1',
|
||||
},
|
||||
},
|
||||
[`project-2@${resolvePathAsUrl('packages/project-2')}`]: {
|
||||
resolution: {
|
||||
type: 'directory',
|
||||
directory: '../packages/project-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
snapshots: {
|
||||
[`project-1@${resolvePathAsUrl('packages/project-1')}(is-negative@1.0.0)(project-2@file:packages/project-2(is-positive@1.0.0))`]: {
|
||||
dependencies: {
|
||||
'project-2': `project-2@${resolvePathAsUrl('packages/project-2')}(is-positive@1.0.0)`,
|
||||
},
|
||||
},
|
||||
[`project-2@${resolvePathAsUrl('packages/project-2')}(is-positive@1.0.0)`]: {},
|
||||
},
|
||||
})
|
||||
|
||||
expect(readPackageJson('deploy')).toStrictEqual({
|
||||
name: 'project-0',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
'project-1': `project-1@${resolvePathAsUrl('packages/project-1')}(is-negative@1.0.0)(project-2@file:packages/project-2(is-positive@1.0.0))`,
|
||||
'project-2': `project-2@${resolvePathAsUrl('packages/project-2')}(is-positive@1.0.0)`,
|
||||
},
|
||||
devDependencies: {},
|
||||
optionalDependencies: {},
|
||||
pnpm: {},
|
||||
} as ProjectManifest)
|
||||
|
||||
expect(readPackageJson('deploy/node_modules/project-1')).toStrictEqual(preparedManifests['project-1'])
|
||||
expect(readPackageJson('deploy/node_modules/project-2')).toStrictEqual(preparedManifests['project-2'])
|
||||
|
||||
const project1Names = fs.readdirSync('deploy/node_modules/.pnpm').filter(name => name.includes('project-1@'))
|
||||
expect(project1Names).not.toStrictEqual([])
|
||||
for (const name of project1Names) {
|
||||
expect(readPackageJson(`deploy/node_modules/.pnpm/${name}/node_modules/project-1`)).toStrictEqual(preparedManifests['project-1'])
|
||||
}
|
||||
|
||||
const project2Names = fs.readdirSync('deploy/node_modules/.pnpm').filter(name => name.includes('project-2@'))
|
||||
expect(project2Names).not.toStrictEqual([])
|
||||
for (const name of project2Names) {
|
||||
expect(readPackageJson(`deploy/node_modules/.pnpm/${name}/node_modules/project-2`)).toStrictEqual(preparedManifests['project-2'])
|
||||
}
|
||||
})
|
||||
@@ -15,6 +15,9 @@
|
||||
{
|
||||
"path": "../../__utils__/prepare"
|
||||
},
|
||||
{
|
||||
"path": "../../__utils__/test-fixtures"
|
||||
},
|
||||
{
|
||||
"path": "../../catalogs/resolver"
|
||||
},
|
||||
@@ -27,6 +30,9 @@
|
||||
{
|
||||
"path": "../../cli/common-cli-options-help"
|
||||
},
|
||||
{
|
||||
"path": "../../config/config"
|
||||
},
|
||||
{
|
||||
"path": "../../fetching/directory-fetcher"
|
||||
},
|
||||
@@ -36,6 +42,9 @@
|
||||
{
|
||||
"path": "../../fs/is-empty-dir-or-nothing"
|
||||
},
|
||||
{
|
||||
"path": "../../lockfile/fs"
|
||||
},
|
||||
{
|
||||
"path": "../../lockfile/types"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user