stonyx 0.2.3-alpha.61 → 0.2.3-alpha.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/cli/new.js +2 -6
- package/dist/modules.d.ts +0 -25
- package/dist/modules.js +13 -220
- package/dist/util/duplicate-core.js +6 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,19 @@ stonyx test # Bootstrap + run tests
|
|
|
27
27
|
stonyx help # Show all available commands
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Stonyx reads `config/environment.ts` (preferred) or `config/environment.js`,
|
|
30
|
+
Stonyx reads `config/environment.ts` (preferred) or `config/environment.js`, initializes all `@stonyx/*` modules from your `devDependencies`, and runs your application.
|
|
31
|
+
|
|
32
|
+
## Exports
|
|
33
|
+
|
|
34
|
+
| Import path | What it exports |
|
|
35
|
+
|-------------|-----------------|
|
|
36
|
+
| `stonyx` | `Stonyx` class (default), `waitForModule(name)` |
|
|
37
|
+
| `stonyx/config` | Live reference to the Stonyx config (default) |
|
|
38
|
+
| `stonyx/log` | Live reference to the Chronicle logger (default) |
|
|
39
|
+
| `stonyx/lifecycle` | `runStartupHooks(modules)`, `runShutdownHooks(modules)` |
|
|
40
|
+
| `stonyx/test-helpers` | `setupIntegrationTests(hooks)` |
|
|
41
|
+
|
|
42
|
+
`stonyx/config` and `stonyx/log` throw if read before Stonyx is initialized. See the [API Reference](docs/api.md) for details.
|
|
31
43
|
|
|
32
44
|
## Documentation
|
|
33
45
|
|
package/dist/cli/new.js
CHANGED
|
@@ -167,12 +167,8 @@ export function generatePackageJson(name, selectedModules, coreVersion = readCor
|
|
|
167
167
|
tsx: '^4.21.0',
|
|
168
168
|
typescript: '^5.8.3'
|
|
169
169
|
};
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
// one discovery sees. Since abofs/stonyx#106 rule 3 the loader scans the
|
|
173
|
-
// de-duplicated union of `dependencies` and `devDependencies`, so either
|
|
174
|
-
// placement is discovered (see docs/modules.md). Requested on the core's own
|
|
175
|
-
// release line rather than at `latest`.
|
|
170
|
+
// Modules are discovered from devDependencies (see docs/modules.md), and are
|
|
171
|
+
// requested on the core's own release line rather than at `latest`.
|
|
176
172
|
for (const mod of selectedModules) {
|
|
177
173
|
devDependencies[mod.package] = moduleTag;
|
|
178
174
|
}
|
package/dist/modules.d.ts
CHANGED
|
@@ -2,29 +2,4 @@ import type { StoynxModule } from './lifecycle.js';
|
|
|
2
2
|
import type Chronicle from '@stonyx/logs';
|
|
3
3
|
export type StoynxConfig = Record<string, Record<string, unknown> | unknown>;
|
|
4
4
|
export default function loadModules(config: StoynxConfig, rootPath: string, chronicle: Chronicle): Promise<StoynxModule[]>;
|
|
5
|
-
/**
|
|
6
|
-
* Blocks until `@stonyx/<moduleName>` has finished initializing.
|
|
7
|
-
*
|
|
8
|
-
* TWO failure sentences, because one sentence stopped being able to carry both
|
|
9
|
-
* facts. Until PR #120 fix round 1 `modulePromises` held every DECLARED
|
|
10
|
-
* `@stonyx/*` name, so "absent from the map" and "absent from the manifest"
|
|
11
|
-
* were the same fact and the single inherited sentence was true. Registration
|
|
12
|
-
* is now scoped to the DISCOVERED set — which is what removes the boot hang,
|
|
13
|
-
* see the registration loop in `loadModules` — so absence from the map has two
|
|
14
|
-
* causes, and the inherited sentence is FALSE for one of them: it tells the
|
|
15
|
-
* operator the package is not in their dependencies while it is sitting in
|
|
16
|
-
* their manifest.
|
|
17
|
-
*
|
|
18
|
-
* Not hypothetical, and not a shape this repo has to imagine: `@stonyx/logs`
|
|
19
|
-
* ships without the `stonyx-module` keyword and is an ordinary `dependencies`
|
|
20
|
-
* entry of THIS package.json. An app in the same shape calling
|
|
21
|
-
* `waitForModule('logs')` would be sent to grep a manifest that already
|
|
22
|
-
* contains the answer. Naming the wrong cause is the defect abofs/stonyx#108
|
|
23
|
-
* was filed over, and it is not made acceptable by being one line long.
|
|
24
|
-
*
|
|
25
|
-
* The declared branch does not say WHICH of the two causes applies, because
|
|
26
|
-
* this function does not know. Discovery does, and it already `console.warn`s
|
|
27
|
-
* the specific reason at load time, so the message points at that warning
|
|
28
|
-
* rather than guessing between them.
|
|
29
|
-
*/
|
|
30
5
|
export declare function waitForModule(moduleName: string): Promise<void>;
|
package/dist/modules.js
CHANGED
|
@@ -8,38 +8,6 @@ import { mergeObject } from '@stonyx/utils/object';
|
|
|
8
8
|
import { importConfig, CONFIG_NOT_FOUND_PREFIX, CONFIG_NOT_LOADABLE_PREFIX } from './util/import-config.js';
|
|
9
9
|
import { findForeignCores, duplicateCoreMessage } from './util/duplicate-core.js';
|
|
10
10
|
const modulePromises = {};
|
|
11
|
-
// The `@stonyx/*` names this app DECLARED — which since PR #120 fix round 1 is
|
|
12
|
-
// a strictly WIDER set than the keys of `modulePromises`. Registration is now
|
|
13
|
-
// scoped to what discovery admitted, so a declared name whose manifest is
|
|
14
|
-
// missing, or whose manifest carries no `stonyx-module` keyword, is in this set
|
|
15
|
-
// and not in that map. `waitForModule`'s failure branch is the only consumer,
|
|
16
|
-
// and this is the only question it needs answered: "is this name in the
|
|
17
|
-
// manifest?" is exactly what `modulePromises` stopped being able to answer.
|
|
18
|
-
//
|
|
19
|
-
// Reassigned per `loadModules` call rather than accumulated, because only the
|
|
20
|
-
// current manifest can answer for the current boot — a name declared by an
|
|
21
|
-
// earlier load must not answer for this one.
|
|
22
|
-
//
|
|
23
|
-
// It is NOT symmetric with `modulePromises`, and the asymmetry is worth
|
|
24
|
-
// stating because the obvious reading of the line above is that it is.
|
|
25
|
-
// `modulePromises` has no reset: no `delete`, no reassignment, no clear, at
|
|
26
|
-
// any commit on `dev`. It accumulates the union of every `loadModules` call in
|
|
27
|
-
// the process while this set is replaced per call. Measured over three loads
|
|
28
|
-
// in one process at c87826b: load A declares and installs
|
|
29
|
-
// `@stonyx/probe-life`; load B declares it with the package absent, so
|
|
30
|
-
// discovery warns and `continue`s and registers nothing — and
|
|
31
|
-
// `waitForModule('probe-life')` still RESOLVES, off load A's stale entry.
|
|
32
|
-
// That is the "reports success for a module never loaded, silently" outcome
|
|
33
|
-
// the registration loop below calls worse than the hang; T23 forecloses that
|
|
34
|
-
// remedy WITHIN one call and no test sees across calls.
|
|
35
|
-
//
|
|
36
|
-
// Inherited, not introduced here — `modulePromises` was never reset at base
|
|
37
|
-
// 5693744 either — and deliberately not closed here, because giving it a
|
|
38
|
-
// per-call lifetime has a blast radius of its own and is filed separately.
|
|
39
|
-
// Production reachability is nil: one `loadModules` call site, guarded by the
|
|
40
|
-
// `Stonyx.instance` early return. The suite is held safe by the
|
|
41
|
-
// unique-module-name-per-test rule in this file's test header, not by this.
|
|
42
|
-
let declaredModuleNames = new Set();
|
|
43
11
|
// Configure module-specific logging
|
|
44
12
|
function configureLog(chronicle, module, config) {
|
|
45
13
|
const { logColor, logMethod, logTimestamp } = config;
|
|
@@ -50,38 +18,8 @@ function configureLog(chronicle, module, config) {
|
|
|
50
18
|
function initializeModule(moduleName, moduleClass, modules, initPromises) {
|
|
51
19
|
const moduleInstance = new moduleClass();
|
|
52
20
|
modules.push(moduleInstance);
|
|
53
|
-
|
|
54
|
-
// the moment it is instantiated. Without this its registered promise dangles
|
|
55
|
-
// — the only resolve on this path lives inside the `init()` wrapper below,
|
|
56
|
-
// which is never created. Third of the three never-resolved paths named in
|
|
57
|
-
// #120, and the one that scoping registration to `discovered` does NOT close,
|
|
58
|
-
// because such a module IS discovered — measured, not reasoned: with only
|
|
59
|
-
// the registration fix applied the suite read 180 pass / 1 fail at f575a3c
|
|
60
|
-
// with T24 the sole failure. The SHA is the point; see the aggregate
|
|
61
|
-
// convention noted at the `declaredDependencies` union below. Resolving here is readiness BY DEFINITION — there is no `init()`,
|
|
62
|
-
// so there is nothing to wait for — and that is what separates it from
|
|
63
|
-
// resolving on a discovery `continue`, where there is no module to be ready
|
|
64
|
-
// at all.
|
|
65
|
-
//
|
|
66
|
-
// It is deliberately NOT the stronger claim that the module initialised.
|
|
67
|
-
// `waitForModule` is documented as blocking until a module "finishes
|
|
68
|
-
// initializing", and a `static async init()` typo — which TypeScript cannot
|
|
69
|
-
// catch, since `init?()` is optional in lifecycle.ts — presents to the loader
|
|
70
|
-
// as a class with no `init()`. This resolves it, and the waiter proceeds
|
|
71
|
-
// against a module whose own initialisation never ran; measured, the waiter
|
|
72
|
-
// observes the module's ready flag still false. The loader cannot tell that
|
|
73
|
-
// typo from a legitimate no-`init()` module, and a no-`init()` module is a
|
|
74
|
-
// supported shape, so resolving is the only non-hanging answer available and
|
|
75
|
-
// availability over a silent hang is the right trade. The claim is what is
|
|
76
|
-
// narrowed here, not the behaviour.
|
|
77
|
-
//
|
|
78
|
-
// `?.` for the reason the resolve below carries it: `initializeModule` is
|
|
79
|
-
// also reached from the standalone path as `initializeModule(projectName, …)`,
|
|
80
|
-
// and `projectName` is never a key in `modulePromises`.
|
|
81
|
-
if (!moduleInstance.init) {
|
|
82
|
-
modulePromises[moduleName]?.resolve();
|
|
21
|
+
if (!moduleInstance.init)
|
|
83
22
|
return;
|
|
84
|
-
}
|
|
85
23
|
initPromises.push((async () => {
|
|
86
24
|
await moduleInstance.init();
|
|
87
25
|
modulePromises[moduleName]?.resolve();
|
|
@@ -140,55 +78,21 @@ export default async function loadModules(config, rootPath, chronicle) {
|
|
|
140
78
|
const modules = [];
|
|
141
79
|
const initPromises = [];
|
|
142
80
|
const rootPackage = await readFile(`${rootPath}/package.json`, { json: true });
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
// This local used to be named `dependencies` while holding `devDependencies`
|
|
146
|
-
// and nothing else. That name is the ruled root cause of #106: every reader
|
|
147
|
-
// of this function — including its own docs — believed discovery scanned
|
|
148
|
-
// both maps, because the identifier said so. It did not. An installed stonyx
|
|
149
|
-
// module declared in `dependencies` was invisible, which is precisely how the
|
|
150
|
-
// published fleet pins the core, so the loader could not see its own
|
|
151
|
-
// siblings.
|
|
152
|
-
//
|
|
153
|
-
// Rule 3 widens the source to the DE-DUPLICATED union of both maps. Object
|
|
154
|
-
// spread IS the de-duplication and is load-bearing: a module declared in both
|
|
155
|
-
// maps collapses to one key, so it is registered once and — for an async
|
|
156
|
-
// module — instantiated once. (A SYNC module is discovered once and
|
|
157
|
-
// instantiated zero times; the loader never imports its entry point. Measured
|
|
158
|
-
// on a dual-declared sync fixture: `modules.length === 0`.)
|
|
159
|
-
//
|
|
160
|
-
// A bare `[ ...Object.keys(deps), ...Object.keys(devDeps) ]` concat instantiates
|
|
161
|
-
// a dual-declared module twice and runs its `init()` twice. That WOULD be
|
|
162
|
-
// invisible to the suite aggregate were T22 not armed; T22's instance-count
|
|
163
|
-
// assertion is what makes it visible. With the concat seeded, T22 is the SOLE
|
|
164
|
-
// failure and both of its counts read actual 2 against expected 1 — measured
|
|
165
|
-
// 180 pass / 1 fail at f575a3c, out of a 181-test suite.
|
|
166
|
-
//
|
|
167
|
-
// The SHA is not decoration. This sentence replaced one that round 1 found
|
|
168
|
-
// false, and its replacement carried a figure measured at the PREVIOUS head
|
|
169
|
-
// under the words "at this head": 178/1, which cannot reconcile against a
|
|
170
|
-
// 181-test suite at all. Four reviewers caught it independently. A bare
|
|
171
|
-
// aggregate has no author and no date, so it rots silently on the next commit
|
|
172
|
-
// that adds a test; an aggregate with the SHA it was taken at stays true
|
|
173
|
-
// forever and tells the next reader exactly what to re-run.
|
|
174
|
-
const declaredDependencies = {
|
|
175
|
-
...(rootPackage.dependencies || {}),
|
|
176
|
-
...(rootPackage.devDependencies || {}),
|
|
177
|
-
};
|
|
81
|
+
const dependencies = (rootPackage.devDependencies || {});
|
|
178
82
|
const projectName = typeof rootPackage.name === 'string' ? rootPackage.name : '';
|
|
179
83
|
// Expose rootPath to public configuration
|
|
180
84
|
config.rootPath = rootPath;
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
85
|
+
const moduleDependencies = Object.keys(dependencies).filter((moduleName) => moduleName.startsWith('@stonyx/'));
|
|
86
|
+
// Setup module promises prior to initialization
|
|
87
|
+
for (const moduleName of moduleDependencies) {
|
|
88
|
+
const promise = {};
|
|
89
|
+
modulePromises[moduleName] = promise;
|
|
90
|
+
promise.ready = new Promise(resolve => promise.resolve = resolve);
|
|
91
|
+
}
|
|
188
92
|
// DISCOVERY, hoisted above the pre-flight.
|
|
189
93
|
//
|
|
190
94
|
// The pre-flight must check exactly what the loader loads, and no more.
|
|
191
|
-
// `@stonyx/*` is a NAME test, not a membership test: a scoped
|
|
95
|
+
// `@stonyx/*` is a NAME test, not a membership test: a scoped devDependency
|
|
192
96
|
// without the `stonyx-module` keyword is warned about and skipped below —
|
|
193
97
|
// never imported, never configured, incapable of registering anything on any
|
|
194
98
|
// singleton — and refusing to boot over its nested copy prescribed the MODULE
|
|
@@ -215,86 +119,6 @@ export default async function loadModules(config, rootPath, chronicle) {
|
|
|
215
119
|
keywords,
|
|
216
120
|
});
|
|
217
121
|
}
|
|
218
|
-
// Setup module promises prior to initialization — over `discovered`, NOT
|
|
219
|
-
// over `moduleDependencies`.
|
|
220
|
-
//
|
|
221
|
-
// This loop used to run above discovery and consume the declared list. That
|
|
222
|
-
// was already the shape at base, but rule 3 widened what the declared list
|
|
223
|
-
// contains, and the widening turned a fast named throw into an unbounded
|
|
224
|
-
// boot hang. A name registered here but never resolved leaves
|
|
225
|
-
// `waitForModule` pending forever; discovery `continue`s past two kinds of
|
|
226
|
-
// name without reaching ANY of this file's three resolve sites — a manifest
|
|
227
|
-
// that is not there, and a package without the `stonyx-module` keyword.
|
|
228
|
-
// Three, not the two this sentence claimed when it was written: the
|
|
229
|
-
// no-`init()` early return in `initializeModule` is a resolve site and the
|
|
230
|
-
// commit that wrote this sentence is the commit that added it. The sites are
|
|
231
|
-
// named rather than numbered by line, because the comment above
|
|
232
|
-
// `initializeModule` reasons from this inventory and a stale count sends the
|
|
233
|
-
// next dangling-promise audit one site short — they are the no-`init()`
|
|
234
|
-
// early return, the `init()` wrapper, and the sync-module `continue` in the
|
|
235
|
-
// load loop.
|
|
236
|
-
// `@stonyx/logs` ships without that keyword and is an ordinary `dependencies`
|
|
237
|
-
// entry, so this is a shape the fleet actually has.
|
|
238
|
-
//
|
|
239
|
-
// name in `dependencies`, not installed, another module awaits it
|
|
240
|
-
// base 5693744 throws "…was not registered in project dependencies", 3 ms
|
|
241
|
-
// a57045e one console.warn, then loadModules never settles
|
|
242
|
-
//
|
|
243
|
-
// Registering only what discovery admitted restores the fail-fast for both
|
|
244
|
-
// of those DISCOVERY FAILURES, and for `devDependencies`-declared names too
|
|
245
|
-
// — that half is INHERITED, not introduced here, and it is a real behaviour
|
|
246
|
-
// change: hang becomes throw — and the throw names the right cause, because
|
|
247
|
-
// `modulePromises` is no longer the app's declared list and `waitForModule`
|
|
248
|
-
// below branches on that difference rather than asserting the old one. T13
|
|
249
|
-
// pins the hang-becomes-throw change and the named cause; the PR body names
|
|
250
|
-
// them. ("Halves" is avoided here because T13 uses that word for its own
|
|
251
|
-
// input-domain split — the `devDependencies` half it covers and the
|
|
252
|
-
// `dependencies` half it hands to T23.)
|
|
253
|
-
//
|
|
254
|
-
// SCOPED DELIBERATELY, because the sentence above would otherwise read as a
|
|
255
|
-
// closure statement for the whole never-settling-promise family and it is
|
|
256
|
-
// not one. What it closes is names that FAIL discovery. A name that PASSES
|
|
257
|
-
// discovery is registered, and where its class HAS an `init()` its promise
|
|
258
|
-
// is resolved only once that `init()` has settled — the qualifier matters,
|
|
259
|
-
// because two of the three resolve sites named above settle without any
|
|
260
|
-
// `init()` running. So two discovered modules whose `init()`s await
|
|
261
|
-
// each other through `waitForModule` are both registered, reach no resolve
|
|
262
|
-
// site, and leave `await Promise.all(initPromises)` at the end of this
|
|
263
|
-
// function pending forever. Measured on a cross-map mutual wait
|
|
264
|
-
// (`@stonyx/c1-a` in `dependencies`, `@stonyx/c1-b` in `devDependencies`,
|
|
265
|
-
// each `init()` awaiting the other), 3000 ms cap:
|
|
266
|
-
// base 5693744 threw in 3 ms — the `dependencies` half was never
|
|
267
|
-
// discovered, so it failed fast for the wrong reason
|
|
268
|
-
// a57045e HUNG, 3001 ms
|
|
269
|
-
// f575a3c HUNG, 3003 ms — scoping registration does not touch it
|
|
270
|
-
// c87826b HUNG, 3002 ms — re-measured at the head this ships from,
|
|
271
|
-
// because the rows above stop before three commits that
|
|
272
|
-
// touch this file and a reader at head cannot tell from a
|
|
273
|
-
// truncated table whether the row still applies
|
|
274
|
-
// Rule 3 moves that shape from fail-fast to deadlock and nothing in this
|
|
275
|
-
// file closes it.
|
|
276
|
-
//
|
|
277
|
-
// NO DETECTION IS ATTEMPTED, and that is a decision rather than an
|
|
278
|
-
// oversight. `waitForModule` is told which module is being waited FOR and
|
|
279
|
-
// never which module is waiting, so a cycle is not observable without
|
|
280
|
-
// caller attribution that does not exist here, and this framework sets no
|
|
281
|
-
// boot timeout to hang a fallback off. A wait cycle is an authoring error in
|
|
282
|
-
// the modules; docs/modules.md's `waitForModule` section records the hazard
|
|
283
|
-
// so it is documented rather than merely known.
|
|
284
|
-
//
|
|
285
|
-
// The alternative — resolving in each `continue` branch — also removes the
|
|
286
|
-
// hang and is worse: `waitForModule` would report success for a module that
|
|
287
|
-
// was never loaded, silently. T23 asserts the named rejection precisely so
|
|
288
|
-
// that remedy cannot pass.
|
|
289
|
-
//
|
|
290
|
-
// Ordering is safe: nothing between here and the old site touches
|
|
291
|
-
// `modulePromises`, discovery only reads manifests, and registration still
|
|
292
|
-
// completes before the first `init()` can run.
|
|
293
|
-
for (const { name: moduleName } of discovered) {
|
|
294
|
-
const promise = {};
|
|
295
|
-
modulePromises[moduleName] = promise;
|
|
296
|
-
promise.ready = new Promise(resolve => promise.resolve = resolve);
|
|
297
|
-
}
|
|
298
122
|
// Pre-flight: invariant I1, "one core". Before ANY module entry point is
|
|
299
123
|
// imported, because the point of the check is the module that would NOT
|
|
300
124
|
// throw — it would load, initialise, and register its hooks on a second
|
|
@@ -353,41 +177,10 @@ export default async function loadModules(config, rootPath, chronicle) {
|
|
|
353
177
|
await Promise.all(initPromises);
|
|
354
178
|
return modules;
|
|
355
179
|
}
|
|
356
|
-
/**
|
|
357
|
-
* Blocks until `@stonyx/<moduleName>` has finished initializing.
|
|
358
|
-
*
|
|
359
|
-
* TWO failure sentences, because one sentence stopped being able to carry both
|
|
360
|
-
* facts. Until PR #120 fix round 1 `modulePromises` held every DECLARED
|
|
361
|
-
* `@stonyx/*` name, so "absent from the map" and "absent from the manifest"
|
|
362
|
-
* were the same fact and the single inherited sentence was true. Registration
|
|
363
|
-
* is now scoped to the DISCOVERED set — which is what removes the boot hang,
|
|
364
|
-
* see the registration loop in `loadModules` — so absence from the map has two
|
|
365
|
-
* causes, and the inherited sentence is FALSE for one of them: it tells the
|
|
366
|
-
* operator the package is not in their dependencies while it is sitting in
|
|
367
|
-
* their manifest.
|
|
368
|
-
*
|
|
369
|
-
* Not hypothetical, and not a shape this repo has to imagine: `@stonyx/logs`
|
|
370
|
-
* ships without the `stonyx-module` keyword and is an ordinary `dependencies`
|
|
371
|
-
* entry of THIS package.json. An app in the same shape calling
|
|
372
|
-
* `waitForModule('logs')` would be sent to grep a manifest that already
|
|
373
|
-
* contains the answer. Naming the wrong cause is the defect abofs/stonyx#108
|
|
374
|
-
* was filed over, and it is not made acceptable by being one line long.
|
|
375
|
-
*
|
|
376
|
-
* The declared branch does not say WHICH of the two causes applies, because
|
|
377
|
-
* this function does not know. Discovery does, and it already `console.warn`s
|
|
378
|
-
* the specific reason at load time, so the message points at that warning
|
|
379
|
-
* rather than guessing between them.
|
|
380
|
-
*/
|
|
381
180
|
export async function waitForModule(moduleName) {
|
|
382
181
|
const fullName = `@stonyx/${moduleName}`;
|
|
383
182
|
const modulePromise = modulePromises[fullName];
|
|
384
|
-
if (!modulePromise)
|
|
385
|
-
throw new Error(
|
|
386
|
-
|
|
387
|
-
'devDependencies, but the loader did not load it: either it is not installed under ' +
|
|
388
|
-
'node_modules, or its package.json does not carry the "stonyx-module" keyword. ' +
|
|
389
|
-
'loadModules warned which one at load time.'
|
|
390
|
-
: `Could wait for module: ${fullName}. Module was not registered in project dependencies`);
|
|
391
|
-
}
|
|
392
|
-
await modulePromise.ready;
|
|
183
|
+
if (!modulePromise)
|
|
184
|
+
throw new Error(`Could wait for module: ${fullName}. Module was not registered in project dependencies`);
|
|
185
|
+
await modulePromises[fullName].ready;
|
|
393
186
|
}
|
|
@@ -265,8 +265,7 @@ export function findForeignCores(modules, core = undefined, report = IGNORE) {
|
|
|
265
265
|
* NOTHING OFF DISK IS TRUSTED FOR DISPLAY — all three interpolated fields.
|
|
266
266
|
*
|
|
267
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 `
|
|
269
|
-
* `devDependencies` (abofs/stonyx#106 rule 3: the loader scans the union),
|
|
268
|
+
* `moduleName` is narrower — it is a key from the app's own `devDependencies`,
|
|
270
269
|
* so forging it is an app author forging their own diagnostic — but it is
|
|
271
270
|
* rendered through the same `padEnd` table, and it caused the same two
|
|
272
271
|
* symptoms: seeded with ANSI escapes and embedded newlines it FORGED A
|
|
@@ -298,7 +297,7 @@ export function findForeignCores(modules, core = undefined, report = IGNORE) {
|
|
|
298
297
|
* nothing but its own wrap. The module name and the version are both inside
|
|
299
298
|
* PADDED columns whose widths are the max over every row, so one long value
|
|
300
299
|
* there pushes every other row's remaining columns off screen. Those two take
|
|
301
|
-
* the tight default; a scoped `@stonyx/*`
|
|
300
|
+
* the tight default; a scoped `@stonyx/*` devDependency key is far short of it,
|
|
302
301
|
* so a real name is sanitised and never shortened either.
|
|
303
302
|
*/
|
|
304
303
|
const PATH_LIMIT = 1024;
|
|
@@ -369,46 +368,10 @@ export function duplicateCoreMessage(foreign) {
|
|
|
369
368
|
'peerDependencies range, never as an exact dependency — @stonyx/discord is the reference shape. ' +
|
|
370
369
|
consumerRemedy,
|
|
371
370
|
'',
|
|
372
|
-
// THIS PARAGRAPH IS A CLAIM ABOUT WHAT THE LOADER SCANS, printed at the
|
|
373
|
-
// moment boot is refused. abofs/stonyx#106 rule 3 widened that scan to the
|
|
374
|
-
// de-duplicated union of `dependencies` and `devDependencies`, and until
|
|
375
|
-
// PR #120 fix round 1 this sentence still said `devDependencies` only —
|
|
376
|
-
// so a refusal could name `seen by "@stonyx/x"` for a package declared
|
|
377
|
-
// ONLY in `dependencies` and then, a few lines later, tell the operator
|
|
378
|
-
// that such a declaration is not counted. Reproduced verbatim before the
|
|
379
|
-
// fix. That sends them to search `devDependencies`, where there is nothing.
|
|
380
|
-
// If the scan source ever changes again, this string changes with it;
|
|
381
|
-
// `duplicate-core-test.ts` D4 asserts the claim, not just the header.
|
|
382
|
-
//
|
|
383
|
-
// FIX ROUND 2 corrects the closing sentence for the same reason. It used to
|
|
384
|
-
// exclude "a copy dragged in by ... a package without the keyword", which
|
|
385
|
-
// is false for the commonest shape there is: `npm install -g stonyx` plus
|
|
386
|
-
// `stonyx new` writes `stonyx` into the app's own `dependencies`
|
|
387
|
-
// (`src/cli/new.ts`), `stonyx/package.json` carries no `keywords` field at
|
|
388
|
-
// all, and every discovered module's ESM walk reaches
|
|
389
|
-
// `<app>/node_modules/stonyx` — so that copy is counted and PRINTED in the
|
|
390
|
-
// `seen by` row this sentence sits under. It is the exact case the
|
|
391
|
-
// duplicate-INSTALL remedy branch above was written for. The sentence now
|
|
392
|
-
// names what is ENUMERATED — the resolution walk — instead of closing a set
|
|
393
|
-
// of packages, because the enumeration is a property the code has and the
|
|
394
|
-
// set is not. D20 pins it against a fixture that counts such a copy.
|
|
395
|
-
//
|
|
396
|
-
// "FIRST copy that walk finds" is not padding, and it is the round-2
|
|
397
|
-
// rewrite's own near-miss: `coreSeenBy` returns on the first `asCore` hit,
|
|
398
|
-
// so a second copy further up the same walk is SHADOWED and never counted.
|
|
399
|
-
// Written as "a copy owned by any package on that walk is counted" this
|
|
400
|
-
// sentence would have been false in the same shape as the one it replaces —
|
|
401
|
-
// over-closing in the other direction. The shadowing case is stated because
|
|
402
|
-
// it is the reason the fail-open note below matters.
|
|
403
371
|
'Scope of this check: it compares physical package ROOTS only. It does not check that the ' +
|
|
404
|
-
'single surviving copy is a compatible version, and
|
|
405
|
-
'
|
|
406
|
-
'
|
|
407
|
-
'
|
|
408
|
-
'the copy each of them would IMPORT: for every one it follows Node\'s ESM resolution walk up ' +
|
|
409
|
-
'from that module\'s own directory and takes the FIRST copy that walk finds, whoever owns it — ' +
|
|
410
|
-
'including this app\'s own node_modules/stonyx, which no module need declare. A copy no ' +
|
|
411
|
-
'module\'s ' +
|
|
412
|
-
'walk reaches first is not counted, and one further up a walk is hidden by a nearer one.',
|
|
372
|
+
'single surviving copy is a compatible version, and it looks at @stonyx/* packages declared in ' +
|
|
373
|
+
'this app\'s devDependencies that carry the "stonyx-module" keyword — nothing else. A copy ' +
|
|
374
|
+
'dragged in by anything outside that set, including an @stonyx/* package declared in ' +
|
|
375
|
+
'dependencies rather than devDependencies, is not counted.',
|
|
413
376
|
].join('\n');
|
|
414
377
|
}
|