@_linked/cli 1.43.1 → 1.44.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.
- package/CHANGELOG.md +15 -0
- package/README.md +11 -8
- package/lib/esm/cli.js +4 -10
- package/lib/esm/cli.js.map +1 -1
- package/lib/esm/client-dep-includes.d.ts +9 -3
- package/lib/esm/client-dep-includes.d.ts.map +1 -1
- package/lib/esm/client-dep-includes.js +10 -4
- package/lib/esm/client-dep-includes.js.map +1 -1
- package/lib/esm/commands/localize.d.ts +1 -1
- package/lib/esm/commands/localize.d.ts.map +1 -1
- package/lib/esm/commands/localize.js +15 -2
- package/lib/esm/commands/localize.js.map +1 -1
- package/lib/esm/localize/ensure.d.ts +4 -2
- package/lib/esm/localize/ensure.d.ts.map +1 -1
- package/lib/esm/localize/ensure.js +26 -41
- package/lib/esm/localize/ensure.js.map +1 -1
- package/lib/esm/localize/list.d.ts +2 -0
- package/lib/esm/localize/list.d.ts.map +1 -1
- package/lib/esm/localize/list.js +35 -1
- package/lib/esm/localize/list.js.map +1 -1
- package/lib/esm/localize/localize.d.ts.map +1 -1
- package/lib/esm/localize/localize.js +1 -1
- package/lib/esm/localize/localize.js.map +1 -1
- package/lib/esm/localize/manifest.d.ts +9 -0
- package/lib/esm/localize/manifest.d.ts.map +1 -1
- package/lib/esm/localize/manifest.js +2 -2
- package/lib/esm/localize/manifest.js.map +1 -1
- package/lib/esm/localize/one-copy-hook.d.ts +16 -0
- package/lib/esm/localize/one-copy-hook.d.ts.map +1 -0
- package/lib/esm/localize/one-copy-hook.js +20 -0
- package/lib/esm/localize/one-copy-hook.js.map +1 -0
- package/lib/esm/localize/prune.d.ts +115 -8
- package/lib/esm/localize/prune.d.ts.map +1 -1
- package/lib/esm/localize/prune.js +355 -126
- package/lib/esm/localize/prune.js.map +1 -1
- package/lib/esm/localize/relink.d.ts +3 -1
- package/lib/esm/localize/relink.d.ts.map +1 -1
- package/lib/esm/localize/relink.js +10 -5
- package/lib/esm/localize/relink.js.map +1 -1
- package/package.json +1 -1
|
@@ -16,35 +16,47 @@
|
|
|
16
16
|
* the same core.
|
|
17
17
|
*
|
|
18
18
|
* What it does. For every localized checkout, each package at the top of its
|
|
19
|
-
* `node_modules` that is a CANDIDATE (below) is removed when
|
|
20
|
-
* it -- `<appRoot>/node_modules/<name>` exists -- at a
|
|
21
|
-
* every range that resolves to that copy: the
|
|
22
|
-
* that of every installed package that would
|
|
23
|
-
* then finds the app's copy instead. Nothing
|
|
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.
|
|
24
25
|
*
|
|
25
|
-
* The candidates
|
|
26
|
-
* run-time check and Vite's dedupe use too --
|
|
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:
|
|
27
28
|
*
|
|
28
29
|
* - a **localized sibling** -- always counts as provided, whatever the range,
|
|
29
30
|
* because the live checkout is the point of localizing it. A range it does
|
|
30
31
|
* not satisfy is said out loud, never acted on;
|
|
31
|
-
* -
|
|
32
|
-
* `optionalDependencies`)
|
|
33
|
-
* version satisfying every localized checkout's range
|
|
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;
|
|
34
35
|
* - `react` and `react-dom`.
|
|
35
36
|
*
|
|
36
|
-
* Nothing else is touched --
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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.
|
|
40
42
|
*
|
|
41
43
|
* Never removed, candidate or not:
|
|
42
44
|
*
|
|
43
|
-
* - a copy whose ranges the app's version does not satisfy
|
|
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;
|
|
45
51
|
* - anything in a checkout that is not under the app root, because then the
|
|
46
52
|
* upward search never reaches the app's `node_modules` and the removed
|
|
47
|
-
* package would simply be missing
|
|
53
|
+
* package would simply be missing;
|
|
54
|
+
* - anything in a checkout recorded with `prune: false` (localized with
|
|
55
|
+
* `--no-prune`).
|
|
56
|
+
*
|
|
57
|
+
* Kept copies are reported once per checkout, as a summary; `--list` gives
|
|
58
|
+
* each one's reason. A removal can make another copy removable, so a prune
|
|
59
|
+
* repeats until a pass removes nothing.
|
|
48
60
|
*
|
|
49
61
|
* A package with a `bin` is NOT exempt. Measured in a checkout under an app
|
|
50
62
|
* root with its own `@_linked/cli` removed: `npx linked build`,
|
|
@@ -65,7 +77,7 @@ import path from 'node:path';
|
|
|
65
77
|
import { isInside, isSymlink, readJson } from './fsops.js';
|
|
66
78
|
import semver from 'semver';
|
|
67
79
|
import { providedPackages } from './provided.js';
|
|
68
|
-
/** The
|
|
80
|
+
/** The rule (`providedPackages`, provided.ts) over `entries`, plus every entry (sibling). */
|
|
69
81
|
export function candidatesFor(entries, deps) {
|
|
70
82
|
const rule = providedPackages(deps.appRoot, Object.keys(entries));
|
|
71
83
|
return {
|
|
@@ -97,6 +109,10 @@ export function satisfies(version, range) {
|
|
|
97
109
|
export function shouldPrune(opts = {}) {
|
|
98
110
|
return opts.prune !== false;
|
|
99
111
|
}
|
|
112
|
+
/** Is pruning on for this recorded checkout? `--no-prune` at localize/adopt records `prune: false`. */
|
|
113
|
+
export function prunesEntry(entry) {
|
|
114
|
+
return entry.prune !== false;
|
|
115
|
+
}
|
|
100
116
|
const REQUIRE_FIELDS = [
|
|
101
117
|
'dependencies',
|
|
102
118
|
'peerDependencies',
|
|
@@ -108,45 +124,103 @@ const DECLARE_FIELDS = [
|
|
|
108
124
|
'peerDependencies',
|
|
109
125
|
'optionalDependencies',
|
|
110
126
|
];
|
|
127
|
+
/** Candidates in every checkout, declared or not: a second React breaks hooks whoever loads it. */
|
|
128
|
+
const ALWAYS_CANDIDATES = new Set(['react', 'react-dom']);
|
|
129
|
+
/** prune repeats until a pass removes nothing, at most this many passes. */
|
|
130
|
+
export const MAX_PRUNE_PASSES = 5;
|
|
131
|
+
export function newPruneCache() {
|
|
132
|
+
return { indexes: new Map(), builds: 0 };
|
|
133
|
+
}
|
|
111
134
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
135
|
+
* Decide, for every recorded checkout and every candidate copy in its
|
|
136
|
+
* `node_modules`, whether pruning removes it -- without removing anything.
|
|
137
|
+
* `pruneProvided` applies it; the run-time check (`checkOneCopy`) and
|
|
138
|
+
* `--list` call it on its own, which is why it has to be cheap: a copy whose
|
|
139
|
+
* checkout's own range rules it out is decided from that `package.json`
|
|
140
|
+
* alone, and every other range comes from one requirement index per
|
|
141
|
+
* `node_modules`, built at most once per call.
|
|
142
|
+
*
|
|
143
|
+
* Candidates are PER OWNER: a copy is a candidate in checkout O when the rule
|
|
144
|
+
* provides it AND it is a localized sibling, `react`/`react-dom`, or one of
|
|
145
|
+
* O's own runtime dependencies (`dependencies`, `peerDependencies`,
|
|
146
|
+
* `optionalDependencies`). O's devDependencies are O's tooling, whatever
|
|
147
|
+
* another checkout declares at runtime.
|
|
116
148
|
*/
|
|
117
|
-
export function
|
|
149
|
+
export function planPrune(entries, deps, scope = {}, cache = newPruneCache(), candidates = candidatesFor(entries, deps)) {
|
|
118
150
|
var _a;
|
|
119
|
-
const
|
|
120
|
-
const kept = [];
|
|
151
|
+
const plan = { remove: [], keep: [], outside: [] };
|
|
121
152
|
const appRoot = realOrSelf(deps.appRoot);
|
|
122
|
-
const candidates = candidatesFor(entries, deps);
|
|
123
153
|
const only = scope.deps ? new Set(scope.deps) : null;
|
|
124
154
|
for (const [name, entry] of Object.entries(entries)) {
|
|
125
155
|
if (scope.owners && !scope.owners.includes(name))
|
|
126
156
|
continue;
|
|
157
|
+
if (!prunesEntry(entry))
|
|
158
|
+
continue;
|
|
127
159
|
const pkgDir = path.join(deps.appRoot, entry.path);
|
|
128
160
|
if (!fs.existsSync(path.join(pkgDir, 'package.json')))
|
|
129
161
|
continue;
|
|
130
162
|
if (!isInside(realOrSelf(pkgDir), appRoot)) {
|
|
131
|
-
|
|
132
|
-
`node_modules from it — its own dependencies are left as they are.`);
|
|
163
|
+
plan.outside.push({ name, path: entry.path });
|
|
133
164
|
continue;
|
|
134
165
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
166
|
+
const pkg = (_a = readJson(path.join(pkgDir, 'package.json'))) !== null && _a !== void 0 ? _a : {};
|
|
167
|
+
const runtime = new Set(REQUIRE_FIELDS.flatMap((f) => { var _a; return Object.keys((_a = pkg[f]) !== null && _a !== void 0 ? _a : {}); }));
|
|
168
|
+
// The names that can be a decision here, looked up directly rather than
|
|
169
|
+
// by listing node_modules (thousands of entries per checkout): this
|
|
170
|
+
// checkout's candidates, and what the rule skipped that it declares.
|
|
171
|
+
const names = new Set();
|
|
172
|
+
for (const n of candidates.names) {
|
|
173
|
+
if (entries[n] || ALWAYS_CANDIDATES.has(n) || runtime.has(n))
|
|
174
|
+
names.add(n);
|
|
175
|
+
}
|
|
176
|
+
for (const n of candidates.skipped.keys()) {
|
|
177
|
+
if (declared(pkg, DECLARE_FIELDS, n) !== undefined)
|
|
178
|
+
names.add(n);
|
|
179
|
+
}
|
|
180
|
+
names.delete(name);
|
|
181
|
+
const ctx = { name, entry, pkg, names, entries, candidates, only, deps };
|
|
145
182
|
for (const nm of nodeModulesRoots(pkgDir, entry)) {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
183
|
+
for (const d of decideIn(nm, ctx, cache)) {
|
|
184
|
+
(d.action === 'remove' ? plan.remove : plan.keep).push(d);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return plan;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* @param entries the localized packages: every one counts as a sibling
|
|
192
|
+
* @param opts unused beyond `prune`, which callers check with `shouldPrune`
|
|
193
|
+
* @param deps `{appRoot, log, warn}`
|
|
194
|
+
* @param scope prune only some checkouts / names (the run-time check)
|
|
195
|
+
*
|
|
196
|
+
* A removal can make another copy removable -- the copy that went was the one
|
|
197
|
+
* asking a range the app misses, or the one a sibling copy's dependency
|
|
198
|
+
* resolved to -- so it plans and removes again until a pass removes nothing
|
|
199
|
+
* (at most `MAX_PRUNE_PASSES`): one call converges. A checkout recorded with
|
|
200
|
+
* `prune: false` is never touched.
|
|
201
|
+
*/
|
|
202
|
+
export function pruneProvided(entries, opts, deps, scope = {}, cache = newPruneCache()) {
|
|
203
|
+
const removed = [];
|
|
204
|
+
let kept = [];
|
|
205
|
+
const candidates = candidatesFor(entries, deps);
|
|
206
|
+
for (let pass = 0; pass < MAX_PRUNE_PASSES; pass++) {
|
|
207
|
+
const passScope = pass === 0 ? scope : { owners: scope.owners };
|
|
208
|
+
const plan = planPrune(entries, deps, passScope, cache, candidates);
|
|
209
|
+
if (pass === 0) {
|
|
210
|
+
for (const o of plan.outside) {
|
|
211
|
+
deps.log(`[localize] ${o.name}: ${o.path} is outside the app root, so Node cannot reach the app's ` +
|
|
212
|
+
`node_modules from it — its own dependencies are left as they are.`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const d of plan.remove) {
|
|
216
|
+
fs.rmSync(d.dir, { recursive: true, force: true });
|
|
217
|
+
removeEmptyScope(d.nm, d.dep);
|
|
218
|
+
removeBinLinksInto(d.nm, d.dir);
|
|
219
|
+
removed.push(d);
|
|
149
220
|
}
|
|
221
|
+
kept = plan.keep;
|
|
222
|
+
if (!plan.remove.length)
|
|
223
|
+
break;
|
|
150
224
|
}
|
|
151
225
|
report(removed, kept, deps);
|
|
152
226
|
return { removed, kept };
|
|
@@ -156,9 +230,10 @@ export function pruneProvided(entries, opts, deps, scope = {}) {
|
|
|
156
230
|
* so that its absence from the checkout's own `node_modules` is not a
|
|
157
231
|
* dependency gone missing? `--relink` asks this before reinstalling a
|
|
158
232
|
* checkout, so a copy pruned on purpose does not trigger a reinstall on every
|
|
159
|
-
* postinstall.
|
|
233
|
+
* postinstall. `pkg` is the checkout's `package.json`: only what would be a
|
|
234
|
+
* candidate in THAT checkout counts.
|
|
160
235
|
*/
|
|
161
|
-
export function isProvidedByApp(dep, range, entries, candidates, deps) {
|
|
236
|
+
export function isProvidedByApp(dep, range, entries, candidates, deps, pkg) {
|
|
162
237
|
const app = appCopy(dep, deps);
|
|
163
238
|
if (!app)
|
|
164
239
|
return false;
|
|
@@ -166,20 +241,21 @@ export function isProvidedByApp(dep, range, entries, candidates, deps) {
|
|
|
166
241
|
return true;
|
|
167
242
|
if (!candidates.names.has(dep))
|
|
168
243
|
return false;
|
|
244
|
+
if (pkg &&
|
|
245
|
+
!ALWAYS_CANDIDATES.has(dep) &&
|
|
246
|
+
declared(pkg, REQUIRE_FIELDS, dep) === undefined)
|
|
247
|
+
return false;
|
|
169
248
|
return range === undefined || satisfies(app.version, range) === true;
|
|
170
249
|
}
|
|
171
|
-
function
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
for (const dep of topLevelPackages(nm)) {
|
|
176
|
-
if (dep === owner)
|
|
177
|
-
continue;
|
|
250
|
+
function decideIn(nm, ctx, cache) {
|
|
251
|
+
const out = [];
|
|
252
|
+
const { name: owner, entry, pkg, names, entries, candidates, only, deps } = ctx;
|
|
253
|
+
for (const dep of [...names].sort()) {
|
|
178
254
|
if (only && !only.has(dep))
|
|
179
255
|
continue;
|
|
180
256
|
const skip = candidates.skipped.get(dep);
|
|
181
|
-
|
|
182
|
-
|
|
257
|
+
const inRule = candidates.names.has(dep);
|
|
258
|
+
const sibling = Boolean(entries[dep]);
|
|
183
259
|
const dir = path.join(nm, dep);
|
|
184
260
|
const nested = readJson(path.join(dir, 'package.json'));
|
|
185
261
|
if (!nested)
|
|
@@ -189,57 +265,211 @@ function pruneOne(nm, ctx) {
|
|
|
189
265
|
continue; // the app does not provide it, so this copy is the only one
|
|
190
266
|
if (realOrSelf(dir) === app.real)
|
|
191
267
|
continue; // already the app's copy
|
|
192
|
-
const
|
|
193
|
-
|
|
268
|
+
const base = {
|
|
269
|
+
ownerName: owner,
|
|
270
|
+
owner: entry.path,
|
|
271
|
+
dep,
|
|
272
|
+
dir,
|
|
273
|
+
where: path.relative(deps.appRoot, dir),
|
|
274
|
+
nm,
|
|
275
|
+
version: nested.version,
|
|
276
|
+
app: app.version,
|
|
277
|
+
sibling,
|
|
278
|
+
};
|
|
279
|
+
if (!inRule) {
|
|
194
280
|
// The rule leaves it out: a localized checkout asks a range the app
|
|
195
281
|
// misses. Said for a checkout that declares it; a copy it only has
|
|
196
282
|
// transitively is none of its business.
|
|
197
283
|
if (declared(pkg, DECLARE_FIELDS, dep) === undefined)
|
|
198
284
|
continue;
|
|
199
|
-
|
|
200
|
-
where,
|
|
201
|
-
dep,
|
|
202
|
-
version: nested.version,
|
|
203
|
-
app: app.version,
|
|
204
|
-
unmet: skip.asks,
|
|
205
|
-
owner: entry.path,
|
|
206
|
-
});
|
|
285
|
+
out.push(Object.assign(Object.assign({}, base), { action: 'keep', reason: 'range', unmet: skip.asks }));
|
|
207
286
|
continue;
|
|
208
287
|
}
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
288
|
+
const own = declared(pkg, DECLARE_FIELDS, dep);
|
|
289
|
+
const ownAsk = own === undefined ? [] : [{ from: owner, range: own }];
|
|
290
|
+
if (sibling) {
|
|
291
|
+
// Always removed: the live checkout is the point of localizing it. A
|
|
292
|
+
// range it misses is drift, said out loud and never acted on.
|
|
293
|
+
const ranges = [...ownAsk, ...requirementIndex(nm, cache).asks(dep)];
|
|
294
|
+
out.push(Object.assign(Object.assign({}, base), { action: 'remove', unmet: ranges.filter((r) => satisfies(app.version, r.range) === false) }));
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// The checkout's own range first: when it rules the copy out, no index is needed.
|
|
298
|
+
if (own !== undefined && satisfies(app.version, own) !== true) {
|
|
299
|
+
out.push(Object.assign(Object.assign({}, base), { action: 'keep', reason: 'range', unmet: ownAsk }));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const unmet = requirementIndex(nm, cache)
|
|
303
|
+
.asks(dep)
|
|
304
|
+
.filter((r) => satisfies(app.version, r.range) !== true);
|
|
305
|
+
if (unmet.length) {
|
|
306
|
+
out.push(Object.assign(Object.assign({}, base), { action: 'keep', reason: 'range', unmet }));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const context = contextChange(dir, app.real, nested);
|
|
310
|
+
if (context) {
|
|
311
|
+
out.push(Object.assign(Object.assign({}, base), { action: 'keep', reason: 'context', unmet: [], context }));
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
out.push(Object.assign(Object.assign({}, base), { action: 'remove', unmet: [] }));
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Would removing the copy at `copyDir` change what ITS OWN dependencies
|
|
320
|
+
* resolve to? Removing it hands its importers the app's copy, which resolves
|
|
321
|
+
* its dependencies from the app root -- while the checkout keeps its own copy
|
|
322
|
+
* of everything that failed the guard. Measured: a checkout kept its
|
|
323
|
+
* `react`/`@types/react` 18 (its range), lost `prism-react-renderer` (in
|
|
324
|
+
* range), and the app's copy of that resolved `react` -- and with it the
|
|
325
|
+
* types -- to the app's 19: the checkout's own `tsc` saw both React type sets
|
|
326
|
+
* and failed. So the copy stays when any of its `dependencies`,
|
|
327
|
+
* `peerDependencies` or `optionalDependencies` (and the `@types/` package
|
|
328
|
+
* TypeScript would take for it) resolves, from the copy's place in the
|
|
329
|
+
* checkout, to a different package than from the app's copy: a different real
|
|
330
|
+
* path that is not the same `name@version`, or nothing at all from the app's.
|
|
331
|
+
* The same version at another path is the same code, so it does not count; a
|
|
332
|
+
* dependency the checkout cannot resolve either is no change.
|
|
333
|
+
*/
|
|
334
|
+
function contextChange(copyDir, appReal, pkg) {
|
|
335
|
+
var _a;
|
|
336
|
+
const names = new Set();
|
|
337
|
+
for (const f of REQUIRE_FIELDS) {
|
|
338
|
+
for (const d of Object.keys((_a = pkg[f]) !== null && _a !== void 0 ? _a : {})) {
|
|
339
|
+
names.add(d);
|
|
340
|
+
names.add(typesPackageFor(d));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const d of names) {
|
|
344
|
+
const here = resolveFrom(copyDir, d);
|
|
345
|
+
if (!here)
|
|
346
|
+
continue;
|
|
347
|
+
const there = resolveFrom(appReal, d);
|
|
348
|
+
if (!there)
|
|
349
|
+
return { dep: d, here: here.version, app: null };
|
|
350
|
+
if (here.real !== there.real && here.version !== there.version)
|
|
351
|
+
return { dep: d, here: here.version, app: there.version };
|
|
352
|
+
}
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
/** `react` → `@types/react`; `@scope/name` → `@types/scope__name`; an `@types/` name is itself. */
|
|
356
|
+
function typesPackageFor(name) {
|
|
357
|
+
if (name.startsWith('@types/'))
|
|
358
|
+
return name;
|
|
359
|
+
return `@types/${name.startsWith('@') ? name.slice(1).replace('/', '__') : name}`;
|
|
360
|
+
}
|
|
361
|
+
/** Node's upward search for `name` from the directory `from`: the first `node_modules/<name>`. */
|
|
362
|
+
function resolveFrom(from, name) {
|
|
363
|
+
for (let dir = from;;) {
|
|
364
|
+
if (path.basename(dir) !== 'node_modules') {
|
|
365
|
+
const candidate = path.join(dir, 'node_modules', name);
|
|
366
|
+
const pkg = readJson(path.join(candidate, 'package.json'));
|
|
367
|
+
if (pkg)
|
|
368
|
+
return { real: realOrSelf(candidate), version: String(pkg.version) };
|
|
369
|
+
}
|
|
370
|
+
const parent = path.dirname(dir);
|
|
371
|
+
if (parent === dir)
|
|
372
|
+
return null;
|
|
373
|
+
dir = parent;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** The package's directory, and the `node_modules` directories between `nm` and it (its own included). */
|
|
377
|
+
function locate(nm, i) {
|
|
378
|
+
if (!i.located) {
|
|
379
|
+
const segs = i.rel.split('/');
|
|
380
|
+
const dir = path.join(nm, ...segs);
|
|
381
|
+
const between = [path.join(dir, 'node_modules')];
|
|
382
|
+
for (let k = 0; k < segs.length; k++) {
|
|
383
|
+
if (segs[k] === 'node_modules')
|
|
384
|
+
between.push(path.join(nm, ...segs.slice(0, k + 1)));
|
|
385
|
+
}
|
|
386
|
+
i.located = { dir, between };
|
|
387
|
+
}
|
|
388
|
+
return i.located;
|
|
389
|
+
}
|
|
390
|
+
function requirementIndex(nm, cache) {
|
|
391
|
+
var _a;
|
|
392
|
+
let index = cache.indexes.get(nm);
|
|
393
|
+
if (!index) {
|
|
394
|
+
cache.builds++;
|
|
395
|
+
const byDep = (_a = fromHiddenLockfile(nm)) !== null && _a !== void 0 ? _a : fromWalk(nm);
|
|
396
|
+
index = {
|
|
397
|
+
asks(dep) {
|
|
398
|
+
var _a;
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const { installed, range } of (_a = byDep.get(dep)) !== null && _a !== void 0 ? _a : []) {
|
|
401
|
+
if (installed.name === dep)
|
|
402
|
+
continue;
|
|
403
|
+
const { dir, between } = locate(nm, installed);
|
|
404
|
+
if (!fs.existsSync(path.join(dir, 'package.json')))
|
|
405
|
+
continue;
|
|
406
|
+
if (between.some((d) => fs.existsSync(path.join(d, dep))))
|
|
407
|
+
continue;
|
|
408
|
+
out.push({ from: `${installed.name}@${installed.version}`, range });
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
cache.indexes.set(nm, index);
|
|
414
|
+
}
|
|
415
|
+
return index;
|
|
416
|
+
}
|
|
417
|
+
/** Record what the package at `rel` (below `nm`) requires. */
|
|
418
|
+
function addRequirer(byDep, rel, pkg) {
|
|
419
|
+
var _a;
|
|
420
|
+
const at = rel.lastIndexOf('/node_modules/');
|
|
421
|
+
const installed = {
|
|
422
|
+
name: at === -1 ? rel : rel.slice(at + '/node_modules/'.length),
|
|
423
|
+
version: pkg.version,
|
|
424
|
+
rel,
|
|
425
|
+
};
|
|
426
|
+
const seen = new Set();
|
|
427
|
+
for (const f of REQUIRE_FIELDS) {
|
|
428
|
+
for (const [dep, range] of Object.entries((_a = pkg[f]) !== null && _a !== void 0 ? _a : {})) {
|
|
429
|
+
if (typeof range !== 'string' || seen.has(dep))
|
|
222
430
|
continue;
|
|
223
|
-
|
|
431
|
+
seen.add(dep);
|
|
432
|
+
let list = byDep.get(dep);
|
|
433
|
+
if (!list)
|
|
434
|
+
byDep.set(dep, (list = []));
|
|
435
|
+
list.push({ installed, range });
|
|
224
436
|
}
|
|
225
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
226
|
-
removeEmptyScope(nm, dep);
|
|
227
|
-
removeBinLinksInto(nm, dir);
|
|
228
|
-
removed.push({
|
|
229
|
-
owner: entry.path,
|
|
230
|
-
dep,
|
|
231
|
-
version: nested.version,
|
|
232
|
-
app: app.version,
|
|
233
|
-
sibling,
|
|
234
|
-
drift: sibling
|
|
235
|
-
? ranges.filter((r) => satisfies(app.version, r.range) === false)
|
|
236
|
-
: [],
|
|
237
|
-
});
|
|
238
437
|
}
|
|
239
|
-
|
|
438
|
+
}
|
|
439
|
+
const NODE_MODULES_PREFIX = 'node_modules/';
|
|
440
|
+
function fromHiddenLockfile(nm) {
|
|
441
|
+
const lock = readJson(path.join(nm, '.package-lock.json'));
|
|
442
|
+
if (!(lock === null || lock === void 0 ? void 0 : lock.packages) || typeof lock.packages !== 'object')
|
|
443
|
+
return null;
|
|
444
|
+
const byDep = new Map();
|
|
445
|
+
for (const [key, meta] of Object.entries(lock.packages)) {
|
|
446
|
+
if (!key.startsWith(NODE_MODULES_PREFIX) || !meta || meta.link)
|
|
447
|
+
continue;
|
|
448
|
+
addRequirer(byDep, key.slice(NODE_MODULES_PREFIX.length), meta);
|
|
449
|
+
}
|
|
450
|
+
return byDep;
|
|
451
|
+
}
|
|
452
|
+
function fromWalk(nm) {
|
|
453
|
+
const byDep = new Map();
|
|
454
|
+
const walk = (dir, prefix) => {
|
|
455
|
+
for (const name of topLevelPackages(dir)) {
|
|
456
|
+
const pkgDir = path.join(dir, name);
|
|
457
|
+
if (isSymlink(pkgDir))
|
|
458
|
+
continue; // resolves from its real path, which is not here
|
|
459
|
+
const rel = prefix + name;
|
|
460
|
+
const pkg = readJson(path.join(pkgDir, 'package.json'));
|
|
461
|
+
if (pkg)
|
|
462
|
+
addRequirer(byDep, rel, pkg);
|
|
463
|
+
const inner = path.join(pkgDir, 'node_modules');
|
|
464
|
+
if (fs.existsSync(inner))
|
|
465
|
+
walk(inner, `${rel}/node_modules/`);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
walk(nm, '');
|
|
469
|
+
return byDep;
|
|
240
470
|
}
|
|
241
471
|
function report(removed, kept, deps) {
|
|
242
|
-
var _a;
|
|
472
|
+
var _a, _b;
|
|
243
473
|
const byOwner = new Map();
|
|
244
474
|
for (const r of removed)
|
|
245
475
|
byOwner.set(r.owner, [...((_a = byOwner.get(r.owner)) !== null && _a !== void 0 ? _a : []), r]);
|
|
@@ -249,48 +479,47 @@ function report(removed, kept, deps) {
|
|
|
249
479
|
.map((r) => `${r.dep}@${r.version} (app: ${r.sibling ? `localized, ${r.app}` : r.app})`)
|
|
250
480
|
.join(', '));
|
|
251
481
|
for (const r of list) {
|
|
252
|
-
for (const d of r.
|
|
482
|
+
for (const d of r.unmet) {
|
|
253
483
|
deps.warn(`[localize] ${owner}: ${d.from} asks ${r.dep}@${d.range}; the localized checkout is ${r.app}. ` +
|
|
254
484
|
`It is used anyway — that is what localizing it means — but the range is now fiction.`);
|
|
255
485
|
}
|
|
256
486
|
}
|
|
257
487
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
488
|
+
const keptBy = new Map();
|
|
489
|
+
for (const k of kept)
|
|
490
|
+
keptBy.set(k.owner, [...((_b = keptBy.get(k.owner)) !== null && _b !== void 0 ? _b : []), k]);
|
|
491
|
+
for (const [owner, list] of keptBy) {
|
|
492
|
+
const one = list.length === 1;
|
|
493
|
+
deps.warn(`[localize] ${owner}: kept ${list.length} own ${one ? 'copy' : 'copies'} the app's cannot ` +
|
|
494
|
+
`replace: ${summarizeKept(list)} — so the app loads ${one ? 'it' : 'them'} twice. ` +
|
|
495
|
+
`\`linked localize --list\` says why.`);
|
|
265
496
|
}
|
|
266
497
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
walk(nm, []);
|
|
293
|
-
return out;
|
|
498
|
+
const SUMMARY_LIMIT = 8;
|
|
499
|
+
/** `react@18.3.1 (app 19.2.0), prism-react-renderer@2.4.1 (its react would change), … and 4 more` */
|
|
500
|
+
export function summarizeKept(list) {
|
|
501
|
+
const shown = list.slice(0, SUMMARY_LIMIT).map(keptShort);
|
|
502
|
+
const more = list.length - shown.length;
|
|
503
|
+
return shown.join(', ') + (more > 0 ? `, … and ${more} more` : '');
|
|
504
|
+
}
|
|
505
|
+
function keptShort(k) {
|
|
506
|
+
return k.reason === 'context'
|
|
507
|
+
? `${k.dep}@${k.version} (its ${k.context.dep} would change)`
|
|
508
|
+
: `${k.dep}@${k.version} (app ${k.app})`;
|
|
509
|
+
}
|
|
510
|
+
/** The full reason a copy stays, for `--list`. */
|
|
511
|
+
export function explainKept(k) {
|
|
512
|
+
if (k.reason === 'context') {
|
|
513
|
+
const c = k.context;
|
|
514
|
+
return (`${k.where}@${k.version}: from the checkout its ${c.dep} is ${c.here}, from the app's copy ` +
|
|
515
|
+
`${c.app === null ? 'nothing' : c.app} — removing it would change what it loads`);
|
|
516
|
+
}
|
|
517
|
+
return (`${k.where}@${k.version}: ` +
|
|
518
|
+
k.unmet.map((u) => `${u.from} asks ${k.dep}@${u.range}`).join(', ') +
|
|
519
|
+
`, and the app has ${k.dep}@${k.app}` +
|
|
520
|
+
(k.unmet.some((u) => satisfies(k.app, u.range) === null)
|
|
521
|
+
? ' (or the range is one localize cannot read)'
|
|
522
|
+
: ''));
|
|
294
523
|
}
|
|
295
524
|
function declared(pkg, fields, dep) {
|
|
296
525
|
var _a;
|