stonyx 0.2.3-alpha.59 → 0.2.3-alpha.60
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/modules.d.ts +25 -0
- package/dist/modules.js +115 -14
- package/dist/util/duplicate-core.js +30 -6
- package/package.json +1 -1
package/dist/modules.d.ts
CHANGED
|
@@ -2,4 +2,29 @@ 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
|
+
*/
|
|
5
30
|
export declare function waitForModule(moduleName: string): Promise<void>;
|
package/dist/modules.js
CHANGED
|
@@ -8,6 +8,19 @@ 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, so it tracks the
|
|
20
|
+
// manifest of the most recent load exactly as `modulePromises` tracks that
|
|
21
|
+
// load's discovery — one derived fact, one lifetime, rather than a second
|
|
22
|
+
// singleton drifting away from the first.
|
|
23
|
+
let declaredModuleNames = new Set();
|
|
11
24
|
// Configure module-specific logging
|
|
12
25
|
function configureLog(chronicle, module, config) {
|
|
13
26
|
const { logColor, logMethod, logTimestamp } = config;
|
|
@@ -25,9 +38,22 @@ function initializeModule(moduleName, moduleClass, modules, initPromises) {
|
|
|
25
38
|
// #120, and the one that scoping registration to `discovered` does NOT close,
|
|
26
39
|
// because such a module IS discovered — measured, not reasoned: with only
|
|
27
40
|
// the registration fix applied the suite read 180/1 with T24 the sole
|
|
28
|
-
// failure. Resolving here is
|
|
29
|
-
// and that is what separates it from
|
|
30
|
-
// where there is no module to be ready
|
|
41
|
+
// failure. Resolving here is readiness BY DEFINITION — there is no `init()`,
|
|
42
|
+
// so there is nothing to wait for — and that is what separates it from
|
|
43
|
+
// resolving on a discovery `continue`, where there is no module to be ready
|
|
44
|
+
// at all.
|
|
45
|
+
//
|
|
46
|
+
// It is deliberately NOT the stronger claim that the module initialised.
|
|
47
|
+
// `waitForModule` is documented as blocking until a module "finishes
|
|
48
|
+
// initializing", and a `static async init()` typo — which TypeScript cannot
|
|
49
|
+
// catch, since `init?()` is optional in lifecycle.ts — presents to the loader
|
|
50
|
+
// as a class with no `init()`. This resolves it, and the waiter proceeds
|
|
51
|
+
// against a module whose own initialisation never ran; measured, the waiter
|
|
52
|
+
// observes the module's ready flag still false. The loader cannot tell that
|
|
53
|
+
// typo from a legitimate no-`init()` module, and a no-`init()` module is a
|
|
54
|
+
// supported shape, so resolving is the only non-hanging answer available and
|
|
55
|
+
// availability over a silent hang is the right trade. The claim is what is
|
|
56
|
+
// narrowed here, not the behaviour.
|
|
31
57
|
//
|
|
32
58
|
// `?.` for the reason the resolve below carries it: `initializeModule` is
|
|
33
59
|
// also reached from the standalone path as `initializeModule(projectName, …)`,
|
|
@@ -114,9 +140,17 @@ export default async function loadModules(config, rootPath, chronicle) {
|
|
|
114
140
|
// A bare `[ ...Object.keys(deps), ...Object.keys(devDeps) ]` concat instantiates
|
|
115
141
|
// a dual-declared module twice and runs its `init()` twice. That WOULD be
|
|
116
142
|
// invisible to the suite aggregate were T22 not armed; T22's instance-count
|
|
117
|
-
// assertion is what makes it visible.
|
|
118
|
-
//
|
|
119
|
-
//
|
|
143
|
+
// assertion is what makes it visible. With the concat seeded, T22 is the SOLE
|
|
144
|
+
// failure and both of its counts read actual 2 against expected 1 — measured
|
|
145
|
+
// 180 pass / 1 fail at f575a3c, out of a 181-test suite.
|
|
146
|
+
//
|
|
147
|
+
// The SHA is not decoration. This sentence replaced one that round 1 found
|
|
148
|
+
// false, and its replacement carried a figure measured at the PREVIOUS head
|
|
149
|
+
// under the words "at this head": 178/1, which cannot reconcile against a
|
|
150
|
+
// 181-test suite at all. Four reviewers caught it independently. A bare
|
|
151
|
+
// aggregate has no author and no date, so it rots silently on the next commit
|
|
152
|
+
// that adds a test; an aggregate with the SHA it was taken at stays true
|
|
153
|
+
// forever and tells the next reader exactly what to re-run.
|
|
120
154
|
const declaredDependencies = {
|
|
121
155
|
...(rootPackage.dependencies || {}),
|
|
122
156
|
...(rootPackage.devDependencies || {}),
|
|
@@ -130,6 +164,7 @@ export default async function loadModules(config, rootPath, chronicle) {
|
|
|
130
164
|
// unresolved. T3 owns that kill. What is a module is decided below, by the
|
|
131
165
|
// `stonyx-module` keyword; this is only a name test that bounds the scan.
|
|
132
166
|
const moduleDependencies = Object.keys(declaredDependencies).filter((moduleName) => moduleName.startsWith('@stonyx/'));
|
|
167
|
+
declaredModuleNames = new Set(moduleDependencies);
|
|
133
168
|
// DISCOVERY, hoisted above the pre-flight.
|
|
134
169
|
//
|
|
135
170
|
// The pre-flight must check exactly what the loader loads, and no more.
|
|
@@ -168,8 +203,16 @@ export default async function loadModules(config, rootPath, chronicle) {
|
|
|
168
203
|
// contains, and the widening turned a fast named throw into an unbounded
|
|
169
204
|
// boot hang. A name registered here but never resolved leaves
|
|
170
205
|
// `waitForModule` pending forever; discovery `continue`s past two kinds of
|
|
171
|
-
// name without reaching
|
|
206
|
+
// name without reaching ANY of this file's three resolve sites — a manifest
|
|
172
207
|
// that is not there, and a package without the `stonyx-module` keyword.
|
|
208
|
+
// Three, not the two this sentence claimed when it was written: the
|
|
209
|
+
// no-`init()` early return in `initializeModule` is a resolve site and the
|
|
210
|
+
// commit that wrote this sentence is the commit that added it. The sites are
|
|
211
|
+
// named rather than numbered by line, because the comment above
|
|
212
|
+
// `initializeModule` reasons from this inventory and a stale count sends the
|
|
213
|
+
// next dangling-promise audit one site short — they are the no-`init()`
|
|
214
|
+
// early return, the `init()` wrapper, and the sync-module `continue` in the
|
|
215
|
+
// load loop.
|
|
173
216
|
// `@stonyx/logs` ships without that keyword and is an ordinary `dependencies`
|
|
174
217
|
// entry, so this is a shape the fleet actually has.
|
|
175
218
|
//
|
|
@@ -177,10 +220,38 @@ export default async function loadModules(config, rootPath, chronicle) {
|
|
|
177
220
|
// base 5693744 throws "…was not registered in project dependencies", 3 ms
|
|
178
221
|
// a57045e one console.warn, then loadModules never settles
|
|
179
222
|
//
|
|
180
|
-
// Registering only what discovery admitted restores the fail-fast for both
|
|
181
|
-
// and for `devDependencies`-declared names too
|
|
182
|
-
// introduced here, and it is a real behaviour
|
|
183
|
-
//
|
|
223
|
+
// Registering only what discovery admitted restores the fail-fast for both
|
|
224
|
+
// of those DISCOVERY FAILURES, and for `devDependencies`-declared names too
|
|
225
|
+
// — that half is INHERITED, not introduced here, and it is a real behaviour
|
|
226
|
+
// change: hang becomes throw — and the throw names the right cause, because
|
|
227
|
+
// `modulePromises` is no longer the app's declared list and `waitForModule`
|
|
228
|
+
// below branches on that difference rather than asserting the old one. T13
|
|
229
|
+
// pins both halves, and the PR body names them.
|
|
230
|
+
//
|
|
231
|
+
// SCOPED DELIBERATELY, because the sentence above would otherwise read as a
|
|
232
|
+
// closure statement for the whole never-settling-promise family and it is
|
|
233
|
+
// not one. What it closes is names that FAIL discovery. A name that PASSES
|
|
234
|
+
// discovery is registered, and its promise is resolved only once its own
|
|
235
|
+
// `init()` has settled — so two discovered modules whose `init()`s await
|
|
236
|
+
// each other through `waitForModule` are both registered, reach no resolve
|
|
237
|
+
// site, and leave `await Promise.all(initPromises)` at the end of this
|
|
238
|
+
// function pending forever. Measured on a cross-map mutual wait
|
|
239
|
+
// (`@stonyx/c1-a` in `dependencies`, `@stonyx/c1-b` in `devDependencies`,
|
|
240
|
+
// each `init()` awaiting the other), 3000 ms cap:
|
|
241
|
+
// base 5693744 threw in 3 ms — the `dependencies` half was never
|
|
242
|
+
// discovered, so it failed fast for the wrong reason
|
|
243
|
+
// a57045e HUNG, 3001 ms
|
|
244
|
+
// f575a3c HUNG, 3003 ms — scoping registration does not touch it
|
|
245
|
+
// Rule 3 moves that shape from fail-fast to deadlock and nothing in this
|
|
246
|
+
// file closes it.
|
|
247
|
+
//
|
|
248
|
+
// NO DETECTION IS ATTEMPTED, and that is a decision rather than an
|
|
249
|
+
// oversight. `waitForModule` is told which module is being waited FOR and
|
|
250
|
+
// never which module is waiting, so a cycle is not observable without
|
|
251
|
+
// caller attribution that does not exist here, and this framework sets no
|
|
252
|
+
// boot timeout to hang a fallback off. A wait cycle is an authoring error in
|
|
253
|
+
// the modules; docs/modules.md's `waitForModule` section records the hazard
|
|
254
|
+
// so it is documented rather than merely known.
|
|
184
255
|
//
|
|
185
256
|
// The alternative — resolving in each `continue` branch — also removes the
|
|
186
257
|
// hang and is worse: `waitForModule` would report success for a module that
|
|
@@ -253,10 +324,40 @@ export default async function loadModules(config, rootPath, chronicle) {
|
|
|
253
324
|
await Promise.all(initPromises);
|
|
254
325
|
return modules;
|
|
255
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Blocks until `@stonyx/<moduleName>` has finished initializing.
|
|
329
|
+
*
|
|
330
|
+
* TWO failure sentences, because one sentence stopped being able to carry both
|
|
331
|
+
* facts. Until PR #120 fix round 1 `modulePromises` held every DECLARED
|
|
332
|
+
* `@stonyx/*` name, so "absent from the map" and "absent from the manifest"
|
|
333
|
+
* were the same fact and the single inherited sentence was true. Registration
|
|
334
|
+
* is now scoped to the DISCOVERED set — which is what removes the boot hang,
|
|
335
|
+
* see the registration loop in `loadModules` — so absence from the map has two
|
|
336
|
+
* causes, and the inherited sentence is FALSE for one of them: it tells the
|
|
337
|
+
* operator the package is not in their dependencies while it is sitting in
|
|
338
|
+
* their manifest.
|
|
339
|
+
*
|
|
340
|
+
* Not hypothetical, and not a shape this repo has to imagine: `@stonyx/logs`
|
|
341
|
+
* ships without the `stonyx-module` keyword and is an ordinary `dependencies`
|
|
342
|
+
* entry of THIS package.json. An app in the same shape calling
|
|
343
|
+
* `waitForModule('logs')` would be sent to grep a manifest that already
|
|
344
|
+
* contains the answer. Naming the wrong cause is the defect abofs/stonyx#108
|
|
345
|
+
* was filed over, and it is not made acceptable by being one line long.
|
|
346
|
+
*
|
|
347
|
+
* The declared branch does not say WHICH of the two causes applies, because
|
|
348
|
+
* this function does not know. Discovery does, and it already `console.warn`s
|
|
349
|
+
* the specific reason at load time, so the message points at that warning
|
|
350
|
+
* rather than guessing between them.
|
|
351
|
+
*/
|
|
256
352
|
export async function waitForModule(moduleName) {
|
|
257
353
|
const fullName = `@stonyx/${moduleName}`;
|
|
258
354
|
const modulePromise = modulePromises[fullName];
|
|
259
|
-
if (!modulePromise)
|
|
260
|
-
throw new Error(
|
|
261
|
-
|
|
355
|
+
if (!modulePromise) {
|
|
356
|
+
throw new Error(declaredModuleNames.has(fullName)
|
|
357
|
+
? `Could wait for module: ${fullName}. It IS declared in this project's dependencies, but the ` +
|
|
358
|
+
'loader did not load it: either it is not installed under node_modules, or its package.json ' +
|
|
359
|
+
'does not carry the "stonyx-module" keyword. loadModules warned which one at load time.'
|
|
360
|
+
: `Could wait for module: ${fullName}. Module was not registered in project dependencies`);
|
|
361
|
+
}
|
|
362
|
+
await modulePromise.ready;
|
|
262
363
|
}
|
|
@@ -374,16 +374,40 @@ export function duplicateCoreMessage(foreign) {
|
|
|
374
374
|
// de-duplicated union of `dependencies` and `devDependencies`, and until
|
|
375
375
|
// PR #120 fix round 1 this sentence still said `devDependencies` only —
|
|
376
376
|
// so a refusal could name `seen by "@stonyx/x"` for a package declared
|
|
377
|
-
// ONLY in `dependencies` and then,
|
|
377
|
+
// ONLY in `dependencies` and then, a few lines later, tell the operator
|
|
378
378
|
// that such a declaration is not counted. Reproduced verbatim before the
|
|
379
379
|
// fix. That sends them to search `devDependencies`, where there is nothing.
|
|
380
380
|
// If the scan source ever changes again, this string changes with it;
|
|
381
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.
|
|
382
403
|
'Scope of this check: it compares physical package ROOTS only. It does not check that the ' +
|
|
383
|
-
'single surviving copy is a compatible version, and
|
|
384
|
-
'this app\'s dependencies or devDependencies that carry the
|
|
385
|
-
'else. Either placement counts: the loader scans the
|
|
386
|
-
'
|
|
387
|
-
'
|
|
404
|
+
'single surviving copy is a compatible version, and the modules it starts from are the ' +
|
|
405
|
+
'@stonyx/* packages declared in this app\'s dependencies or devDependencies that carry the ' +
|
|
406
|
+
'"stonyx-module" keyword — nothing else. Either placement counts: the loader scans the ' +
|
|
407
|
+
'de-duplicated union of both maps. What it then reports is not those packages\' own copies but ' +
|
|
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 declares. A copy no module\'s ' +
|
|
411
|
+
'walk reaches first is not counted, and one further up a walk is hidden by a nearer one.',
|
|
388
412
|
].join('\n');
|
|
389
413
|
}
|