@_linked/cli 1.46.2 → 1.47.0

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.
Files changed (167) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +48 -36
  3. package/hooks/drift.cjs +169 -0
  4. package/hooks/git-hook.cjs +170 -0
  5. package/hooks/hook-shim.cjs +63 -0
  6. package/hooks/lock-at-rest.cjs +268 -0
  7. package/hooks/manifests.cjs +216 -0
  8. package/hooks/platform.cjs +25 -0
  9. package/lib/esm/app-release/build-backend.d.ts.map +1 -1
  10. package/lib/esm/app-release/build-backend.js +2 -1
  11. package/lib/esm/app-release/build-backend.js.map +1 -1
  12. package/lib/esm/cli-methods.d.ts.map +1 -1
  13. package/lib/esm/cli-methods.js +166 -150
  14. package/lib/esm/cli-methods.js.map +1 -1
  15. package/lib/esm/cli.js +20 -22
  16. package/lib/esm/cli.js.map +1 -1
  17. package/lib/esm/commands/build-app.d.ts.map +1 -1
  18. package/lib/esm/commands/build-app.js +2 -1
  19. package/lib/esm/commands/build-app.js.map +1 -1
  20. package/lib/esm/commands/build-package.d.ts.map +1 -1
  21. package/lib/esm/commands/build-package.js +6 -2
  22. package/lib/esm/commands/build-package.js.map +1 -1
  23. package/lib/esm/commands/localize.d.ts +9 -15
  24. package/lib/esm/commands/localize.d.ts.map +1 -1
  25. package/lib/esm/commands/localize.js +43 -74
  26. package/lib/esm/commands/localize.js.map +1 -1
  27. package/lib/esm/commands/setup-publish.d.ts.map +1 -1
  28. package/lib/esm/commands/setup-publish.js +22 -15
  29. package/lib/esm/commands/setup-publish.js.map +1 -1
  30. package/lib/esm/commands/start.d.ts +8 -0
  31. package/lib/esm/commands/start.d.ts.map +1 -1
  32. package/lib/esm/commands/start.js +20 -8
  33. package/lib/esm/commands/start.js.map +1 -1
  34. package/lib/esm/installed-packages.d.ts +5 -13
  35. package/lib/esm/installed-packages.d.ts.map +1 -1
  36. package/lib/esm/installed-packages.js +5 -38
  37. package/lib/esm/installed-packages.js.map +1 -1
  38. package/lib/esm/lifecycle.d.ts +35 -22
  39. package/lib/esm/lifecycle.d.ts.map +1 -1
  40. package/lib/esm/lifecycle.js +62 -36
  41. package/lib/esm/lifecycle.js.map +1 -1
  42. package/lib/esm/localize/adopt.d.ts +5 -5
  43. package/lib/esm/localize/adopt.d.ts.map +1 -1
  44. package/lib/esm/localize/adopt.js +46 -55
  45. package/lib/esm/localize/adopt.js.map +1 -1
  46. package/lib/esm/localize/app-root.d.ts +16 -0
  47. package/lib/esm/localize/app-root.d.ts.map +1 -0
  48. package/lib/esm/localize/app-root.js +94 -0
  49. package/lib/esm/localize/app-root.js.map +1 -0
  50. package/lib/esm/localize/app-setup.d.ts +21 -0
  51. package/lib/esm/localize/app-setup.d.ts.map +1 -0
  52. package/lib/esm/localize/app-setup.js +178 -0
  53. package/lib/esm/localize/app-setup.js.map +1 -0
  54. package/lib/esm/localize/delocalize.d.ts.map +1 -1
  55. package/lib/esm/localize/delocalize.js +169 -104
  56. package/lib/esm/localize/delocalize.js.map +1 -1
  57. package/lib/esm/localize/drift-hook.d.ts +18 -0
  58. package/lib/esm/localize/drift-hook.d.ts.map +1 -0
  59. package/lib/esm/localize/drift-hook.js +37 -0
  60. package/lib/esm/localize/drift-hook.js.map +1 -0
  61. package/lib/esm/localize/drift.d.ts +36 -0
  62. package/lib/esm/localize/drift.d.ts.map +1 -0
  63. package/lib/esm/localize/drift.js +34 -0
  64. package/lib/esm/localize/drift.js.map +1 -0
  65. package/lib/esm/localize/errors.d.ts +14 -8
  66. package/lib/esm/localize/errors.d.ts.map +1 -1
  67. package/lib/esm/localize/errors.js +14 -8
  68. package/lib/esm/localize/errors.js.map +1 -1
  69. package/lib/esm/localize/fsops.d.ts +11 -27
  70. package/lib/esm/localize/fsops.d.ts.map +1 -1
  71. package/lib/esm/localize/fsops.js +40 -115
  72. package/lib/esm/localize/fsops.js.map +1 -1
  73. package/lib/esm/localize/git-hooks.d.ts +33 -0
  74. package/lib/esm/localize/git-hooks.d.ts.map +1 -0
  75. package/lib/esm/localize/git-hooks.js +146 -0
  76. package/lib/esm/localize/git-hooks.js.map +1 -0
  77. package/lib/esm/localize/hook-module.d.ts +6 -0
  78. package/lib/esm/localize/hook-module.d.ts.map +1 -0
  79. package/lib/esm/localize/hook-module.js +37 -0
  80. package/lib/esm/localize/hook-module.js.map +1 -0
  81. package/lib/esm/localize/index.d.ts +14 -15
  82. package/lib/esm/localize/index.d.ts.map +1 -1
  83. package/lib/esm/localize/index.js +8 -9
  84. package/lib/esm/localize/index.js.map +1 -1
  85. package/lib/esm/localize/list.d.ts +6 -10
  86. package/lib/esm/localize/list.d.ts.map +1 -1
  87. package/lib/esm/localize/list.js +49 -189
  88. package/lib/esm/localize/list.js.map +1 -1
  89. package/lib/esm/localize/localize.d.ts +46 -62
  90. package/lib/esm/localize/localize.d.ts.map +1 -1
  91. package/lib/esm/localize/localize.js +411 -285
  92. package/lib/esm/localize/localize.js.map +1 -1
  93. package/lib/esm/localize/members.d.ts +13 -0
  94. package/lib/esm/localize/members.d.ts.map +1 -0
  95. package/lib/esm/localize/members.js +46 -0
  96. package/lib/esm/localize/members.js.map +1 -0
  97. package/lib/esm/localize/resolve.d.ts +1 -2
  98. package/lib/esm/localize/resolve.d.ts.map +1 -1
  99. package/lib/esm/localize/resolve.js +3 -5
  100. package/lib/esm/localize/resolve.js.map +1 -1
  101. package/lib/esm/localize/run.d.ts +1 -0
  102. package/lib/esm/localize/run.d.ts.map +1 -1
  103. package/lib/esm/localize/run.js +11 -6
  104. package/lib/esm/localize/run.js.map +1 -1
  105. package/lib/esm/localize/setup-lockfile.d.ts +13 -0
  106. package/lib/esm/localize/setup-lockfile.d.ts.map +1 -0
  107. package/lib/esm/localize/setup-lockfile.js +80 -0
  108. package/lib/esm/localize/setup-lockfile.js.map +1 -0
  109. package/lib/esm/utils/createPackageLocation.d.ts +4 -13
  110. package/lib/esm/utils/createPackageLocation.d.ts.map +1 -1
  111. package/lib/esm/utils/createPackageLocation.js +12 -21
  112. package/lib/esm/utils/createPackageLocation.js.map +1 -1
  113. package/lib/esm/utils/packageSetup.d.ts +5 -2
  114. package/lib/esm/utils/packageSetup.d.ts.map +1 -1
  115. package/lib/esm/utils/packageSetup.js +9 -3
  116. package/lib/esm/utils/packageSetup.js.map +1 -1
  117. package/lib/esm/utils/platform.d.ts +24 -0
  118. package/lib/esm/utils/platform.d.ts.map +1 -0
  119. package/lib/esm/utils/platform.js +62 -0
  120. package/lib/esm/utils/platform.js.map +1 -0
  121. package/lib/esm/utils/shapeReferences.d.ts.map +1 -1
  122. package/lib/esm/utils/shapeReferences.js +3 -2
  123. package/lib/esm/utils/shapeReferences.js.map +1 -1
  124. package/lib/esm/utils/spawn.d.ts +65 -0
  125. package/lib/esm/utils/spawn.d.ts.map +1 -0
  126. package/lib/esm/utils/spawn.js +181 -0
  127. package/lib/esm/utils/spawn.js.map +1 -0
  128. package/lib/esm/utils.d.ts.map +1 -1
  129. package/lib/esm/utils.js +19 -5
  130. package/lib/esm/utils.js.map +1 -1
  131. package/lib/esm/vite-config.d.ts +5 -12
  132. package/lib/esm/vite-config.d.ts.map +1 -1
  133. package/lib/esm/vite-config.js +41 -33
  134. package/lib/esm/vite-config.js.map +1 -1
  135. package/package.json +4 -3
  136. package/lib/esm/localize/ensure.d.ts +0 -44
  137. package/lib/esm/localize/ensure.d.ts.map +0 -1
  138. package/lib/esm/localize/ensure.js +0 -151
  139. package/lib/esm/localize/ensure.js.map +0 -1
  140. package/lib/esm/localize/manifest.d.ts +0 -63
  141. package/lib/esm/localize/manifest.d.ts.map +0 -1
  142. package/lib/esm/localize/manifest.js +0 -133
  143. package/lib/esm/localize/manifest.js.map +0 -1
  144. package/lib/esm/localize/one-copy-hook.d.ts +0 -16
  145. package/lib/esm/localize/one-copy-hook.d.ts.map +0 -1
  146. package/lib/esm/localize/one-copy-hook.js +0 -20
  147. package/lib/esm/localize/one-copy-hook.js.map +0 -1
  148. package/lib/esm/localize/provided.d.ts +0 -28
  149. package/lib/esm/localize/provided.d.ts.map +0 -1
  150. package/lib/esm/localize/provided.js +0 -89
  151. package/lib/esm/localize/provided.js.map +0 -1
  152. package/lib/esm/localize/prune.d.ts +0 -182
  153. package/lib/esm/localize/prune.d.ts.map +0 -1
  154. package/lib/esm/localize/prune.js +0 -703
  155. package/lib/esm/localize/prune.js.map +0 -1
  156. package/lib/esm/localize/relink.d.ts +0 -13
  157. package/lib/esm/localize/relink.d.ts.map +0 -1
  158. package/lib/esm/localize/relink.js +0 -181
  159. package/lib/esm/localize/relink.js.map +0 -1
  160. package/lib/esm/localized-dedupe.d.ts +0 -26
  161. package/lib/esm/localized-dedupe.d.ts.map +0 -1
  162. package/lib/esm/localized-dedupe.js +0 -139
  163. package/lib/esm/localized-dedupe.js.map +0 -1
  164. package/lib/esm/utils/relinkHooks.d.ts +0 -19
  165. package/lib/esm/utils/relinkHooks.d.ts.map +0 -1
  166. package/lib/esm/utils/relinkHooks.js +0 -74
  167. package/lib/esm/utils/relinkHooks.js.map +0 -1
