@_linked/cli 1.41.0 → 1.42.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 +23 -0
- package/README.md +21 -54
- package/lib/esm/cli-methods.js +2 -2
- package/lib/esm/cli-methods.js.map +1 -1
- package/lib/esm/cli.js +13 -1
- package/lib/esm/cli.js.map +1 -1
- package/lib/esm/commands/localize.d.ts +6 -15
- package/lib/esm/commands/localize.d.ts.map +1 -1
- package/lib/esm/commands/localize.js +26 -35
- package/lib/esm/commands/localize.js.map +1 -1
- package/lib/esm/installed-packages.d.ts +12 -12
- package/lib/esm/installed-packages.d.ts.map +1 -1
- package/lib/esm/installed-packages.js +37 -27
- package/lib/esm/installed-packages.js.map +1 -1
- package/lib/esm/interfaces.d.ts +1 -1
- package/lib/esm/interfaces.js.map +1 -1
- package/lib/esm/lifecycle.d.ts +4 -3
- package/lib/esm/lifecycle.d.ts.map +1 -1
- package/lib/esm/lifecycle.js +6 -14
- package/lib/esm/lifecycle.js.map +1 -1
- package/lib/esm/localize/adopt.d.ts +10 -0
- package/lib/esm/localize/adopt.d.ts.map +1 -0
- package/lib/esm/localize/adopt.js +76 -0
- package/lib/esm/localize/adopt.js.map +1 -0
- package/lib/esm/localize/delocalize.d.ts +6 -0
- package/lib/esm/localize/delocalize.d.ts.map +1 -0
- package/lib/esm/localize/delocalize.js +138 -0
- package/lib/esm/localize/delocalize.js.map +1 -0
- package/lib/esm/localize/ensure.d.ts +26 -0
- package/lib/esm/localize/ensure.d.ts.map +1 -0
- package/lib/esm/localize/ensure.js +152 -0
- package/lib/esm/localize/ensure.js.map +1 -0
- package/lib/esm/localize/errors.d.ts +23 -0
- package/lib/esm/localize/errors.d.ts.map +1 -0
- package/lib/esm/localize/errors.js +26 -0
- package/lib/esm/localize/errors.js.map +1 -0
- package/lib/esm/localize/fsops.d.ts +36 -0
- package/lib/esm/localize/fsops.d.ts.map +1 -0
- package/lib/esm/localize/fsops.js +155 -0
- package/lib/esm/localize/fsops.js.map +1 -0
- package/lib/esm/localize/index.d.ts +29 -0
- package/lib/esm/localize/index.d.ts.map +1 -0
- package/lib/esm/localize/index.js +22 -0
- package/lib/esm/localize/index.js.map +1 -0
- package/lib/esm/localize/list.d.ts +18 -0
- package/lib/esm/localize/list.d.ts.map +1 -0
- package/lib/esm/localize/list.js +174 -0
- package/lib/esm/localize/list.js.map +1 -0
- package/lib/esm/localize/localize.d.ts +84 -0
- package/lib/esm/localize/localize.d.ts.map +1 -0
- package/lib/esm/localize/localize.js +354 -0
- package/lib/esm/localize/localize.js.map +1 -0
- package/lib/esm/localize/manifest.d.ts +54 -0
- package/lib/esm/localize/manifest.d.ts.map +1 -0
- package/lib/esm/localize/manifest.js +133 -0
- package/lib/esm/localize/manifest.js.map +1 -0
- package/lib/esm/localize/provided.d.ts +28 -0
- package/lib/esm/localize/provided.d.ts.map +1 -0
- package/lib/esm/localize/provided.js +89 -0
- package/lib/esm/localize/provided.js.map +1 -0
- package/lib/esm/localize/prune.d.ts +58 -0
- package/lib/esm/localize/prune.d.ts.map +1 -0
- package/lib/esm/localize/prune.js +390 -0
- package/lib/esm/localize/prune.js.map +1 -0
- package/lib/esm/localize/relink.d.ts +11 -0
- package/lib/esm/localize/relink.d.ts.map +1 -0
- package/lib/esm/localize/relink.js +174 -0
- package/lib/esm/localize/relink.js.map +1 -0
- package/lib/esm/localize/resolve.d.ts +47 -0
- package/lib/esm/localize/resolve.d.ts.map +1 -0
- package/lib/esm/localize/resolve.js +153 -0
- package/lib/esm/localize/resolve.js.map +1 -0
- package/lib/esm/localize/run.d.ts +35 -0
- package/lib/esm/localize/run.d.ts.map +1 -0
- package/lib/esm/localize/run.js +30 -0
- package/lib/esm/localize/run.js.map +1 -0
- package/lib/esm/localized-dedupe.d.ts +6 -0
- package/lib/esm/localized-dedupe.d.ts.map +1 -1
- package/lib/esm/localized-dedupe.js +23 -66
- package/lib/esm/localized-dedupe.js.map +1 -1
- package/package.json +1 -5
|
@@ -0,0 +1,390 @@
|
|
|
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 (below) is removed when the app provides
|
|
20
|
+
* it -- `<appRoot>/node_modules/<name>` exists -- at a version that satisfies
|
|
21
|
+
* every range that resolves to that copy: the checkout's own declaration and
|
|
22
|
+
* that of every installed package that would load it. Node's upward search
|
|
23
|
+
* then finds the app's copy instead. Nothing else in the checkout moves.
|
|
24
|
+
*
|
|
25
|
+
* The candidates are `providedPackages` (provided.ts) -- the one rule the
|
|
26
|
+
* run-time check and Vite's dedupe use too -- over the entries being pruned:
|
|
27
|
+
*
|
|
28
|
+
* - a **localized sibling** -- always counts as provided, whatever the range,
|
|
29
|
+
* because the live checkout is the point of localizing it. A range it does
|
|
30
|
+
* not satisfy is said out loud, never acted on;
|
|
31
|
+
* - a **runtime dependency** (`dependencies`, `peerDependencies`,
|
|
32
|
+
* `optionalDependencies`) of any localized checkout that the app has at a
|
|
33
|
+
* version satisfying every localized checkout's range for it;
|
|
34
|
+
* - `react` and `react-dom`.
|
|
35
|
+
*
|
|
36
|
+
* Nothing else is touched -- a devDependency the app happens to have too
|
|
37
|
+
* stays, so the checkout's tooling does not come to depend on the app's. A
|
|
38
|
+
* runtime dependency the rule leaves out because a localized range misses the
|
|
39
|
+
* app's version is kept and said out loud, like any other copy kept below.
|
|
40
|
+
*
|
|
41
|
+
* Never removed, candidate or not:
|
|
42
|
+
*
|
|
43
|
+
* - a copy whose ranges the app's version does not satisfy -- kept, with a
|
|
44
|
+
* warning naming the package, the range and the app's version;
|
|
45
|
+
* - anything in a checkout that is not under the app root, because then the
|
|
46
|
+
* upward search never reaches the app's `node_modules` and the removed
|
|
47
|
+
* package would simply be missing.
|
|
48
|
+
*
|
|
49
|
+
* A package with a `bin` is NOT exempt. Measured in a checkout under an app
|
|
50
|
+
* root with its own `@_linked/cli` removed: `npx linked build`,
|
|
51
|
+
* `npm exec linked -- --help`, a script running `npm exec linked` and a script
|
|
52
|
+
* running a bare `linked` all resolved `linked` to the app root's copy,
|
|
53
|
+
* because npm puts every ancestor's `node_modules/.bin` on the PATH. The
|
|
54
|
+
* removed package's links in the checkout's `node_modules/.bin` are removed
|
|
55
|
+
* with it, so nothing dangles.
|
|
56
|
+
*
|
|
57
|
+
* It never runs npm and never touches the checkout's `package.json` or
|
|
58
|
+
* `package-lock.json`. `npm install` in the checkout puts the copies back;
|
|
59
|
+
* `ensure` (ensure.ts) is the install-then-prune step every in-checkout
|
|
60
|
+
* install goes through, and the run-time check (`checkOneCopy`) takes them out
|
|
61
|
+
* again before `linked start` and friends.
|
|
62
|
+
*/
|
|
63
|
+
import fs from 'node:fs';
|
|
64
|
+
import path from 'node:path';
|
|
65
|
+
import { isInside, isSymlink, readJson } from './fsops.js';
|
|
66
|
+
import semver from 'semver';
|
|
67
|
+
import { providedPackages } from './provided.js';
|
|
68
|
+
/** The candidates for `entries`, by `providedPackages` (provided.ts). */
|
|
69
|
+
export function candidatesFor(entries, deps) {
|
|
70
|
+
const rule = providedPackages(deps.appRoot, Object.keys(entries));
|
|
71
|
+
return {
|
|
72
|
+
names: new Set([...rule.names, ...Object.keys(entries)]),
|
|
73
|
+
skipped: new Map(rule.skipped.map((s) => [s.name, s])),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Does the app's installed `version` satisfy `range`? `null` means "cannot
|
|
78
|
+
* tell" -- a dist-tag, `file:`, `npm:`, `workspace:`, a git URL, or a version
|
|
79
|
+
* that is not one -- and every caller then keeps the copy. The failure mode is
|
|
80
|
+
* therefore always the safe one: a copy kept that could have gone, never a
|
|
81
|
+
* copy removed that was needed.
|
|
82
|
+
*
|
|
83
|
+
* npm's own `semver` decides, so prereleases follow npm's rule: `1.3.0-beta.1`
|
|
84
|
+
* satisfies a range only when one of its comparators names a prerelease on
|
|
85
|
+
* the same `major.minor.patch`.
|
|
86
|
+
*/
|
|
87
|
+
export function satisfies(version, range) {
|
|
88
|
+
if (typeof version !== 'string' || typeof range !== 'string')
|
|
89
|
+
return null;
|
|
90
|
+
if (semver.valid(version) === null)
|
|
91
|
+
return null;
|
|
92
|
+
if (semver.validRange(range) === null)
|
|
93
|
+
return null;
|
|
94
|
+
return semver.satisfies(version, range);
|
|
95
|
+
}
|
|
96
|
+
/** Pruning is on unless a caller says `{prune: false}`. */
|
|
97
|
+
export function shouldPrune(opts = {}) {
|
|
98
|
+
return opts.prune !== false;
|
|
99
|
+
}
|
|
100
|
+
const REQUIRE_FIELDS = [
|
|
101
|
+
'dependencies',
|
|
102
|
+
'peerDependencies',
|
|
103
|
+
'optionalDependencies',
|
|
104
|
+
];
|
|
105
|
+
const DECLARE_FIELDS = [
|
|
106
|
+
'dependencies',
|
|
107
|
+
'devDependencies',
|
|
108
|
+
'peerDependencies',
|
|
109
|
+
'optionalDependencies',
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* @param entries the localized packages: every one counts as a sibling
|
|
113
|
+
* @param opts unused beyond `prune`, which callers check with `shouldPrune`
|
|
114
|
+
* @param deps `{appRoot, log, warn}`
|
|
115
|
+
* @param scope prune only some checkouts / names (the run-time check)
|
|
116
|
+
*/
|
|
117
|
+
export function pruneProvided(entries, opts, deps, scope = {}) {
|
|
118
|
+
var _a;
|
|
119
|
+
const removed = [];
|
|
120
|
+
const kept = [];
|
|
121
|
+
const appRoot = realOrSelf(deps.appRoot);
|
|
122
|
+
const candidates = candidatesFor(entries, deps);
|
|
123
|
+
const only = scope.deps ? new Set(scope.deps) : null;
|
|
124
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
125
|
+
if (scope.owners && !scope.owners.includes(name))
|
|
126
|
+
continue;
|
|
127
|
+
const pkgDir = path.join(deps.appRoot, entry.path);
|
|
128
|
+
if (!fs.existsSync(path.join(pkgDir, 'package.json')))
|
|
129
|
+
continue;
|
|
130
|
+
if (!isInside(realOrSelf(pkgDir), appRoot)) {
|
|
131
|
+
deps.log(`[localize] ${name}: ${entry.path} is outside the app root, so Node cannot reach the app's ` +
|
|
132
|
+
`node_modules from it — its own dependencies are left as they are.`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const ctx = {
|
|
136
|
+
name,
|
|
137
|
+
entry,
|
|
138
|
+
pkgDir,
|
|
139
|
+
pkg: (_a = readJson(path.join(pkgDir, 'package.json'))) !== null && _a !== void 0 ? _a : {},
|
|
140
|
+
entries,
|
|
141
|
+
candidates,
|
|
142
|
+
only,
|
|
143
|
+
deps,
|
|
144
|
+
};
|
|
145
|
+
for (const nm of nodeModulesRoots(pkgDir, entry)) {
|
|
146
|
+
const result = pruneOne(nm, ctx);
|
|
147
|
+
removed.push(...result.removed);
|
|
148
|
+
kept.push(...result.kept);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
report(removed, kept, deps);
|
|
152
|
+
return { removed, kept };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Is `dep` -- declared by a checkout at `range` -- something the app provides,
|
|
156
|
+
* so that its absence from the checkout's own `node_modules` is not a
|
|
157
|
+
* dependency gone missing? `--relink` asks this before reinstalling a
|
|
158
|
+
* checkout, so a copy pruned on purpose does not trigger a reinstall on every
|
|
159
|
+
* postinstall.
|
|
160
|
+
*/
|
|
161
|
+
export function isProvidedByApp(dep, range, entries, candidates, deps) {
|
|
162
|
+
const app = appCopy(dep, deps);
|
|
163
|
+
if (!app)
|
|
164
|
+
return false;
|
|
165
|
+
if (entries[dep])
|
|
166
|
+
return true;
|
|
167
|
+
if (!candidates.names.has(dep))
|
|
168
|
+
return false;
|
|
169
|
+
return range === undefined || satisfies(app.version, range) === true;
|
|
170
|
+
}
|
|
171
|
+
function pruneOne(nm, ctx) {
|
|
172
|
+
const removed = [];
|
|
173
|
+
const kept = [];
|
|
174
|
+
const { name: owner, entry, pkg, entries, candidates, only, deps } = ctx;
|
|
175
|
+
for (const dep of topLevelPackages(nm)) {
|
|
176
|
+
if (dep === owner)
|
|
177
|
+
continue;
|
|
178
|
+
if (only && !only.has(dep))
|
|
179
|
+
continue;
|
|
180
|
+
const skip = candidates.skipped.get(dep);
|
|
181
|
+
if (!candidates.names.has(dep) && !skip)
|
|
182
|
+
continue;
|
|
183
|
+
const dir = path.join(nm, dep);
|
|
184
|
+
const nested = readJson(path.join(dir, 'package.json'));
|
|
185
|
+
if (!nested)
|
|
186
|
+
continue;
|
|
187
|
+
const app = appCopy(dep, deps);
|
|
188
|
+
if (!app)
|
|
189
|
+
continue; // the app does not provide it, so this copy is the only one
|
|
190
|
+
if (realOrSelf(dir) === app.real)
|
|
191
|
+
continue; // already the app's copy
|
|
192
|
+
const where = path.relative(deps.appRoot, dir);
|
|
193
|
+
if (!candidates.names.has(dep)) {
|
|
194
|
+
// The rule leaves it out: a localized checkout asks a range the app
|
|
195
|
+
// misses. Said for a checkout that declares it; a copy it only has
|
|
196
|
+
// transitively is none of its business.
|
|
197
|
+
if (declared(pkg, DECLARE_FIELDS, dep) === undefined)
|
|
198
|
+
continue;
|
|
199
|
+
kept.push({
|
|
200
|
+
where,
|
|
201
|
+
dep,
|
|
202
|
+
version: nested.version,
|
|
203
|
+
app: app.version,
|
|
204
|
+
unmet: skip.asks,
|
|
205
|
+
owner: entry.path,
|
|
206
|
+
});
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const sibling = Boolean(entries[dep]);
|
|
210
|
+
const ranges = rangesResolvingTo(nm, dep, pkg, owner);
|
|
211
|
+
if (!sibling) {
|
|
212
|
+
const unmet = ranges.filter((r) => satisfies(app.version, r.range) !== true);
|
|
213
|
+
if (unmet.length) {
|
|
214
|
+
kept.push({
|
|
215
|
+
where,
|
|
216
|
+
dep,
|
|
217
|
+
version: nested.version,
|
|
218
|
+
app: app.version,
|
|
219
|
+
unmet,
|
|
220
|
+
owner: entry.path,
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
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
|
+
}
|
|
239
|
+
return { removed, kept };
|
|
240
|
+
}
|
|
241
|
+
function report(removed, kept, deps) {
|
|
242
|
+
var _a;
|
|
243
|
+
const byOwner = new Map();
|
|
244
|
+
for (const r of removed)
|
|
245
|
+
byOwner.set(r.owner, [...((_a = byOwner.get(r.owner)) !== null && _a !== void 0 ? _a : []), r]);
|
|
246
|
+
for (const [owner, list] of byOwner) {
|
|
247
|
+
deps.log(`[localize] ${owner}: removed its own copies of what the app provides — ` +
|
|
248
|
+
list
|
|
249
|
+
.map((r) => `${r.dep}@${r.version} (app: ${r.sibling ? `localized, ${r.app}` : r.app})`)
|
|
250
|
+
.join(', '));
|
|
251
|
+
for (const r of list) {
|
|
252
|
+
for (const d of r.drift) {
|
|
253
|
+
deps.warn(`[localize] ${owner}: ${d.from} asks ${r.dep}@${d.range}; the localized checkout is ${r.app}. ` +
|
|
254
|
+
`It is used anyway — that is what localizing it means — but the range is now fiction.`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const k of kept) {
|
|
259
|
+
deps.warn(`[localize] kept ${k.where}@${k.version}: ` +
|
|
260
|
+
k.unmet.map((u) => `${u.from} asks ${k.dep}@${u.range}`).join(', ') +
|
|
261
|
+
`, and the app has ${k.dep}@${k.app}${k.unmet.some((u) => satisfies(k.app, u.range) === null)
|
|
262
|
+
? ' (or the range is one localize cannot read)'
|
|
263
|
+
: ''}. ` +
|
|
264
|
+
`${k.owner} loads its own copy, so the app loads two. Align the ranges, or update the app's ${k.dep}.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Every range that resolves to `<nm>/<dep>`: the checkout's own declaration,
|
|
269
|
+
* plus each installed package under `nm` that requires `dep` and has no copy
|
|
270
|
+
* of its own between itself and `nm`.
|
|
271
|
+
*/
|
|
272
|
+
function rangesResolvingTo(nm, dep, checkoutPkg, owner) {
|
|
273
|
+
const out = [];
|
|
274
|
+
const own = declared(checkoutPkg, DECLARE_FIELDS, dep);
|
|
275
|
+
if (own !== undefined)
|
|
276
|
+
out.push({ from: owner, range: own });
|
|
277
|
+
const walk = (dir, chain) => {
|
|
278
|
+
for (const name of topLevelPackages(dir)) {
|
|
279
|
+
const pkgDir = path.join(dir, name);
|
|
280
|
+
if (isSymlink(pkgDir))
|
|
281
|
+
continue; // resolves from its real path, which is not here
|
|
282
|
+
const pkg = readJson(path.join(pkgDir, 'package.json'));
|
|
283
|
+
const range = pkg && name !== dep ? declared(pkg, REQUIRE_FIELDS, dep) : undefined;
|
|
284
|
+
const inner = path.join(pkgDir, 'node_modules');
|
|
285
|
+
const shadowed = [...chain, inner].some((d) => fs.existsSync(path.join(d, dep)));
|
|
286
|
+
if (range !== undefined && !shadowed)
|
|
287
|
+
out.push({ from: `${name}@${pkg.version}`, range });
|
|
288
|
+
if (fs.existsSync(inner))
|
|
289
|
+
walk(inner, [...chain, inner]);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
walk(nm, []);
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
function declared(pkg, fields, dep) {
|
|
296
|
+
var _a;
|
|
297
|
+
for (const f of fields)
|
|
298
|
+
if (((_a = pkg[f]) === null || _a === void 0 ? void 0 : _a[dep]) !== undefined)
|
|
299
|
+
return pkg[f][dep];
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
/** The app's own copy of `dep`: `<appRoot>/node_modules/<dep>`, followed through a link. */
|
|
303
|
+
function appCopy(dep, deps) {
|
|
304
|
+
const dir = path.join(deps.appRoot, 'node_modules', dep);
|
|
305
|
+
const pkg = readJson(path.join(dir, 'package.json'));
|
|
306
|
+
if (!(pkg === null || pkg === void 0 ? void 0 : pkg.version))
|
|
307
|
+
return null;
|
|
308
|
+
return { version: pkg.version, real: realOrSelf(dir) };
|
|
309
|
+
}
|
|
310
|
+
/** The package dir's `node_modules`, and the clone root's when the package is a monorepo subdir. */
|
|
311
|
+
export function nodeModulesRoots(pkgDir, entry) {
|
|
312
|
+
const roots = [path.join(pkgDir, 'node_modules')];
|
|
313
|
+
if (entry.subdir) {
|
|
314
|
+
const rel = path.normalize(entry.subdir);
|
|
315
|
+
if (pkgDir.endsWith(path.sep + rel))
|
|
316
|
+
roots.push(path.join(pkgDir.slice(0, -(rel.length + 1)), 'node_modules'));
|
|
317
|
+
}
|
|
318
|
+
return roots.filter((d) => fs.existsSync(d));
|
|
319
|
+
}
|
|
320
|
+
/** Package names directly under a `node_modules`, scoped ones as `@scope/name`. */
|
|
321
|
+
function topLevelPackages(nm) {
|
|
322
|
+
const out = [];
|
|
323
|
+
let items;
|
|
324
|
+
try {
|
|
325
|
+
items = fs.readdirSync(nm, { withFileTypes: true });
|
|
326
|
+
}
|
|
327
|
+
catch (_a) {
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
for (const item of items) {
|
|
331
|
+
if (item.name.startsWith('.'))
|
|
332
|
+
continue;
|
|
333
|
+
if (item.name.startsWith('@')) {
|
|
334
|
+
let inner = [];
|
|
335
|
+
try {
|
|
336
|
+
inner = fs.readdirSync(path.join(nm, item.name));
|
|
337
|
+
}
|
|
338
|
+
catch (_b) {
|
|
339
|
+
/* not a directory */
|
|
340
|
+
}
|
|
341
|
+
for (const sub of inner)
|
|
342
|
+
if (!sub.startsWith('.'))
|
|
343
|
+
out.push(`${item.name}/${sub}`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
out.push(item.name);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
/** Links in `<nm>/.bin` that pointed into a package just removed: dangling now. */
|
|
352
|
+
function removeBinLinksInto(nm, removedDir) {
|
|
353
|
+
const bin = path.join(nm, '.bin');
|
|
354
|
+
let names;
|
|
355
|
+
try {
|
|
356
|
+
names = fs.readdirSync(bin);
|
|
357
|
+
}
|
|
358
|
+
catch (_a) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
for (const name of names) {
|
|
362
|
+
const link = path.join(bin, name);
|
|
363
|
+
if (!isSymlink(link))
|
|
364
|
+
continue;
|
|
365
|
+
const target = path.resolve(bin, fs.readlinkSync(link));
|
|
366
|
+
if (isInside(target, removedDir))
|
|
367
|
+
fs.unlinkSync(link);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function removeEmptyScope(nm, dep) {
|
|
371
|
+
if (!dep.startsWith('@'))
|
|
372
|
+
return;
|
|
373
|
+
const scope = path.join(nm, dep.split('/')[0]);
|
|
374
|
+
try {
|
|
375
|
+
if (!fs.readdirSync(scope).length)
|
|
376
|
+
fs.rmdirSync(scope);
|
|
377
|
+
}
|
|
378
|
+
catch (_a) {
|
|
379
|
+
/* already gone */
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
export function realOrSelf(p) {
|
|
383
|
+
try {
|
|
384
|
+
return fs.realpathSync(p);
|
|
385
|
+
}
|
|
386
|
+
catch (_a) {
|
|
387
|
+
return p;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
//# sourceMappingURL=prune.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prune.js","sourceRoot":"","sources":["../../../src/localize/prune.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pruning -- stop a checkout's own `node_modules` holding a second copy of\n * something the app already provides. On by default after every install in a\n * checkout and on every `--relink`; `--no-prune` (`{prune: false}`) skips it.\n *\n * `shouldPrune(opts)` is the one place that default lives.\n *\n * Why it is needed. `npm install` inside a checkout (localize.ts, step 3)\n * installs what the CHECKOUT's lockfile says: its own copy of every\n * dependency, often older than the app's. Node resolves a module from the\n * importer's REAL path and takes the first `node_modules` it meets going up,\n * so a localized `A` that imports `B` gets `A/node_modules/B` -- a registry\n * copy -- and never the localized `B`; and a framework package that holds a\n * registry of classes (or React) loads once per copy. Measured with three\n * localized `@_linked/*` packages: plain Node and `tsx` loaded three copies of\n * the same core.\n *\n * What it does. For every localized checkout, each package at the top of its\n * `node_modules` that is a CANDIDATE (below) is removed when the app provides\n * it -- `<appRoot>/node_modules/<name>` exists -- at a version that satisfies\n * every range that resolves to that copy: the checkout's own declaration and\n * that of every installed package that would load it. Node's upward search\n * then finds the app's copy instead. Nothing else in the checkout moves.\n *\n * The candidates are `providedPackages` (provided.ts) -- the one rule the\n * run-time check and Vite's dedupe use too -- over the entries being pruned:\n *\n * - a **localized sibling** -- always counts as provided, whatever the range,\n * because the live checkout is the point of localizing it. A range it does\n * not satisfy is said out loud, never acted on;\n * - a **runtime dependency** (`dependencies`, `peerDependencies`,\n * `optionalDependencies`) of any localized checkout that the app has at a\n * version satisfying every localized checkout's range for it;\n * - `react` and `react-dom`.\n *\n * Nothing else is touched -- a devDependency the app happens to have too\n * stays, so the checkout's tooling does not come to depend on the app's. A\n * runtime dependency the rule leaves out because a localized range misses the\n * app's version is kept and said out loud, like any other copy kept below.\n *\n * Never removed, candidate or not:\n *\n * - a copy whose ranges the app's version does not satisfy -- kept, with a\n * warning naming the package, the range and the app's version;\n * - anything in a checkout that is not under the app root, because then the\n * upward search never reaches the app's `node_modules` and the removed\n * package would simply be missing.\n *\n * A package with a `bin` is NOT exempt. Measured in a checkout under an app\n * root with its own `@_linked/cli` removed: `npx linked build`,\n * `npm exec linked -- --help`, a script running `npm exec linked` and a script\n * running a bare `linked` all resolved `linked` to the app root's copy,\n * because npm puts every ancestor's `node_modules/.bin` on the PATH. The\n * removed package's links in the checkout's `node_modules/.bin` are removed\n * with it, so nothing dangles.\n *\n * It never runs npm and never touches the checkout's `package.json` or\n * `package-lock.json`. `npm install` in the checkout puts the copies back;\n * `ensure` (ensure.ts) is the install-then-prune step every in-checkout\n * install goes through, and the run-time check (`checkOneCopy`) takes them out\n * again before `linked start` and friends.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport {isInside, isSymlink, readJson} from './fsops.js';\nimport semver from 'semver';\nimport {providedPackages} from './provided.js';\nimport type {ProvidedSkip} from './provided.js';\n\nimport type {ManifestEntry} from './manifest.js';\nimport type {Deps} from './run.js';\n\n/** What pruning reads from a command's options. */\nexport interface PruneOptions {\n /** On unless explicitly false. */\n prune?: boolean;\n}\n\n/** Limit a prune to some checkouts and/or some dependency names. */\nexport interface PruneScope {\n /** Only these localized packages' checkouts. */\n owners?: string[];\n /** Only these dependency names. */\n deps?: string[];\n}\n\n/** What pruning may remove: the rule's names plus every localized sibling, and what the rule skipped. */\nexport interface Candidates {\n names: Set<string>;\n skipped: Map<string, ProvidedSkip>;\n}\n\n/** The candidates for `entries`, by `providedPackages` (provided.ts). */\nexport function candidatesFor(\n entries: Record<string, ManifestEntry>,\n deps: Pick<Deps, 'appRoot'>,\n): Candidates {\n const rule = providedPackages(deps.appRoot, Object.keys(entries));\n return {\n names: new Set([...rule.names, ...Object.keys(entries)]),\n skipped: new Map(rule.skipped.map((s) => [s.name, s])),\n };\n}\n\n/**\n * Does the app's installed `version` satisfy `range`? `null` means \"cannot\n * tell\" -- a dist-tag, `file:`, `npm:`, `workspace:`, a git URL, or a version\n * that is not one -- and every caller then keeps the copy. The failure mode is\n * therefore always the safe one: a copy kept that could have gone, never a\n * copy removed that was needed.\n *\n * npm's own `semver` decides, so prereleases follow npm's rule: `1.3.0-beta.1`\n * satisfies a range only when one of its comparators names a prerelease on\n * the same `major.minor.patch`.\n */\nexport function satisfies(version: string, range: string): boolean | null {\n if (typeof version !== 'string' || typeof range !== 'string') return null;\n if (semver.valid(version) === null) return null;\n if (semver.validRange(range) === null) return null;\n return semver.satisfies(version, range);\n}\n\n/** Pruning is on unless a caller says `{prune: false}`. */\nexport function shouldPrune(opts: PruneOptions = {}): boolean {\n return opts.prune !== false;\n}\n\nconst REQUIRE_FIELDS = [\n 'dependencies',\n 'peerDependencies',\n 'optionalDependencies',\n];\nconst DECLARE_FIELDS = [\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n 'optionalDependencies',\n];\n\n/**\n * @param entries the localized packages: every one counts as a sibling\n * @param opts unused beyond `prune`, which callers check with `shouldPrune`\n * @param deps `{appRoot, log, warn}`\n * @param scope prune only some checkouts / names (the run-time check)\n */\nexport function pruneProvided(\n entries: Record<string, ManifestEntry>,\n opts: PruneOptions,\n deps: Pick<Deps, 'appRoot' | 'log' | 'warn'>,\n scope: PruneScope = {},\n): {removed: any[]; kept: any[]} {\n const removed: any[] = [];\n const kept: any[] = [];\n const appRoot = realOrSelf(deps.appRoot);\n const candidates = candidatesFor(entries, deps);\n const only = scope.deps ? new Set(scope.deps) : null;\n\n for (const [name, entry] of Object.entries(entries)) {\n if (scope.owners && !scope.owners.includes(name)) continue;\n const pkgDir = path.join(deps.appRoot, entry.path);\n if (!fs.existsSync(path.join(pkgDir, 'package.json'))) continue;\n if (!isInside(realOrSelf(pkgDir), appRoot)) {\n deps.log(\n `[localize] ${name}: ${entry.path} is outside the app root, so Node cannot reach the app's ` +\n `node_modules from it — its own dependencies are left as they are.`,\n );\n continue;\n }\n const ctx = {\n name,\n entry,\n pkgDir,\n pkg: readJson(path.join(pkgDir, 'package.json')) ?? {},\n entries,\n candidates,\n only,\n deps,\n };\n for (const nm of nodeModulesRoots(pkgDir, entry)) {\n const result = pruneOne(nm, ctx);\n removed.push(...result.removed);\n kept.push(...result.kept);\n }\n }\n\n report(removed, kept, deps);\n return {removed, kept};\n}\n\n/**\n * Is `dep` -- declared by a checkout at `range` -- something the app provides,\n * so that its absence from the checkout's own `node_modules` is not a\n * dependency gone missing? `--relink` asks this before reinstalling a\n * checkout, so a copy pruned on purpose does not trigger a reinstall on every\n * postinstall.\n */\nexport function isProvidedByApp(\n dep: string,\n range: string | undefined,\n entries: Record<string, ManifestEntry>,\n candidates: Candidates,\n deps: Pick<Deps, 'appRoot'>,\n): boolean {\n const app = appCopy(dep, deps);\n if (!app) return false;\n if (entries[dep]) return true;\n if (!candidates.names.has(dep)) return false;\n return range === undefined || satisfies(app.version, range) === true;\n}\n\nfunction pruneOne(nm, ctx) {\n const removed = [];\n const kept = [];\n const {name: owner, entry, pkg, entries, candidates, only, deps} = ctx;\n\n for (const dep of topLevelPackages(nm)) {\n if (dep === owner) continue;\n if (only && !only.has(dep)) continue;\n const skip = candidates.skipped.get(dep);\n if (!candidates.names.has(dep) && !skip) continue;\n const dir = path.join(nm, dep);\n const nested = readJson(path.join(dir, 'package.json'));\n if (!nested) continue;\n const app = appCopy(dep, deps);\n if (!app) continue; // the app does not provide it, so this copy is the only one\n if (realOrSelf(dir) === app.real) continue; // already the app's copy\n\n const where = path.relative(deps.appRoot, dir);\n if (!candidates.names.has(dep)) {\n // The rule leaves it out: a localized checkout asks a range the app\n // misses. Said for a checkout that declares it; a copy it only has\n // transitively is none of its business.\n if (declared(pkg, DECLARE_FIELDS, dep) === undefined) continue;\n kept.push({\n where,\n dep,\n version: nested.version,\n app: app.version,\n unmet: skip!.asks,\n owner: entry.path,\n });\n continue;\n }\n const sibling = Boolean(entries[dep]);\n const ranges = rangesResolvingTo(nm, dep, pkg, owner);\n\n if (!sibling) {\n const unmet = ranges.filter(\n (r) => satisfies(app.version, r.range) !== true,\n );\n if (unmet.length) {\n kept.push({\n where,\n dep,\n version: nested.version,\n app: app.version,\n unmet,\n owner: entry.path,\n });\n continue;\n }\n }\n\n fs.rmSync(dir, {recursive: true, force: true});\n removeEmptyScope(nm, dep);\n removeBinLinksInto(nm, dir);\n removed.push({\n owner: entry.path,\n dep,\n version: nested.version,\n app: app.version,\n sibling,\n drift: sibling\n ? ranges.filter((r) => satisfies(app.version, r.range) === false)\n : [],\n });\n }\n return {removed, kept};\n}\n\nfunction report(removed, kept, deps) {\n const byOwner = new Map();\n for (const r of removed)\n byOwner.set(r.owner, [...(byOwner.get(r.owner) ?? []), r]);\n for (const [owner, list] of byOwner) {\n deps.log(\n `[localize] ${owner}: removed its own copies of what the app provides — ` +\n list\n .map(\n (r) =>\n `${r.dep}@${r.version} (app: ${r.sibling ? `localized, ${r.app}` : r.app})`,\n )\n .join(', '),\n );\n for (const r of list) {\n for (const d of r.drift) {\n deps.warn(\n `[localize] ${owner}: ${d.from} asks ${r.dep}@${d.range}; the localized checkout is ${r.app}. ` +\n `It is used anyway — that is what localizing it means — but the range is now fiction.`,\n );\n }\n }\n }\n for (const k of kept) {\n deps.warn(\n `[localize] kept ${k.where}@${k.version}: ` +\n k.unmet.map((u) => `${u.from} asks ${k.dep}@${u.range}`).join(', ') +\n `, and the app has ${k.dep}@${k.app}${\n k.unmet.some((u) => satisfies(k.app, u.range) === null)\n ? ' (or the range is one localize cannot read)'\n : ''\n }. ` +\n `${k.owner} loads its own copy, so the app loads two. Align the ranges, or update the app's ${k.dep}.`,\n );\n }\n}\n\n/**\n * Every range that resolves to `<nm>/<dep>`: the checkout's own declaration,\n * plus each installed package under `nm` that requires `dep` and has no copy\n * of its own between itself and `nm`.\n */\nfunction rangesResolvingTo(nm, dep, checkoutPkg, owner) {\n const out = [];\n const own = declared(checkoutPkg, DECLARE_FIELDS, dep);\n if (own !== undefined) out.push({from: owner, range: own});\n\n const walk = (dir, chain) => {\n for (const name of topLevelPackages(dir)) {\n const pkgDir = path.join(dir, name);\n if (isSymlink(pkgDir)) continue; // resolves from its real path, which is not here\n const pkg = readJson(path.join(pkgDir, 'package.json'));\n const range =\n pkg && name !== dep ? declared(pkg, REQUIRE_FIELDS, dep) : undefined;\n const inner = path.join(pkgDir, 'node_modules');\n const shadowed = [...chain, inner].some((d) =>\n fs.existsSync(path.join(d, dep)),\n );\n if (range !== undefined && !shadowed)\n out.push({from: `${name}@${pkg.version}`, range});\n if (fs.existsSync(inner)) walk(inner, [...chain, inner]);\n }\n };\n walk(nm, []);\n return out;\n}\n\nfunction declared(pkg, fields, dep) {\n for (const f of fields) if (pkg[f]?.[dep] !== undefined) return pkg[f][dep];\n return undefined;\n}\n\n/** The app's own copy of `dep`: `<appRoot>/node_modules/<dep>`, followed through a link. */\nfunction appCopy(dep, deps) {\n const dir = path.join(deps.appRoot, 'node_modules', dep);\n const pkg = readJson(path.join(dir, 'package.json'));\n if (!pkg?.version) return null;\n return {version: pkg.version, real: realOrSelf(dir)};\n}\n\n/** The package dir's `node_modules`, and the clone root's when the package is a monorepo subdir. */\nexport function nodeModulesRoots(pkgDir, entry) {\n const roots = [path.join(pkgDir, 'node_modules')];\n if (entry.subdir) {\n const rel = path.normalize(entry.subdir);\n if (pkgDir.endsWith(path.sep + rel))\n roots.push(path.join(pkgDir.slice(0, -(rel.length + 1)), 'node_modules'));\n }\n return roots.filter((d) => fs.existsSync(d));\n}\n\n/** Package names directly under a `node_modules`, scoped ones as `@scope/name`. */\nfunction topLevelPackages(nm) {\n const out = [];\n let items;\n try {\n items = fs.readdirSync(nm, {withFileTypes: true});\n } catch {\n return out;\n }\n for (const item of items) {\n if (item.name.startsWith('.')) continue;\n if (item.name.startsWith('@')) {\n let inner = [];\n try {\n inner = fs.readdirSync(path.join(nm, item.name));\n } catch {\n /* not a directory */\n }\n for (const sub of inner)\n if (!sub.startsWith('.')) out.push(`${item.name}/${sub}`);\n } else {\n out.push(item.name);\n }\n }\n return out;\n}\n\n/** Links in `<nm>/.bin` that pointed into a package just removed: dangling now. */\nfunction removeBinLinksInto(nm, removedDir) {\n const bin = path.join(nm, '.bin');\n let names;\n try {\n names = fs.readdirSync(bin);\n } catch {\n return;\n }\n for (const name of names) {\n const link = path.join(bin, name);\n if (!isSymlink(link)) continue;\n const target = path.resolve(bin, fs.readlinkSync(link));\n if (isInside(target, removedDir)) fs.unlinkSync(link);\n }\n}\n\nfunction removeEmptyScope(nm, dep) {\n if (!dep.startsWith('@')) return;\n const scope = path.join(nm, dep.split('/')[0]);\n try {\n if (!fs.readdirSync(scope).length) fs.rmdirSync(scope);\n } catch {\n /* already gone */\n }\n}\n\nexport function realOrSelf(p: string): string {\n try {\n return fs.realpathSync(p);\n } catch {\n return p;\n }\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PruneOptions } from './prune.js';
|
|
2
|
+
import type { Deps } from './run.js';
|
|
3
|
+
/**
|
|
4
|
+
* @param {object} deps
|
|
5
|
+
* @param {{prune?: boolean}} [opts] unless `prune` is
|
|
6
|
+
* false, every checkout's own copies of what the app provides are removed
|
|
7
|
+
* after relinking (see prune.ts), and a dependency missing from a checkout
|
|
8
|
+
* because it was pruned is not mistaken for one a root install took.
|
|
9
|
+
*/
|
|
10
|
+
export declare function relink(deps: Deps, opts?: PruneOptions): number;
|
|
11
|
+
//# sourceMappingURL=relink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relink.d.ts","sourceRoot":"","sources":["../../../src/localize/relink.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,UAAU,CAAC;AAEnC;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,GAAE,YAAiB,GAAG,MAAM,CAiElE"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--relink` -- the `postinstall` hook. Recreate every recorded symlink after
|
|
3
|
+
* an install removed it (`npm ci` deletes `node_modules` and exits 0).
|
|
4
|
+
*
|
|
5
|
+
* Three rules, each of which is the difference between a useful hook and one
|
|
6
|
+
* everybody disables:
|
|
7
|
+
*
|
|
8
|
+
* - **It always exits 0.** A postinstall that fails an install because of a
|
|
9
|
+
* developer's private state is worse than the problem it solves.
|
|
10
|
+
* - **With no manifest it produces NO output at all.** That is the CI case and
|
|
11
|
+
* the fresh-clone case. Any output there and people learn to ignore this
|
|
12
|
+
* hook's output, which is the one thing it can never become.
|
|
13
|
+
* - **It never runs npm at the consumer root.** A postinstall that installs
|
|
14
|
+
* the root, recurses. A test asserts it.
|
|
15
|
+
*
|
|
16
|
+
* It DOES run `npm install` inside a checkout whose dependencies are gone, and
|
|
17
|
+
* only then. Measured on npm 11: a root `npm install` treats a linked checkout
|
|
18
|
+
* that sits under the root as part of the root's tree, marks its dependencies
|
|
19
|
+
* `extraneous` and prunes them -- for a localized published dependency as
|
|
20
|
+
* much as for an undeclared one. Putting the link back without the
|
|
21
|
+
* dependencies leaves a checkout that cannot build. That install runs in the
|
|
22
|
+
* checkout, never at the root, so it cannot re-enter this hook; and it runs
|
|
23
|
+
* only when a declared dependency is missing, so an ordinary install stays
|
|
24
|
+
* silent and fast.
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { readManifest } from './manifest.js';
|
|
29
|
+
import { isSymlink, readJson, writeLink } from './fsops.js';
|
|
30
|
+
import { declaredRange } from './localize.js';
|
|
31
|
+
import { candidatesFor, isProvidedByApp, pruneProvided, shouldPrune, } from './prune.js';
|
|
32
|
+
import { ensure } from './ensure.js';
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} deps
|
|
35
|
+
* @param {{prune?: boolean}} [opts] unless `prune` is
|
|
36
|
+
* false, every checkout's own copies of what the app provides are removed
|
|
37
|
+
* after relinking (see prune.ts), and a dependency missing from a checkout
|
|
38
|
+
* because it was pruned is not mistaken for one a root install took.
|
|
39
|
+
*/
|
|
40
|
+
export function relink(deps, opts = {}) {
|
|
41
|
+
let manifest;
|
|
42
|
+
try {
|
|
43
|
+
manifest = readManifest(deps.appRoot, deps);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
// Even a refusal is only a warning here: never fail an install.
|
|
47
|
+
deps.warn(`[localize] ${e.message}`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
if (!manifest.exists)
|
|
51
|
+
return 0;
|
|
52
|
+
const names = Object.keys(manifest.entries);
|
|
53
|
+
if (!names.length)
|
|
54
|
+
return 0;
|
|
55
|
+
let relinked = 0;
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
const entry = manifest.entries[name];
|
|
58
|
+
const checkout = path.join(deps.appRoot, entry.path);
|
|
59
|
+
if (!fs.existsSync(checkout)) {
|
|
60
|
+
deps.warn(`[localize] ${name} is recorded as local but ${entry.path} is gone — using the registry ` +
|
|
61
|
+
`copy. ` +
|
|
62
|
+
(entry.repo
|
|
63
|
+
? `\`linked localize ${name}\` to re-clone.`
|
|
64
|
+
: `It has no recorded repository, so restore the checkout by hand, then ` +
|
|
65
|
+
`\`linked localize ${name} --adopt\`.`));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
reinstallIfPruned(name, entry, manifest.entries, opts, deps);
|
|
69
|
+
if (isSymlink(path.join(deps.appRoot, 'node_modules', name))) {
|
|
70
|
+
// Already linked; rewriting it would be noise on every install.
|
|
71
|
+
const existing = fs.realpathSync(path.join(deps.appRoot, 'node_modules', name));
|
|
72
|
+
if (existing === fs.realpathSync(checkout))
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
writeLink(deps.appRoot, name, checkout);
|
|
76
|
+
relinked++;
|
|
77
|
+
deps.log(`[localize] relinked ${name} -> ${entry.path} (branch ${entry.branch})`);
|
|
78
|
+
warnRangeDrift(name, entry, checkout, deps);
|
|
79
|
+
}
|
|
80
|
+
if (relinked) {
|
|
81
|
+
deps.log(`[localize] ${relinked} local package${relinked === 1 ? '' : 's'} relinked. ` +
|
|
82
|
+
'`linked localize --list` for detail.');
|
|
83
|
+
}
|
|
84
|
+
if (shouldPrune(opts)) {
|
|
85
|
+
try {
|
|
86
|
+
pruneProvided(manifest.entries, opts, deps);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
// Never fail an install over it.
|
|
90
|
+
deps.warn(`[localize] pruning failed: ${e.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Restore a checkout's own `node_modules` when a root install pruned it: any
|
|
97
|
+
* declared dependency (or devDependency -- the checkout's build needs those)
|
|
98
|
+
* missing from it. A failure warns; the hook still exits 0.
|
|
99
|
+
*/
|
|
100
|
+
function reinstallIfPruned(name, entry, entries, opts, deps) {
|
|
101
|
+
const checkout = path.join(deps.appRoot, entry.path);
|
|
102
|
+
// When pruning, a dependency the app provides is absent on purpose.
|
|
103
|
+
const candidates = shouldPrune(opts) ? candidatesFor(entries, deps) : null;
|
|
104
|
+
const provided = candidates
|
|
105
|
+
? (dep, range) => isProvidedByApp(dep, range, entries, candidates, deps)
|
|
106
|
+
: () => false;
|
|
107
|
+
const missing = missingDependencies(checkout, cloneRootOf(entry, deps), provided);
|
|
108
|
+
if (!missing.length)
|
|
109
|
+
return;
|
|
110
|
+
deps.log(`[localize] ${name}: ${missing.length} of its dependencies are gone from ${entry.path}/node_modules ` +
|
|
111
|
+
`(a root install prunes a checkout under the root) — reinstalling it`);
|
|
112
|
+
try {
|
|
113
|
+
ensure(entry, deps, opts);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
deps.warn(`[localize] ${name}: ${e.message}\n` +
|
|
117
|
+
`Retry with \`linked localize --reinstall ${name}\`.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Declared dependency names found in neither the checkout's own `node_modules`
|
|
122
|
+
* nor, for a package inside a monorepo, the clone root's -- where a workspaces
|
|
123
|
+
* install hoists them. Checking only the package directory there would find
|
|
124
|
+
* every hoisted dependency "missing" and reinstall on every postinstall.
|
|
125
|
+
*/
|
|
126
|
+
function missingDependencies(checkout, cloneRoot, provided = () => false) {
|
|
127
|
+
const pkg = readJson(path.join(checkout, 'package.json'));
|
|
128
|
+
if (!pkg)
|
|
129
|
+
return [];
|
|
130
|
+
const declared = Object.assign(Object.assign({}, pkg.devDependencies), pkg.dependencies);
|
|
131
|
+
const roots = cloneRoot === checkout ? [checkout] : [checkout, cloneRoot];
|
|
132
|
+
return Object.keys(declared).filter((dep) => !roots.some((root) => fs.existsSync(path.join(root, 'node_modules', dep))) && !provided(dep, declared[dep], pkg));
|
|
133
|
+
}
|
|
134
|
+
/** The clone a `subdir` entry lives in: its path with the subdir taken off the end. */
|
|
135
|
+
function cloneRootOf(entry, deps) {
|
|
136
|
+
const pkgDir = path.join(deps.appRoot, entry.path);
|
|
137
|
+
if (!entry.subdir)
|
|
138
|
+
return pkgDir;
|
|
139
|
+
const rel = path.normalize(entry.subdir);
|
|
140
|
+
return pkgDir.endsWith(path.sep + rel)
|
|
141
|
+
? pkgDir.slice(0, -(rel.length + 1))
|
|
142
|
+
: pkgDir;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* npm matches a link by NAME and never checks the range, so a checkout at
|
|
146
|
+
* 2.0.99 silently satisfies `^3.0.0` and `npm ls` does not flag it. This warns
|
|
147
|
+
* and never errors, and the range is never used to decide whether a checkout
|
|
148
|
+
* is usable -- only to say out loud that the declared range is now fiction.
|
|
149
|
+
*/
|
|
150
|
+
function warnRangeDrift(name, entry, checkout, deps) {
|
|
151
|
+
var _a;
|
|
152
|
+
const declared = declaredRange(name, deps);
|
|
153
|
+
const version = (_a = readJson(path.join(checkout, 'package.json'))) === null || _a === void 0 ? void 0 : _a.version;
|
|
154
|
+
if (!declared || !version)
|
|
155
|
+
return;
|
|
156
|
+
if (sameMajor(version, declared) &&
|
|
157
|
+
(!entry.range || entry.range === declared))
|
|
158
|
+
return;
|
|
159
|
+
deps.warn(`[localize] ${name}: checkout is ${version}, package.json asks ${declared}` +
|
|
160
|
+
(entry.range ? ` (recorded ${entry.range})` : '') +
|
|
161
|
+
` — the symlink wins; npm does NOT check the range.`);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Deliberately crude: enough to spot obvious drift for a warning, never used
|
|
165
|
+
* to gate anything. Real semver (`satisfies` in prune.ts) here would invite
|
|
166
|
+
* someone to treat the answer as a constraint.
|
|
167
|
+
*/
|
|
168
|
+
function sameMajor(version, range) {
|
|
169
|
+
const m = /^[\^~]?(\d+)\./.exec(range);
|
|
170
|
+
if (!m)
|
|
171
|
+
return true;
|
|
172
|
+
return version.split('.')[0] === m[1];
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=relink.js.map
|