@vzn/vx-lockfile 0.0.636 → 0.0.637
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -30
- package/package.json +4 -6
- package/src/bun.ts +88 -92
- package/src/index.ts +349 -73
- package/src/json.ts +6 -22
- package/src/npm.ts +23 -48
- package/src/pnpm.ts +19 -109
- package/src/posix.ts +14 -0
- package/src/yarn.ts +92 -93
- package/src/blocks.ts +0 -83
- package/src/prune.ts +0 -400
- package/src/scope.ts +0 -17
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Website: <https://vznjs.github.io/vx/> · this plugin: <https://vznjs.github.io/vx/plugins/vx-lockfile/> · source: <https://github.com/vznjs/vx>
|
|
4
4
|
|
|
5
|
-
Lockfile plugins for [`@vzn/vx`](https://vznjs.github.io/vx/): `pnpm()`, `bun()`, `npm()` and `yarn()`. Each keys every task on its **own project's resolved dependency closure** from the package manager's lockfile, not on the whole file. `pnpm update foo` (or `bun add`, `npm install`, `yarn up`) re-keys exactly the projects that reach `foo` — through their dependencies, transitively, and through workspace links — and `vx run … --affected` selects the same projects. Zero dependencies: Bun's own YAML and JSONC parsers, and a classic-yarn reader.
|
|
5
|
+
Lockfile plugins for [`@vzn/vx`](https://vznjs.github.io/vx/): `pnpm()`, `bun()`, `npm()` and `yarn()`. Each keys every task on its **own project's resolved dependency closure** from the package manager's lockfile, not on the whole file. `pnpm update foo` (or `bun add`, `npm install`, `yarn up`) re-keys exactly the projects that reach `foo` — through their dependencies, transitively, and through workspace links — and `vx run … --affected` selects the same projects. Each also gives vx the **projects, names and edges the lockfile records** (`discover`), adds each package's `package.json` to its cached tasks' inputs, and folds the manager's install settings into every task. Zero dependencies: Bun's own YAML and JSONC parsers, and a classic-yarn reader.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Core keys no package file: without a plugin no lockfile, install setting or `package.json` reaches any cache key, so an install re-keys nothing and a build made against the old `node_modules` replays. A JS workspace declares one.
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ npm install -D @vzn/vx @vzn/vx-lockfile # or: pnpm add -D -w · yarn add -D (-
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
|
-
// vx
|
|
16
|
+
// .vx/workspace.ts
|
|
17
17
|
import { defineWorkspace } from '@vzn/vx/config'
|
|
18
18
|
import { pnpm } from '@vzn/vx-lockfile' // or bun, npm, yarn
|
|
19
19
|
|
|
@@ -22,56 +22,61 @@ export default defineWorkspace({
|
|
|
22
22
|
})
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
That is the whole setup. The plugin **claims** its lockfile (`VxPlugin.fingerprint`),
|
|
25
|
+
That is the whole setup. The plugin **claims** its lockfile and its manager's install settings (`VxPlugin.fingerprint`), and its `key` hook folds one digest per project plus each settings file present. `vx why <task>` names the material as `plugin @vzn/vx-lockfile/pnpm` (or `/bun`, `/npm`, `/yarn`) and `plugin @vzn/vx-lockfile/.npmrc` (or the settings file that moved).
|
|
26
26
|
|
|
27
|
-
| Option | Values | Meaning
|
|
28
|
-
| ------- | ------------------------------------ |
|
|
29
|
-
| `scope` | `'project'` (default), `'workspace'` | `project`: each task folds its own project's closure. `workspace`: the whole file
|
|
27
|
+
| Option | Values | Meaning |
|
|
28
|
+
| ------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `scope` | `'project'` (default), `'workspace'` | `project`: each task folds its own project's closure. `workspace`: the whole file into every task — the coarse key, for a workspace not yet ready to trust the precision. |
|
|
30
30
|
|
|
31
31
|
## What a project's digest covers
|
|
32
32
|
|
|
33
33
|
Every package the project can reach, by resolved identity (name, version, integrity), and under bun and npm by the name it installs under, not where it sits, so a re-hoist of one version re-keys nothing, plus the install-wide material every project folds — and **the root package's own closure**. The root's `dependencies` and `devDependencies` reach every task: their bins run from the root `node_modules/.bin`, which vx puts on every task's PATH, and Node's resolution walks up to the root `node_modules` (`@types/*`, a plugin a root tool's config names). So a root devDependency bump (`pnpm add -Dw oxlint@next`) re-keys every project and `--affected` selects every project; a package only project A reaches still moves only A. A root `workspace:` dependency folds that package's closure into every project too. Per manager:
|
|
34
34
|
|
|
35
35
|
- **pnpm** (`pnpm-lock.yaml`, lockfile v5, v6, v9): importers → snapshots, transitively; each package by name, version **and resolved peers** (`foo@1(react@18)` is not `foo@1(react@19)`), resolution and any `patchedDependencies` entry; `link:` folds the linked importer's reach, and a `file:` directory dependency folds its own closure (v9 keys it `name@file:…`, v5/v6 by the bare `file:…`); every other top-level field but `overrides` and the catalogs, whose effect is the snapshot an importer reaches (`settings`, `packageExtensionsChecksum`, `pnpmfileChecksum`, `ignoredOptionalDependencies`, v6's `onlyBuiltDependencies`, and any field pnpm adds later) and the lockfile version into every project. A multi-document lockfile (pnpm 10.x and 11 write the env lockfile — `configDependencies`, the package manager's own install — ahead of the project's) reads its last document as the lockfile and folds the ones before it into every project.
|
|
36
|
-
- **bun** (`bun.lock`): Bun's hoisted layout — a dependency `d` of the package at path `p` is `p/d` when that key exists, else the nearest ancestor's, else the root's, so a nested version counts for the package it is nested under and no other; `workspace:` entries fold the linked package's reach; each `patchedDependencies` entry and the CONTENT of its patch file (bun.lock records only its path) with the package it patches, so a patch edit re-keys only the projects reaching it; every top-level field but `workspaces`, `packages`, the catalogs, `overrides` and `patchedDependencies` (`trustedDependencies`, and any field Bun adds later), and any patch naming no entry, into every project — a catalog bump reaches only the projects whose `catalog:` dependency resolves anew, through the `packages` entry it installs.
|
|
37
|
-
- **npm** (`package-lock.json
|
|
38
|
-
- **yarn** (`yarn.lock`): berry (yarn 2+) resolves `name@npm:range` descriptors to entries, workspaces included, each entry folding every field but its dependency lists (so the root workspace's `dependenciesMeta` reaches every project), `__metadata`'s `version` and `cacheKey` into every project. A Yarn 4 catalog dependency (`catalog:`, `catalog:<name>`) is recorded as that literal and its range lives in `.yarnrc.yml`, so it reaches every entry of its package: a bump of any of them re-keys the workspace. So does a descriptor the file does not key: a root `resolutions` override (to a `patch:`, or another range) leaves the original descriptor out of `yarn.lock`, and the entry installed in its place is reached through the package's name, so a patch edit re-keys the workspaces that depend on it. A path range (`portal:`, `file:`, `link:`) reaches the entry keyed bound to the package naming it (`::locator=…`), not another workspace's of the same name. Yarn's builtin compat patches (`resolve`, `typescript`, `fsevents`) are reached too: the workspace asks for the plain descriptor, and the patched entry is the one Yarn installs. `.yarnrc.yml`
|
|
36
|
+
- **bun** (`bun.lock`; a `bun.lockb` alone is refused — it is binary — naming `bun install --save-text-lockfile`, which writes `bun.lock`): Bun's hoisted layout — a dependency `d` of the package at path `p` is `p/d` when that key exists, else the nearest ancestor's, else the root's, so a nested version counts for the package it is nested under and no other; `workspace:` entries fold the linked package's reach; each `patchedDependencies` entry and the CONTENT of its patch file (bun.lock records only its path) with the package it patches, so a patch edit re-keys only the projects reaching it; every top-level field but `workspaces`, `packages`, the catalogs, `overrides` and `patchedDependencies` (`trustedDependencies`, and any field Bun adds later), and any patch naming no entry, into every project — a catalog bump reaches only the projects whose `catalog:` dependency resolves anew, through the `packages` entry it installs.
|
|
37
|
+
- **npm** (`npm-shrinkwrap.json` when present, as npm reads it, else `package-lock.json`; lockfileVersion 2 and 3): the `packages` map — `p/node_modules/d`, then each ancestor directory's (a workspace nested in another's directory resolves through the outer one's `node_modules`, as Node does), then `node_modules/d`; a `link: true` entry folds its target workspace's reach; root `overrides` reach a project through the entries they force (npm 10 does not write them to the lockfile). Version 1 (npm 6) has no `packages` map and is refused.
|
|
38
|
+
- **yarn** (`yarn.lock`): berry (yarn 2+) resolves `name@npm:range` descriptors to entries, workspaces included, each entry folding every field but its dependency lists (so the root workspace's `dependenciesMeta` reaches every project), `__metadata`'s `version` and `cacheKey` into every project. A Yarn 4 catalog dependency (`catalog:`, `catalog:<name>`) is recorded as that literal and its range lives in `.yarnrc.yml`, so it reaches every entry of its package: a bump of any of them re-keys the workspace. So does a descriptor the file does not key: a root `resolutions` override (to a `patch:`, or another range) leaves the original descriptor out of `yarn.lock`, and the entry installed in its place is reached through the package's name, so a patch edit re-keys the workspaces that depend on it. A path range (`portal:`, `file:`, `link:`) reaches the entry keyed bound to the package naming it (`::locator=…`), not another workspace's of the same name. Yarn's builtin compat patches (`resolve`, `typescript`, `fsevents`) are reached too: the workspace asks for the plain descriptor, and the patched entry is the one Yarn installs. `yarn()` claims `.yarnrc.yml` and folds it into every task, so a catalog edit re-keys every project, as a `pnpm-workspace.yaml` edit does under `pnpm()`. Classic (yarn 1) records no workspaces, so it yields one digest for the root that every project folds — coarse, and honest about what the file records — and no project edges (below).
|
|
39
39
|
|
|
40
40
|
A project the lockfile has no entry for folds the root's digest alone — the only `node_modules` it can resolve from. A phantom dependency (imported, never declared by the project or the root — a sibling's package hoisted to the root) is not in any closure; declare it.
|
|
41
41
|
|
|
42
42
|
A lockfile the parser cannot read **refuses the run**, naming the file, the reason and the install that regenerates it. That is deliberate: the alternative to reading the lockfile is keying on nothing, and a key that is missing material is a stale hit waiting to happen. Under `--affected` the refusal also says which side could not be read — the working tree's copy, or the one at the base ref (a lockfile-migration commit hits the second). A lockfile holding git merge conflict markers is refused the same way: it names two installs at once, and yarn classic's format parsed one side of it without a word.
|
|
43
43
|
|
|
44
|
-
##
|
|
44
|
+
## Install settings
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
Files no lockfile records lay `node_modules` out anew under a byte-identical lockfile (pnpm's `node-linker`, bun's `linker = "isolated"`, a registry in `.npmrc`, Yarn 4's catalogs). Each plugin claims its manager's and folds each present file whole into every task; a change to one selects every project under `--affected`:
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
| Plugin | Settings |
|
|
49
|
+
| -------- | ------------------------------- |
|
|
50
|
+
| `pnpm()` | `pnpm-workspace.yaml`, `.npmrc` |
|
|
51
|
+
| `bun()` | `bunfig.toml`, `.npmrc` |
|
|
52
|
+
| `npm()` | `.npmrc` |
|
|
53
|
+
| `yarn()` | `.yarnrc.yml` |
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
A file has one claimant, so two of these plugins that share a settings file (`pnpm()` and `bun()` both claim `.npmrc`) cannot be declared together.
|
|
51
56
|
|
|
52
|
-
## `
|
|
57
|
+
## `package.json` as an input
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
Every cached task of a project the lockfile lists gets its literal `package.json` appended to `cache.inputs.files` (the `project` stage), so an edit to a manifest re-keys that project's tasks and, through `^` keys, its dependants. A task with no `cache` block stays uncached; one whose inputs already name `package.json` is left alone. A task that rewrites its own `package.json` with the same bytes (tsdown) keeps its hits under `inputStability: { strategy: 'mutable' }`.
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
vx prune <project...> [--out-dir <dir>] [--docker] [--production]
|
|
58
|
-
```
|
|
61
|
+
## Project edges
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
`discover` gives vx each project's edges as the lockfile records them, read from the same memo as the digests:
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
RUN bunx vx run app#build
|
|
67
|
-
```
|
|
65
|
+
- **deps** (order `^task`, reach `--filter …` and `--affected`): a dependency the lockfile resolves to a workspace package — pnpm `link:`, bun's `workspace:` entries (a `workspace:` path or alias, and an `npm:` alias Bun links to the workspace package, by the spec it records), npm's `link: true` entries, yarn berry's workspace entries and `link:` / `portal:` paths.
|
|
66
|
+
- **peers**: a peer with no installed entry of the same name that resolves to a workspace package orders the build too, unless it would close a cycle through the order edges; then it is reach only (in project-name order, so the edge kept is stable). pnpm records an auto-installed peer under `dependencies`, so `pnpm()` asks the project's `package.json` which of those entries are peers; yarn berry resolves a peer to the workspace of its name, which Yarn hoists.
|
|
67
|
+
- Each importer with a `package.json` is a project: one with no vx config joins as a project a plugin added (its tasks come from the `project` stage, else the default `build`). The root importer is the exception: it is a project only by its own vx config. An importer with no `package.json`, and an edge to one, is skipped. A project is named by its `package.json` `name` unless its config names it, or another importer's `package.json` shares the name; then its path names it.
|
|
68
|
+
- A yarn classic (v1) lockfile records no workspaces: `yarn()` gives no edges and warns `yarn.lock: yarn classic (v1) records no workspaces, so yarn() gives vx no project edges — upgrade to Yarn 2+`.
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
These edges are what was installed, so they differ from a guess over manifest ranges where the manager decided otherwise: pnpm installs a plain range (`^2.0.0`, `*`, `catalog:`) from the registry unless it is `workspace:`, yarn packs a `file:` directory as a copy, bun installs `dependencies` over a devDependency of the same key. They are the only package edges vx has: core reads none from a `package.json`, so a workspace whose tasks use `^build` declares one of these plugins (or another that returns `deps`), and without one vx refuses the `^` naming the fix. A dependency an install adds or removes reaches `--affected` as this lockfile's change.
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
## Cost
|
|
73
|
+
|
|
74
|
+
The claim, the per-project key, the memo and the `--affected` diff are core's `lockfileClaim`; this package is the parsers, and they are internal: it exports `pnpm`, `bun`, `npm`, `yarn` and `LockfileOptions`. A lockfile is parsed **once per content**: the digests and each importer's edges are memoised under the cache dir (`lockfile-claims/<file>.json`) by the file's xxh3 and the claimant, so a warm run pays one read, one hash and one small JSON read — never a parse — and the read happens once per run, not per task. Under `scope: 'workspace'` the file is parsed for its edges alone, once per content, so an unreadable lockfile is refused there too. The settings files are read once per run. The digest is one hash per strongly connected component of the dependency graph (lockfiles carry cycles), children first, so a 1000-importer / 3000-package lockfile digests in ~20 ms when it does change.
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
## `--affected`
|
|
77
|
+
|
|
78
|
+
`vx run test --affected=origin/main` after a lockfile change used to select every project. With a plugin declared at `scope: 'project'`, core hands it the lockfile at the base ref and in the working tree; it digests both and names the projects whose digest moved. At `scope: 'workspace'` a change still selects every project. A lockfile that appeared or was deleted still selects everything — every project's `node_modules` is in question.
|
|
74
79
|
|
|
75
80
|
## Testing
|
|
76
81
|
|
|
77
|
-
`bun test` covers each parser's digests (transitive bumps, nested versions, workspace links, peer suffixes, patches, install-wide knobs, cycles, key order, aliases, refusals)
|
|
82
|
+
`bun test` covers each parser's digests (transitive bumps, nested versions, workspace links, peer suffixes, patches, install-wide knobs, cycles, key order, aliases, refusals), `discover`'s edges per manager (`discover.test.ts`) and the edges core's manifest graph read over the same real installs, recorded when core stopped reading them (`discover-parity.test.ts`), the settings claims, and `vx run` / `vx why` / `--affected` end to end. vx's own repository declares `bun()`.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-lockfile",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.637",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "Lockfile plugins for vx — pnpm(), bun(), npm(), yarn(): each keys every task on its project's own resolved dependency closure from the package manager's lockfile, so a lockfile change re-keys only the projects it reaches; --affected follows the same answer
|
|
5
|
+
"description": "Lockfile plugins for vx — pnpm(), bun(), npm(), yarn(): each keys every task on its project's own resolved dependency closure from the package manager's lockfile, so a lockfile change re-keys only the projects it reaches; --affected follows the same answer.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"vx",
|
|
8
8
|
"vx-plugin",
|
|
@@ -11,9 +11,7 @@
|
|
|
11
11
|
"pnpm",
|
|
12
12
|
"bun",
|
|
13
13
|
"npm",
|
|
14
|
-
"yarn"
|
|
15
|
-
"prune",
|
|
16
|
-
"docker"
|
|
14
|
+
"yarn"
|
|
17
15
|
],
|
|
18
16
|
"type": "module",
|
|
19
17
|
"main": "./src/index.ts",
|
|
@@ -37,7 +35,7 @@
|
|
|
37
35
|
"access": "public"
|
|
38
36
|
},
|
|
39
37
|
"peerDependencies": {
|
|
40
|
-
"@vzn/vx": "^0.0.
|
|
38
|
+
"@vzn/vx": "^0.0.637"
|
|
41
39
|
},
|
|
42
40
|
"repository": {
|
|
43
41
|
"type": "git",
|
package/src/bun.ts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Bun.JSONC is the parser: no dependency, and the file is Bun's own.
|
|
12
12
|
|
|
13
|
-
import { reachDigests } from '@vzn/vx'
|
|
14
|
-
import { depsOf,
|
|
15
|
-
import
|
|
13
|
+
import { reachDigests, type LockfileImporter } from '@vzn/vx'
|
|
14
|
+
import { depsOf, peerOnly, record, type Json } from './json.js'
|
|
15
|
+
import { joinPosix } from './posix.js'
|
|
16
16
|
|
|
17
17
|
export interface Lockfile {
|
|
18
18
|
readonly version: string
|
|
@@ -20,6 +20,10 @@ export interface Lockfile {
|
|
|
20
20
|
readonly workspaces: ReadonlyMap<string, ReadonlyMap<string, string>>
|
|
21
21
|
/** workspace package name → its dir */
|
|
22
22
|
readonly workspaceDirs: ReadonlyMap<string, string>
|
|
23
|
+
/** root-relative dir → its installed dependencies (`dependencies` win, as Bun 1.4 installs them) */
|
|
24
|
+
readonly workspaceInstalled: ReadonlyMap<string, ReadonlyMap<string, string>>
|
|
25
|
+
/** root-relative dir → the peers it installs no entry for */
|
|
26
|
+
readonly workspacePeers: ReadonlyMap<string, ReadonlySet<string>>
|
|
23
27
|
/** node_modules path → the package there */
|
|
24
28
|
readonly packages: ReadonlyMap<string, Entry>
|
|
25
29
|
/** `patchedDependencies`: `name@version` (or `name`) → the patch file's path */
|
|
@@ -64,10 +68,17 @@ export function parseLockfile(text: string): Lockfile {
|
|
|
64
68
|
}
|
|
65
69
|
const workspaces = new Map<string, ReadonlyMap<string, string>>()
|
|
66
70
|
const workspaceDirs = new Map<string, string>()
|
|
71
|
+
const workspaceInstalled = new Map<string, ReadonlyMap<string, string>>()
|
|
72
|
+
const workspacePeers = new Map<string, ReadonlySet<string>>()
|
|
67
73
|
for (const [dir, manifest] of Object.entries(record(d['workspaces']) ?? {})) {
|
|
68
74
|
const m = record(manifest) ?? {}
|
|
69
75
|
const key = dir === '' ? '.' : dir
|
|
70
76
|
workspaces.set(key, depsOf(m))
|
|
77
|
+
workspaceInstalled.set(
|
|
78
|
+
key,
|
|
79
|
+
depsOf(m, ['devDependencies', 'optionalDependencies', 'dependencies']),
|
|
80
|
+
)
|
|
81
|
+
workspacePeers.set(key, peerOnly(m))
|
|
71
82
|
if (typeof m['name'] === 'string') workspaceDirs.set(m['name'], key)
|
|
72
83
|
}
|
|
73
84
|
const packages = new Map<string, Entry>()
|
|
@@ -97,6 +108,8 @@ export function parseLockfile(text: string): Lockfile {
|
|
|
97
108
|
version: String(d['lockfileVersion']),
|
|
98
109
|
workspaces,
|
|
99
110
|
workspaceDirs,
|
|
111
|
+
workspaceInstalled,
|
|
112
|
+
workspacePeers,
|
|
100
113
|
packages,
|
|
101
114
|
patches,
|
|
102
115
|
global,
|
|
@@ -134,6 +147,77 @@ function packageLevels(path: string): string[] {
|
|
|
134
147
|
return out
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Each workspace's dependencies that resolve to a `workspace:` package: an
|
|
152
|
+
* installed entry is a dep, a peer with none of the same name a peer. A
|
|
153
|
+
* `workspace:` path or alias (`workspace:../b`, `workspace:@s/b@^1`) names
|
|
154
|
+
* its target, which Bun links though `packages` holds no nested entry for
|
|
155
|
+
* it, and so does an `npm:` alias no entry installs under its own name (a
|
|
156
|
+
* satisfied one: Bun links the workspace package); anything else resolves
|
|
157
|
+
* through Bun's layout.
|
|
158
|
+
*/
|
|
159
|
+
export function importerLinks(lock: Lockfile): ReadonlyMap<string, LockfileImporter> {
|
|
160
|
+
const out = new Map<string, LockfileImporter>()
|
|
161
|
+
const viaLayout = (from: string, name: string): string | undefined => {
|
|
162
|
+
const key = resolve(lock, from, name)
|
|
163
|
+
const id = key === undefined ? '' : lock.packages.get(key)!.id
|
|
164
|
+
const at = id.indexOf('@workspace:')
|
|
165
|
+
return at === -1 ? undefined : id.slice(at + '@workspace:'.length) || '.'
|
|
166
|
+
}
|
|
167
|
+
for (const dir of lock.workspaces.keys()) {
|
|
168
|
+
const from = importerFrom(lock, dir)
|
|
169
|
+
const links = { deps: {} as Record<string, string>, peers: {} as Record<string, string> }
|
|
170
|
+
const add = (kind: 'deps' | 'peers', name: string, target: string | undefined): void => {
|
|
171
|
+
if (target !== undefined && target !== dir && lock.workspaces.has(target)) {
|
|
172
|
+
links[kind][name] = target
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const [name, spec] of lock.workspaceInstalled.get(dir)!) {
|
|
176
|
+
add('deps', name, workspaceSpecTarget(lock, dir, from, name, spec) ?? viaLayout(from, name))
|
|
177
|
+
}
|
|
178
|
+
for (const name of lock.workspacePeers.get(dir)!) add('peers', name, viaLayout(from, name))
|
|
179
|
+
out.set(dir, links)
|
|
180
|
+
}
|
|
181
|
+
return out
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const ALIAS = /^(@[^@/]+\/[^@/]+|[^@/.][^@/]*)(?:@.*)?$/
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The workspace dir a spec names past Bun's layout: `workspace:<path>`,
|
|
188
|
+
* `workspace:<name>@<range>`, or `npm:<name>@<range>` when the entry the
|
|
189
|
+
* dependency resolves to is not a `<name>` package.
|
|
190
|
+
*/
|
|
191
|
+
function workspaceSpecTarget(
|
|
192
|
+
lock: Lockfile,
|
|
193
|
+
dir: string,
|
|
194
|
+
from: string,
|
|
195
|
+
key: string,
|
|
196
|
+
spec: string,
|
|
197
|
+
): string | undefined {
|
|
198
|
+
if (spec.startsWith('npm:')) {
|
|
199
|
+
const alias = ALIAS.exec(spec.slice('npm:'.length))?.[1]
|
|
200
|
+
const at = resolve(lock, from, key)
|
|
201
|
+
if (alias === undefined || lock.packages.get(at ?? '')?.id.startsWith(`${alias}@`)) {
|
|
202
|
+
return undefined
|
|
203
|
+
}
|
|
204
|
+
return lock.workspaceDirs.get(alias)
|
|
205
|
+
}
|
|
206
|
+
if (!spec.startsWith('workspace:')) return undefined
|
|
207
|
+
const rest = spec.slice('workspace:'.length)
|
|
208
|
+
if (rest.startsWith('/')) return undefined
|
|
209
|
+
if (rest.startsWith('.')) return joinPosix(dir, rest)
|
|
210
|
+
const alias = ALIAS.exec(rest)
|
|
211
|
+
return alias === null ? undefined : lock.workspaceDirs.get(alias[1]!)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The node_modules path a workspace resolves from: its own name, or the root's ''. */
|
|
215
|
+
function importerFrom(lock: Lockfile, dir: string): string {
|
|
216
|
+
if (dir === '.') return ''
|
|
217
|
+
for (const [name, wsDir] of lock.workspaceDirs) if (wsDir === dir) return name
|
|
218
|
+
return ''
|
|
219
|
+
}
|
|
220
|
+
|
|
137
221
|
/**
|
|
138
222
|
* The patch files `patchedDependencies` names. bun.lock records a patch by
|
|
139
223
|
* PATH, never by a hash of its content, so an edited patch left the lockfile
|
|
@@ -205,8 +289,7 @@ export function importerDigests(
|
|
|
205
289
|
// Its dir, as pnpm's importers fold theirs (item 1073).
|
|
206
290
|
const own = node(`workspace\0${dir}`, `workspace\0${dir}`)
|
|
207
291
|
importers.set(dir, own)
|
|
208
|
-
|
|
209
|
-
for (const [name, wsDir] of lock.workspaceDirs) if (wsDir === dir && dir !== '.') from = name
|
|
292
|
+
const from = importerFrom(lock, dir)
|
|
210
293
|
for (const [name, spec] of deps) {
|
|
211
294
|
const target = resolve(lock, from, name)
|
|
212
295
|
if (target !== undefined) edges[own]!.push(index.get(target)!)
|
|
@@ -235,93 +318,6 @@ export function importerDigests(
|
|
|
235
318
|
return out
|
|
236
319
|
}
|
|
237
320
|
|
|
238
|
-
/**
|
|
239
|
-
* The lockfile cut to the workspaces at `dirs` (`.` the root) and the
|
|
240
|
-
* packages they reach through Bun's hoisted walk, each where it sat: Bun
|
|
241
|
-
* installs a nested key whose root counterpart is gone as written
|
|
242
|
-
* (`--frozen-lockfile` passes), so nothing is re-hoisted. Every other
|
|
243
|
-
* field as written — catalogs, overrides, patches and trusted
|
|
244
|
-
* dependencies are the workspace's, and the root manifest still names them.
|
|
245
|
-
*/
|
|
246
|
-
export function pruneLockfile(text: string, { dirs, dropped }: PruneScope): string {
|
|
247
|
-
const lock = parseLockfile(text)
|
|
248
|
-
const doc = Bun.JSONC.parse(text) as Json
|
|
249
|
-
const reached = new Set<string>()
|
|
250
|
-
const visit = (key: string | undefined): void => {
|
|
251
|
-
if (key === undefined || reached.has(key)) return
|
|
252
|
-
reached.add(key)
|
|
253
|
-
const entry = lock.packages.get(key)!
|
|
254
|
-
const at = entry.id.indexOf('@workspace:')
|
|
255
|
-
if (at !== -1) {
|
|
256
|
-
const dir = entry.id.slice(at + '@workspace:'.length) || '.'
|
|
257
|
-
if (!dirs.has(dir))
|
|
258
|
-
throw new Error(`bun.lock: ${key} is workspace ${dir}, which the subset leaves out`)
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
for (const name of entry.deps.keys()) visit(resolve(lock, key, name))
|
|
262
|
-
}
|
|
263
|
-
const workspaces = record(doc['workspaces']) ?? {}
|
|
264
|
-
const skip = new Map<string, Set<string>>()
|
|
265
|
-
for (const [dir, manifest] of Object.entries(workspaces)) {
|
|
266
|
-
const key = dir === '' ? '.' : dir
|
|
267
|
-
if (!dirs.has(key)) {
|
|
268
|
-
delete workspaces[dir]
|
|
269
|
-
continue
|
|
270
|
-
}
|
|
271
|
-
const names = devOnly(record(manifest), dropped)
|
|
272
|
-
skip.set(key, names)
|
|
273
|
-
dropDev(record(manifest), names)
|
|
274
|
-
}
|
|
275
|
-
for (const [dir, deps] of lock.workspaces) {
|
|
276
|
-
if (!dirs.has(dir)) continue
|
|
277
|
-
let from = ''
|
|
278
|
-
for (const [name, wsDir] of lock.workspaceDirs) if (wsDir === dir && dir !== '.') from = name
|
|
279
|
-
if (from !== '') visit(from)
|
|
280
|
-
const gone = skip.get(dir)
|
|
281
|
-
for (const name of deps.keys()) if (gone?.has(name) !== true) visit(resolve(lock, from, name))
|
|
282
|
-
}
|
|
283
|
-
const packages = record(doc['packages']) ?? {}
|
|
284
|
-
for (const key of Object.keys(packages)) if (!reached.has(key)) delete packages[key]
|
|
285
|
-
return writeLockfile(doc)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Bun's own text layout, so a pruned file diffs against the source as
|
|
290
|
-
* removals: two-space indent, a trailing comma after every member but the
|
|
291
|
-
* document's last, each `packages` entry on one line with a blank line
|
|
292
|
-
* between entries.
|
|
293
|
-
*/
|
|
294
|
-
function writeLockfile(doc: Json): string {
|
|
295
|
-
const inline = (v: unknown): string => {
|
|
296
|
-
if (Array.isArray(v)) return `[${v.map(inline).join(', ')}]`
|
|
297
|
-
const r = record(v)
|
|
298
|
-
if (r === undefined) return JSON.stringify(v)
|
|
299
|
-
const members = Object.entries(r).map(([k, x]) => `${JSON.stringify(k)}: ${inline(x)}`)
|
|
300
|
-
return members.length === 0 ? '{}' : `{ ${members.join(', ')} }`
|
|
301
|
-
}
|
|
302
|
-
const block = (v: unknown, depth: number): string => {
|
|
303
|
-
const pad = ' '.repeat(depth + 1)
|
|
304
|
-
const end = ' '.repeat(depth)
|
|
305
|
-
if (Array.isArray(v)) {
|
|
306
|
-
return v.length === 0
|
|
307
|
-
? '[]'
|
|
308
|
-
: `[\n${v.map((x) => `${pad}${block(x, depth + 1)},\n`).join('')}${end}]`
|
|
309
|
-
}
|
|
310
|
-
const r = record(v)
|
|
311
|
-
if (r === undefined) return JSON.stringify(v)
|
|
312
|
-
const members = Object.entries(r)
|
|
313
|
-
if (members.length === 0) return '{}'
|
|
314
|
-
return `{\n${members.map(([k, x]) => `${pad}${JSON.stringify(k)}: ${block(x, depth + 1)},\n`).join('')}${end}}`
|
|
315
|
-
}
|
|
316
|
-
const members = Object.entries(doc).map(([k, v]) => {
|
|
317
|
-
if (k !== 'packages') return ` ${JSON.stringify(k)}: ${block(v, 1)}`
|
|
318
|
-
const entries = Object.entries(record(v) ?? {})
|
|
319
|
-
if (entries.length === 0) return ` "packages": {}`
|
|
320
|
-
return ` "packages": {\n${entries.map(([p, t]) => ` ${JSON.stringify(p)}: ${inline(t)},\n`).join('\n')} }`
|
|
321
|
-
})
|
|
322
|
-
return `{\n${members.join(',\n')}\n}\n`
|
|
323
|
-
}
|
|
324
|
-
|
|
325
321
|
/** The name a package key installs under: its last segment, scope included. */
|
|
326
322
|
function installName(key: string): string {
|
|
327
323
|
const parts = key.split('/')
|