@@ -1,703 +0,0 @@
1
- /**
2
- * Pruning -- stop a checkout's own `node_modules` holding a second copy of
3
- * something the app already provides. On by default after every install in a
4
- * checkout and on every `--relink`; `--no-prune` (`{prune: false}`) skips it.
5
- *
6
- * `shouldPrune(opts)` is the one place that default lives.
7
- *
8
- * Why it is needed. `npm install` inside a checkout (localize.ts, step 3)
9
- * installs what the CHECKOUT's lockfile says: its own copy of every
10
- * dependency, often older than the app's. Node resolves a module from the
11
- * importer's REAL path and takes the first `node_modules` it meets going up,
12
- * so a localized `A` that imports `B` gets `A/node_modules/B` -- a registry
13
- * copy -- and never the localized `B`; and a framework package that holds a
14
- * registry of classes (or React) loads once per copy. Measured with three
15
- * localized `@_linked/*` packages: plain Node and `tsx` loaded three copies of
16
- * the same core.
17
- *
18
- * What it does. For every localized checkout, each package at the top of its
19
- * `node_modules` that is a CANDIDATE in that checkout (below) is removed when
20
- * the app provides it -- `<appRoot>/node_modules/<name>` exists -- at a
21
- * version that satisfies every range that resolves to that copy: the
22
- * checkout's own declaration and that of every installed package that would
23
- * load it. Node's upward search then finds the app's copy instead. Nothing
24
- * else in the checkout moves.
25
- *
26
- * The candidates start from `providedPackages` (provided.ts) -- the one rule
27
- * the run-time check and Vite's dedupe use too -- and are taken PER CHECKOUT:
28
- *
29
- * - a **localized sibling** -- always counts as provided, whatever the range,
30
- * because the live checkout is the point of localizing it. A range it does
31
- * not satisfy is said out loud, never acted on;
32
- * - one of **this checkout's own runtime dependencies** (`dependencies`,
33
- * `peerDependencies`, `optionalDependencies`) that the rule provides -- the
34
- * app has it at a version satisfying every localized checkout's range;
35
- * - `react` and `react-dom`.
36
- *
37
- * Nothing else is touched -- this checkout's devDependency stays even when
38
- * another checkout depends on the same package at runtime, so the checkout's
39
- * tooling does not come to depend on the app's. A runtime dependency the rule
40
- * leaves out because a localized range misses the app's version is kept and
41
- * reported, like any other copy kept below.
42
- *
43
- * Never removed, candidate or not:
44
- *
45
- * - a copy whose ranges the app's version does not satisfy;
46
- * - a copy whose own dependencies would resolve to something else from the
47
- * app's copy than they do from the checkout (`contextChange` below): the
48
- * checkout keeps its own copies of whatever failed the guard, and removing
49
- * the copy would mix the two -- measured as a checkout's `tsc` seeing two
50
- * React type sets;
51
- * - a copy that ships its own types while the checkout also holds an
52
- * `@types/` stub for it (`staleTypes` below): TypeScript would take the stub
53
- * once the copy is gone;
54
- * - anything in a checkout that is not under the app root, because then the
55
- * upward search never reaches the app's `node_modules` and the removed
56
- * package would simply be missing;
57
- * - anything in a checkout recorded with `prune: false` (localized with
58
- * `--no-prune`).
59
- *
60
- * Kept copies are reported once per checkout, as a summary; `--list` gives
61
- * each one's reason. A removal can make another copy removable, so a prune
62
- * repeats until a pass removes nothing.
63
- *
64
- * A package with a `bin` is NOT exempt. Measured in a checkout under an app
65
- * root with its own `@_linked/cli` removed: `npx linked build`,
66
- * `npm exec linked -- --help`, a script running `npm exec linked` and a script
67
- * running a bare `linked` all resolved `linked` to the app root's copy,
68
- * because npm puts every ancestor's `node_modules/.bin` on the PATH. The
69
- * removed package's links in the checkout's `node_modules/.bin` are removed
70
- * with it, so nothing dangles.
71
- *
72
- * It never runs npm and never touches the checkout's `package.json` or
73
- * `package-lock.json`. `npm install` in the checkout puts the copies back;
74
- * `ensure` (ensure.ts) is the install-then-prune step every in-checkout
75
- * install goes through, and the run-time check (`checkOneCopy`) takes them out
76
- * again before `linked start` and friends.
77
- */
78
- import fs from 'node:fs';
79
- import path from 'node:path';
80
- import { isInside, isSymlink, readJson } from './fsops.js';
81
- import semver from 'semver';
82
- import { providedPackages } from './provided.js';
83
- /** The rule (`providedPackages`, provided.ts) over `entries`, plus every entry (sibling). */
84
- export function candidatesFor(entries, deps) {
85
- const rule = providedPackages(deps.appRoot, Object.keys(entries));
86
- return {
87
- names: new Set([...rule.names, ...Object.keys(entries)]),
88
- skipped: new Map(rule.skipped.map((s) => [s.name, s])),
89
- };
90
- }
91
- /**
92
- * Does the app's installed `version` satisfy `range`? `null` means "cannot
93
- * tell" -- a dist-tag, `file:`, `npm:`, `workspace:`, a git URL, or a version
94
- * that is not one -- and every caller then keeps the copy. The failure mode is
95
- * therefore always the safe one: a copy kept that could have gone, never a
96
- * copy removed that was needed.
97
- *
98
- * npm's own `semver` decides, so prereleases follow npm's rule: `1.3.0-beta.1`
99
- * satisfies a range only when one of its comparators names a prerelease on
100
- * the same `major.minor.patch`.
101
- */
102
- export function satisfies(version, range) {
103
- if (typeof version !== 'string' || typeof range !== 'string')
104
- return null;
105
- if (semver.valid(version) === null)
106
- return null;
107
- if (semver.validRange(range) === null)
108
- return null;
109
- return semver.satisfies(version, range);
110
- }
111
- /** Pruning is on unless a caller says `{prune: false}`. */
112
- export function shouldPrune(opts = {}) {
113
- return opts.prune !== false;
114
- }
115
- /** Is pruning on for this recorded checkout? `--no-prune` at localize/adopt records `prune: false`. */
116
- export function prunesEntry(entry) {
117
- return entry.prune !== false;
118
- }
119
- const REQUIRE_FIELDS = [
120
- 'dependencies',
121
- 'peerDependencies',
122
- 'optionalDependencies',
123
- ];
124
- const DECLARE_FIELDS = [
125
- 'dependencies',
126
- 'devDependencies',
127
- 'peerDependencies',
128
- 'optionalDependencies',
129
- ];
130
- /** Candidates in every checkout, declared or not: a second React breaks hooks whoever loads it. */
131
- const ALWAYS_CANDIDATES = new Set(['react', 'react-dom']);
132
- /** prune repeats until a pass removes nothing, at most this many passes. */
133
- export const MAX_PRUNE_PASSES = 5;
134
- export function newPruneCache() {
135
- return { indexes: new Map(), builds: 0 };
136
- }
137
- /**
138
- * Decide, for every recorded checkout and every candidate copy in its
139
- * `node_modules`, whether pruning removes it -- without removing anything.
140
- * `pruneProvided` applies it; the run-time check (`checkOneCopy`) and
141
- * `--list` call it on its own, which is why it has to be cheap: a copy whose
142
- * checkout's own range rules it out is decided from that `package.json`
143
- * alone, and every other range comes from one requirement index per
144
- * `node_modules`, built at most once per call.
145
- *
146
- * Candidates are PER OWNER: a copy is a candidate in checkout O when the rule
147
- * provides it AND it is a localized sibling, `react`/`react-dom`, or one of
148
- * O's own runtime dependencies (`dependencies`, `peerDependencies`,
149
- * `optionalDependencies`). O's devDependencies are O's tooling, whatever
150
- * another checkout declares at runtime.
151
- */
152
- export function planPrune(entries, deps, scope = {}, cache = newPruneCache(), candidates = candidatesFor(entries, deps)) {
153
- var _a;
154
- const plan = { remove: [], keep: [], outside: [] };
155
- const appRoot = realOrSelf(deps.appRoot);
156
- const only = scope.deps ? new Set(scope.deps) : null;
157
- for (const [name, entry] of Object.entries(entries)) {
158
- if (scope.owners && !scope.owners.includes(name))
159
- continue;
160
- if (!prunesEntry(entry))
161
- continue;
162
- const pkgDir = path.join(deps.appRoot, entry.path);
163
- if (!fs.existsSync(path.join(pkgDir, 'package.json')))
164
- continue;
165
- if (!isInside(realOrSelf(pkgDir), appRoot)) {
166
- plan.outside.push({ name, path: entry.path });
167
- continue;
168
- }
169
- const pkg = (_a = readJson(path.join(pkgDir, 'package.json'))) !== null && _a !== void 0 ? _a : {};
170
- const runtime = new Set(REQUIRE_FIELDS.flatMap((f) => { var _a; return Object.keys((_a = pkg[f]) !== null && _a !== void 0 ? _a : {}); }));
171
- // The names that can be a decision here, looked up directly rather than
172
- // by listing node_modules (thousands of entries per checkout): this
173
- // checkout's candidates, and what the rule skipped that it declares.
174
- const names = new Set();
175
- for (const n of candidates.names) {
176
- if (entries[n] || ALWAYS_CANDIDATES.has(n) || runtime.has(n))
177
- names.add(n);
178
- }
179
- for (const n of candidates.skipped.keys()) {
180
- if (declared(pkg, DECLARE_FIELDS, n) !== undefined)
181
- names.add(n);
182
- }
183
- names.delete(name);
184
- const ctx = { name, entry, pkg, names, entries, candidates, only, deps };
185
- for (const nm of nodeModulesRoots(pkgDir, entry)) {
186
- for (const d of decideIn(nm, ctx, cache)) {
187
- (d.action === 'remove' ? plan.remove : plan.keep).push(d);
188
- }
189
- }
190
- }
191
- return plan;
192
- }
193
- /**
194
- * @param entries the localized packages: every one counts as a sibling
195
- * @param opts unused beyond `prune`, which callers check with `shouldPrune`
196
- * @param deps `{appRoot, log, warn}`
197
- * @param scope prune only some checkouts / names (the run-time check)
198
- *
199
- * A removal can make another copy removable -- the copy that went was the one
200
- * asking a range the app misses, or the one a sibling copy's dependency
201
- * resolved to -- so it plans and removes again until a pass removes nothing
202
- * (at most `MAX_PRUNE_PASSES`): one call converges. A checkout recorded with
203
- * `prune: false` is never touched.
204
- */
205
- export function pruneProvided(entries, opts, deps, scope = {}, cache = newPruneCache()) {
206
- const removed = [];
207
- let kept = [];
208
- const candidates = candidatesFor(entries, deps);
209
- for (let pass = 0; pass < MAX_PRUNE_PASSES; pass++) {
210
- const passScope = pass === 0 ? scope : { owners: scope.owners };
211
- const plan = planPrune(entries, deps, passScope, cache, candidates);
212
- if (pass === 0) {
213
- for (const o of plan.outside) {
214
- deps.log(`[localize] ${o.name}: ${o.path} is outside the app root, so Node cannot reach the app's ` +
215
- `node_modules from it — its own dependencies are left as they are.`);
216
- }
217
- }
218
- for (const d of plan.remove) {
219
- fs.rmSync(d.dir, { recursive: true, force: true });
220
- removeEmptyScope(d.nm, d.dep);
221
- removeBinLinksInto(d.nm, d.dir);
222
- removed.push(d);
223
- }
224
- kept = plan.keep;
225
- if (!plan.remove.length)
226
- break;
227
- }
228
- report(removed, kept, deps);
229
- return { removed, kept };
230
- }
231
- /**
232
- * Is `dep` -- declared by a checkout at `range` -- something the app provides,
233
- * so that its absence from the checkout's own `node_modules` is not a
234
- * dependency gone missing? `--relink` asks this before reinstalling a
235
- * checkout, so a copy pruned on purpose does not trigger a reinstall on every
236
- * postinstall. `pkg` is the checkout's `package.json`: only what would be a
237
- * candidate in THAT checkout counts. With `checkout`, a copy the prune would
238
- * keep for a stub `@types/` package there (`staleTypes`) is not provided: its
239
- * absence is a dependency gone missing, and the reinstall puts it back.
240
- */
241
- export function isProvidedByApp(dep, range, entries, candidates, deps, pkg, checkout) {
242
- const app = appCopy(dep, deps);
243
- if (!app)
244
- return false;
245
- if (entries[dep])
246
- return true;
247
- if (!candidates.names.has(dep))
248
- return false;
249
- if (pkg &&
250
- !ALWAYS_CANDIDATES.has(dep) &&
251
- declared(pkg, REQUIRE_FIELDS, dep) === undefined)
252
- return false;
253
- if (range !== undefined && satisfies(app.version, range) !== true)
254
- return false;
255
- // Pruned or not, a copy the prune would keep because of a stub `@types/`
256
- // package is one the checkout needs: absent, it is missing. The app's copy
257
- // stands in for the one that is gone.
258
- if (checkout &&
259
- shipsOwnTypes(app.real, readJson(path.join(app.real, 'package.json'))) &&
260
- staleTypes(path.join(checkout.dir, 'node_modules'), dep, null))
261
- return false;
262
- return true;
263
- }
264
- function decideIn(nm, ctx, cache) {
265
- const out = [];
266
- const { name: owner, entry, pkg, names, entries, candidates, only, deps } = ctx;
267
- for (const dep of [...names].sort()) {
268
- if (only && !only.has(dep))
269
- continue;
270
- const skip = candidates.skipped.get(dep);
271
- const inRule = candidates.names.has(dep);
272
- const sibling = Boolean(entries[dep]);
273
- const dir = path.join(nm, dep);
274
- const nested = readJson(path.join(dir, 'package.json'));
275
- if (!nested)
276
- continue;
277
- const app = appCopy(dep, deps);
278
- if (!app)
279
- continue; // the app does not provide it, so this copy is the only one
280
- if (realOrSelf(dir) === app.real)
281
- continue; // already the app's copy
282
- const base = {
283
- ownerName: owner,
284
- owner: entry.path,
285
- dep,
286
- dir,
287
- where: path.relative(deps.appRoot, dir),
288
- nm,
289
- version: nested.version,
290
- app: app.version,
291
- sibling,
292
- };
293
- if (!inRule) {
294
- // The rule leaves it out: a localized checkout asks a range the app
295
- // misses. Said for a checkout that declares it; a copy it only has
296
- // transitively is none of its business.
297
- if (declared(pkg, DECLARE_FIELDS, dep) === undefined)
298
- continue;
299
- out.push(Object.assign(Object.assign({}, base), { action: 'keep', reason: 'range', unmet: skip.asks }));
300
- continue;
301
- }
302
- const own = declared(pkg, DECLARE_FIELDS, dep);
303
- const ownAsk = own === undefined ? [] : [{ from: owner, range: own }];
304
- if (sibling) {
305
- // Always removed: the live checkout is the point of localizing it. A
306
- // range it misses is drift, said out loud and never acted on.
307
- const ranges = [...ownAsk, ...requirementIndex(nm, cache).asks(dep)];
308
- out.push(Object.assign(Object.assign({}, base), { action: 'remove', unmet: ranges.filter((r) => satisfies(app.version, r.range) === false) }));
309
- continue;
310
- }
311
- // The checkout's own range first: when it rules the copy out, no index is needed.
312
- if (own !== undefined && satisfies(app.version, own) !== true) {
313
- out.push(Object.assign(Object.assign({}, base), { action: 'keep', reason: 'range', unmet: ownAsk }));
314
- continue;
315
- }
316
- const unmet = requirementIndex(nm, cache)
317
- .asks(dep)
318
- .filter((r) => satisfies(app.version, r.range) !== true);
319
- if (unmet.length) {
320
- out.push(Object.assign(Object.assign({}, base), { action: 'keep', reason: 'range', unmet }));
321
- continue;
322
- }
323
- const types = shipsOwnTypes(dir, nested) ? staleTypes(nm, dep, dir) : null;
324
- if (types) {
325
- out.push(Object.assign(Object.assign({}, base), { action: 'keep', reason: 'types', unmet: [], types }));
326
- continue;
327
- }
328
- const context = contextChange(dir, app.real, nested);
329
- if (context) {
330
- out.push(Object.assign(Object.assign({}, base), { action: 'keep', reason: 'context', unmet: [], context }));
331
- continue;
332
- }
333
- out.push(Object.assign(Object.assign({}, base), { action: 'remove', unmet: [] }));
334
- }
335
- return out;
336
- }
337
- /**
338
- * Would removing the copy at `copyDir` change what ITS OWN dependencies
339
- * resolve to? Removing it hands its importers the app's copy, which resolves
340
- * its dependencies from the app root -- while the checkout keeps its own copy
341
- * of everything that failed the guard. Measured: a checkout kept its
342
- * `react`/`@types/react` 18 (its range), lost `prism-react-renderer` (in
343
- * range), and the app's copy of that resolved `react` -- and with it the
344
- * types -- to the app's 19: the checkout's own `tsc` saw both React type sets
345
- * and failed. So the copy stays when any of its `dependencies`,
346
- * `peerDependencies` or `optionalDependencies` (and the `@types/` package
347
- * TypeScript would take for it) resolves, from the copy's place in the
348
- * checkout, to a different package than from the app's copy: a different real
349
- * path that is not the same `name@version`, or nothing at all from the app's.
350
- * The same version at another path is the same code, so it does not count; a
351
- * dependency the checkout cannot resolve either is no change.
352
- */
353
- function contextChange(copyDir, appReal, pkg) {
354
- var _a;
355
- const names = new Set();
356
- for (const f of REQUIRE_FIELDS) {
357
- for (const d of Object.keys((_a = pkg[f]) !== null && _a !== void 0 ? _a : {})) {
358
- names.add(d);
359
- names.add(typesPackageFor(d));
360
- }
361
- }
362
- for (const d of names) {
363
- const here = resolveFrom(copyDir, d);
364
- if (!here)
365
- continue;
366
- const there = resolveFrom(appReal, d);
367
- if (!there)
368
- return { dep: d, here: here.version, app: null };
369
- if (here.real !== there.real && here.version !== there.version)
370
- return { dep: d, here: here.version, app: there.version };
371
- }
372
- return undefined;
373
- }
374
- /**
375
- * Would TypeScript, resolving `dep` from the checkout that owns `nm` once the
376
- * copy at `<nm>/<dep>` (`removing`; `null` when it is already gone) is
377
- * removed, land on an `@types/` stub before it reaches any other copy of
378
- * `dep`? TypeScript looks, in each `node_modules` going up, for `<dep>` and
379
- * then for `@types/<dep>`, and takes the first it finds. So with the copy gone
380
- * a checkout that still holds `@types/<dep>` -- typically as some other
381
- * package's dependency -- gets that stub, not the app's copy's own types.
382
- * Measured: the app's `minimatch` moved to 9.0.9, satisfying cli's `^9.0.4`,
383
- * the checkout's own `minimatch` 9 was removed, and cli's `tsc` resolved
384
- * `minimatch` to the checkout's `@types/minimatch@3.0.5` (depcheck →
385
- * multimatch → @types/minimatch): `TS2724 '"minimatch"' has no exported member
386
- * named 'minimatch'`.
387
- *
388
- * Callers ask only when the copy ships its own types: one that does not had
389
- * TypeScript take the same stub before the removal as after.
390
- */
391
- function staleTypes(nm, dep, removing) {
392
- const typesName = typesPackageFor(dep);
393
- if (typesName === dep)
394
- return null;
395
- for (let dir = path.dirname(nm);;) {
396
- if (path.basename(dir) !== 'node_modules') {
397
- const here = path.join(dir, 'node_modules');
398
- const other = path.join(here, dep);
399
- if (other !== removing && fs.existsSync(path.join(other, 'package.json')))
400
- return null; // another copy of dep comes first
401
- const stub = readJson(path.join(here, typesName, 'package.json'));
402
- if (stub)
403
- return { name: typesName, version: String(stub.version) };
404
- }
405
- const parent = path.dirname(dir);
406
- if (parent === dir)
407
- return null;
408
- dir = parent;
409
- }
410
- }
411
- /**
412
- * Does the package at `dir` carry its own type declarations? `types` /
413
- * `typings`, `typesVersions`, a `types` condition anywhere in `exports`, or an
414
- * `index.d.ts` next to its `package.json` -- where TypeScript looks for them.
415
- */
416
- export function shipsOwnTypes(dir, pkg) {
417
- if (!pkg)
418
- return false;
419
- if (pkg.types || pkg.typings || pkg.typesVersions)
420
- return true;
421
- if (hasTypesCondition(pkg.exports))
422
- return true;
423
- return fs.existsSync(path.join(dir, 'index.d.ts'));
424
- }
425
- function hasTypesCondition(exp) {
426
- if (!exp || typeof exp !== 'object')
427
- return false;
428
- if (Array.isArray(exp))
429
- return exp.some(hasTypesCondition);
430
- return Object.entries(exp).some(([k, v]) => k === 'types' || hasTypesCondition(v));
431
- }
432
- /** `react` → `@types/react`; `@scope/name` → `@types/scope__name`; an `@types/` name is itself. */
433
- function typesPackageFor(name) {
434
- if (name.startsWith('@types/'))
435
- return name;
436
- return `@types/${name.startsWith('@') ? name.slice(1).replace('/', '__') : name}`;
437
- }
438
- /** Node's upward search for `name` from the directory `from`: the first `node_modules/<name>`. */
439
- function resolveFrom(from, name) {
440
- for (let dir = from;;) {
441
- if (path.basename(dir) !== 'node_modules') {
442
- const candidate = path.join(dir, 'node_modules', name);
443
- const pkg = readJson(path.join(candidate, 'package.json'));
444
- if (pkg)
445
- return { real: realOrSelf(candidate), version: String(pkg.version) };
446
- }
447
- const parent = path.dirname(dir);
448
- if (parent === dir)
449
- return null;
450
- dir = parent;
451
- }
452
- }
453
- /** The package's directory, and the `node_modules` directories between `nm` and it (its own included). */
454
- function locate(nm, i) {
455
- if (!i.located) {
456
- const segs = i.rel.split('/');
457
- const dir = path.join(nm, ...segs);
458
- const between = [path.join(dir, 'node_modules')];
459
- for (let k = 0; k < segs.length; k++) {
460
- if (segs[k] === 'node_modules')
461
- between.push(path.join(nm, ...segs.slice(0, k + 1)));
462
- }
463
- i.located = { dir, between };
464
- }
465
- return i.located;
466
- }
467
- function requirementIndex(nm, cache) {
468
- var _a;
469
- let index = cache.indexes.get(nm);
470
- if (!index) {
471
- cache.builds++;
472
- const byDep = (_a = fromHiddenLockfile(nm)) !== null && _a !== void 0 ? _a : fromWalk(nm);
473
- index = {
474
- asks(dep) {
475
- var _a;
476
- const out = [];
477
- for (const { installed, range } of (_a = byDep.get(dep)) !== null && _a !== void 0 ? _a : []) {
478
- if (installed.name === dep)
479
- continue;
480
- const { dir, between } = locate(nm, installed);
481
- if (!fs.existsSync(path.join(dir, 'package.json')))
482
- continue;
483
- if (between.some((d) => fs.existsSync(path.join(d, dep))))
484
- continue;
485
- out.push({ from: `${installed.name}@${installed.version}`, range });
486
- }
487
- return out;
488
- },
489
- };
490
- cache.indexes.set(nm, index);
491
- }
492
- return index;
493
- }
494
- /** Record what the package at `rel` (below `nm`) requires. */
495
- function addRequirer(byDep, rel, pkg) {
496
- var _a;
497
- const at = rel.lastIndexOf('/node_modules/');
498
- const installed = {
499
- name: at === -1 ? rel : rel.slice(at + '/node_modules/'.length),
500
- version: pkg.version,
501
- rel,
502
- };
503
- const seen = new Set();
504
- for (const f of REQUIRE_FIELDS) {
505
- for (const [dep, range] of Object.entries((_a = pkg[f]) !== null && _a !== void 0 ? _a : {})) {
506
- if (typeof range !== 'string' || seen.has(dep))
507
- continue;
508
- seen.add(dep);
509
- let list = byDep.get(dep);
510
- if (!list)
511
- byDep.set(dep, (list = []));
512
- list.push({ installed, range });
513
- }
514
- }
515
- }
516
- const NODE_MODULES_PREFIX = 'node_modules/';
517
- function fromHiddenLockfile(nm) {
518
- const lock = readJson(path.join(nm, '.package-lock.json'));
519
- if (!(lock === null || lock === void 0 ? void 0 : lock.packages) || typeof lock.packages !== 'object')
520
- return null;
521
- const byDep = new Map();
522
- for (const [key, meta] of Object.entries(lock.packages)) {
523
- if (!key.startsWith(NODE_MODULES_PREFIX) || !meta || meta.link)
524
- continue;
525
- addRequirer(byDep, key.slice(NODE_MODULES_PREFIX.length), meta);
526
- }
527
- return byDep;
528
- }
529
- function fromWalk(nm) {
530
- const byDep = new Map();
531
- const walk = (dir, prefix) => {
532
- for (const name of topLevelPackages(dir)) {
533
- const pkgDir = path.join(dir, name);
534
- if (isSymlink(pkgDir))
535
- continue; // resolves from its real path, which is not here
536
- const rel = prefix + name;
537
- const pkg = readJson(path.join(pkgDir, 'package.json'));
538
- if (pkg)
539
- addRequirer(byDep, rel, pkg);
540
- const inner = path.join(pkgDir, 'node_modules');
541
- if (fs.existsSync(inner))
542
- walk(inner, `${rel}/node_modules/`);
543
- }
544
- };
545
- walk(nm, '');
546
- return byDep;
547
- }
548
- function report(removed, kept, deps) {
549
- var _a, _b;
550
- const byOwner = new Map();
551
- for (const r of removed)
552
- byOwner.set(r.owner, [...((_a = byOwner.get(r.owner)) !== null && _a !== void 0 ? _a : []), r]);
553
- for (const [owner, list] of byOwner) {
554
- deps.log(`[localize] ${owner}: removed its own copies of what the app provides — ` +
555
- list
556
- .map((r) => `${r.dep}@${r.version} (app: ${r.sibling ? `localized, ${r.app}` : r.app})`)
557
- .join(', '));
558
- for (const r of list) {
559
- for (const d of r.unmet) {
560
- deps.warn(`[localize] ${owner}: ${d.from} asks ${r.dep}@${d.range}; the localized checkout is ${r.app}. ` +
561
- `It is used anyway — that is what localizing it means — but the range is now fiction.`);
562
- }
563
- }
564
- }
565
- const keptBy = new Map();
566
- for (const k of kept)
567
- keptBy.set(k.owner, [...((_b = keptBy.get(k.owner)) !== null && _b !== void 0 ? _b : []), k]);
568
- for (const [owner, list] of keptBy) {
569
- const one = list.length === 1;
570
- deps.warn(`[localize] ${owner}: kept ${list.length} own ${one ? 'copy' : 'copies'} the app's cannot ` +
571
- `replace: ${summarizeKept(list)} — so the app loads ${one ? 'it' : 'them'} twice. ` +
572
- `\`linked localize --list\` says why.`);
573
- }
574
- }
575
- const SUMMARY_LIMIT = 8;
576
- /** `react@18.3.1 (app 19.2.0), prism-react-renderer@2.4.1 (its react would change), … and 4 more` */
577
- export function summarizeKept(list) {
578
- const shown = list.slice(0, SUMMARY_LIMIT).map(keptShort);
579
- const more = list.length - shown.length;
580
- return shown.join(', ') + (more > 0 ? `, … and ${more} more` : '');
581
- }
582
- function keptShort(k) {
583
- if (k.reason === 'types')
584
- return `${k.dep}@${k.version} (TypeScript would take ${k.types.name}@${k.types.version})`;
585
- return k.reason === 'context'
586
- ? `${k.dep}@${k.version} (its ${k.context.dep} would change)`
587
- : `${k.dep}@${k.version} (app ${k.app})`;
588
- }
589
- /** The full reason a copy stays, for `--list`. */
590
- export function explainKept(k) {
591
- if (k.reason === 'types') {
592
- const t = k.types;
593
- return (`${k.where}@${k.version}: it ships its own types and the checkout also has ${t.name}@${t.version} — ` +
594
- `removing it would make TypeScript take that stub instead of the app's ${k.dep}@${k.app}`);
595
- }
596
- if (k.reason === 'context') {
597
- const c = k.context;
598
- return (`${k.where}@${k.version}: from the checkout its ${c.dep} is ${c.here}, from the app's copy ` +
599
- `${c.app === null ? 'nothing' : c.app} — removing it would change what it loads`);
600
- }
601
- return (`${k.where}@${k.version}: ` +
602
- k.unmet.map((u) => `${u.from} asks ${k.dep}@${u.range}`).join(', ') +
603
- `, and the app has ${k.dep}@${k.app}` +
604
- (k.unmet.some((u) => satisfies(k.app, u.range) === null)
605
- ? ' (or the range is one localize cannot read)'
606
- : ''));
607
- }
608
- function declared(pkg, fields, dep) {
609
- var _a;
610
- for (const f of fields)
611
- if (((_a = pkg[f]) === null || _a === void 0 ? void 0 : _a[dep]) !== undefined)
612
- return pkg[f][dep];
613
- return undefined;
614
- }
615
- /** The app's own copy of `dep`: `<appRoot>/node_modules/<dep>`, followed through a link. */
616
- function appCopy(dep, deps) {
617
- const dir = path.join(deps.appRoot, 'node_modules', dep);
618
- const pkg = readJson(path.join(dir, 'package.json'));
619
- if (!(pkg === null || pkg === void 0 ? void 0 : pkg.version))
620
- return null;
621
- return { version: pkg.version, real: realOrSelf(dir) };
622
- }
623
- /** The package dir's `node_modules`, and the clone root's when the package is a monorepo subdir. */
624
- export function nodeModulesRoots(pkgDir, entry) {
625
- const roots = [path.join(pkgDir, 'node_modules')];
626
- if (entry.subdir) {
627
- const rel = path.normalize(entry.subdir);
628
- if (pkgDir.endsWith(path.sep + rel))
629
- roots.push(path.join(pkgDir.slice(0, -(rel.length + 1)), 'node_modules'));
630
- }
631
- return roots.filter((d) => fs.existsSync(d));
632
- }
633
- /** Package names directly under a `node_modules`, scoped ones as `@scope/name`. */
634
- function topLevelPackages(nm) {
635
- const out = [];
636
- let items;
637
- try {
638
- items = fs.readdirSync(nm, { withFileTypes: true });
639
- }
640
- catch (_a) {
641
- return out;
642
- }
643
- for (const item of items) {
644
- if (item.name.startsWith('.'))
645
- continue;
646
- if (item.name.startsWith('@')) {
647
- let inner = [];
648
- try {
649
- inner = fs.readdirSync(path.join(nm, item.name));
650
- }
651
- catch (_b) {
652
- /* not a directory */
653
- }
654
- for (const sub of inner)
655
- if (!sub.startsWith('.'))
656
- out.push(`${item.name}/${sub}`);
657
- }
658
- else {
659
- out.push(item.name);
660
- }
661
- }
662
- return out;
663
- }
664
- /** Links in `<nm>/.bin` that pointed into a package just removed: dangling now. */
665
- function removeBinLinksInto(nm, removedDir) {
666
- const bin = path.join(nm, '.bin');
667
- let names;
668
- try {
669
- names = fs.readdirSync(bin);
670
- }
671
- catch (_a) {
672
- return;
673
- }
674
- for (const name of names) {
675
- const link = path.join(bin, name);
676
- if (!isSymlink(link))
677
- continue;
678
- const target = path.resolve(bin, fs.readlinkSync(link));
679
- if (isInside(target, removedDir))
680
- fs.unlinkSync(link);
681
- }
682
- }
683
- function removeEmptyScope(nm, dep) {
684
- if (!dep.startsWith('@'))
685
- return;
686
- const scope = path.join(nm, dep.split('/')[0]);
687
- try {
688
- if (!fs.readdirSync(scope).length)
689
- fs.rmdirSync(scope);
690
- }
691
- catch (_a) {
692
- /* already gone */
693
- }
694
- }
695
- export function realOrSelf(p) {
696
- try {
697
- return fs.realpathSync(p);
698
- }
699
- catch (_a) {
700
- return p;
701
- }
702
- }
703
- //# sourceMappingURL=prune.js.map