stonyx 0.2.3-alpha.57 → 0.2.3-alpha.59

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 CHANGED
@@ -27,7 +27,7 @@ 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`, initializes all `@stonyx/*` modules declared in your `dependencies` or `devDependencies`, and runs your application.
30
+ Stonyx reads `config/environment.ts` (preferred) or `config/environment.js`, loads the `@stonyx/*` modules declared in your `dependencies` or `devDependencies` (initializing the async ones), and runs your application.
31
31
 
32
32
  ## Documentation
33
33
 
package/dist/cli/new.js CHANGED
@@ -167,8 +167,12 @@ export function generatePackageJson(name, selectedModules, coreVersion = readCor
167
167
  tsx: '^4.21.0',
168
168
  typescript: '^5.8.3'
169
169
  };
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`.
170
+ // The scaffolder writes modules to `devDependencies` because that remains
171
+ // the conventional placement for an application — NOT because it is the only
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`.
172
176
  for (const mod of selectedModules) {
173
177
  devDependencies[mod.package] = moduleTag;
174
178
  }
package/dist/modules.js CHANGED
@@ -18,8 +18,24 @@ function configureLog(chronicle, module, config) {
18
18
  function initializeModule(moduleName, moduleClass, modules, initPromises) {
19
19
  const moduleInstance = new moduleClass();
20
20
  modules.push(moduleInstance);
21
- if (!moduleInstance.init)
21
+ // A module class with no `init()` has nothing to initialise, so it is ready
22
+ // the moment it is instantiated. Without this its registered promise dangles
23
+ // — the only resolve on this path lives inside the `init()` wrapper below,
24
+ // which is never created. Third of the three never-resolved paths named in
25
+ // #120, and the one that scoping registration to `discovered` does NOT close,
26
+ // because such a module IS discovered — measured, not reasoned: with only
27
+ // the registration fix applied the suite read 180/1 with T24 the sole
28
+ // failure. Resolving here is truthful, because the module really did load,
29
+ // and that is what separates it from resolving on a discovery `continue`,
30
+ // where there is no module to be ready.
31
+ //
32
+ // `?.` for the reason the resolve below carries it: `initializeModule` is
33
+ // also reached from the standalone path as `initializeModule(projectName, …)`,
34
+ // and `projectName` is never a key in `modulePromises`.
35
+ if (!moduleInstance.init) {
36
+ modulePromises[moduleName]?.resolve();
22
37
  return;
38
+ }
23
39
  initPromises.push((async () => {
24
40
  await moduleInstance.init();
25
41
  modulePromises[moduleName]?.resolve();
@@ -90,10 +106,17 @@ export default async function loadModules(config, rootPath, chronicle) {
90
106
  //
91
107
  // Rule 3 widens the source to the DE-DUPLICATED union of both maps. Object
92
108
  // spread IS the de-duplication and is load-bearing: a module declared in both
93
- // maps collapses to one key, so it is registered once and instantiated once.
94
- // A bare `[ ...Object.keys(deps), ...Object.keys(devDeps) ]` concat produces a
95
- // byte-identical suite aggregate while instantiating such a module twice and
96
- // running its `init()` twice — T22's instance-count assertion owns that kill.
109
+ // maps collapses to one key, so it is registered once and — for an async
110
+ // module — instantiated once. (A SYNC module is discovered once and
111
+ // instantiated zero times; the loader never imports its entry point. Measured
112
+ // on a dual-declared sync fixture: `modules.length === 0`.)
113
+ //
114
+ // A bare `[ ...Object.keys(deps), ...Object.keys(devDeps) ]` concat instantiates
115
+ // a dual-declared module twice and runs its `init()` twice. That WOULD be
116
+ // invisible to the suite aggregate were T22 not armed; T22's instance-count
117
+ // assertion is what makes it visible. Measured at this head with the concat
118
+ // seeded: 178/1 with T22 the sole failure, both of its counts reading actual 2
119
+ // against expected 1.
97
120
  const declaredDependencies = {
98
121
  ...(rootPackage.dependencies || {}),
99
122
  ...(rootPackage.devDependencies || {}),
@@ -107,16 +130,10 @@ export default async function loadModules(config, rootPath, chronicle) {
107
130
  // unresolved. T3 owns that kill. What is a module is decided below, by the
108
131
  // `stonyx-module` keyword; this is only a name test that bounds the scan.
109
132
  const moduleDependencies = Object.keys(declaredDependencies).filter((moduleName) => moduleName.startsWith('@stonyx/'));
110
- // Setup module promises prior to initialization
111
- for (const moduleName of moduleDependencies) {
112
- const promise = {};
113
- modulePromises[moduleName] = promise;
114
- promise.ready = new Promise(resolve => promise.resolve = resolve);
115
- }
116
133
  // DISCOVERY, hoisted above the pre-flight.
117
134
  //
118
135
  // The pre-flight must check exactly what the loader loads, and no more.
119
- // `@stonyx/*` is a NAME test, not a membership test: a scoped devDependency
136
+ // `@stonyx/*` is a NAME test, not a membership test: a scoped dependency
120
137
  // without the `stonyx-module` keyword is warned about and skipped below —
121
138
  // never imported, never configured, incapable of registering anything on any
122
139
  // singleton — and refusing to boot over its nested copy prescribed the MODULE
@@ -143,6 +160,41 @@ export default async function loadModules(config, rootPath, chronicle) {
143
160
  keywords,
144
161
  });
145
162
  }
163
+ // Setup module promises prior to initialization — over `discovered`, NOT
164
+ // over `moduleDependencies`.
165
+ //
166
+ // This loop used to run above discovery and consume the declared list. That
167
+ // was already the shape at base, but rule 3 widened what the declared list
168
+ // contains, and the widening turned a fast named throw into an unbounded
169
+ // boot hang. A name registered here but never resolved leaves
170
+ // `waitForModule` pending forever; discovery `continue`s past two kinds of
171
+ // name without reaching either of this file's two resolve sites — a manifest
172
+ // that is not there, and a package without the `stonyx-module` keyword.
173
+ // `@stonyx/logs` ships without that keyword and is an ordinary `dependencies`
174
+ // entry, so this is a shape the fleet actually has.
175
+ //
176
+ // name in `dependencies`, not installed, another module awaits it
177
+ // base 5693744 throws "…was not registered in project dependencies", 3 ms
178
+ // a57045e one console.warn, then loadModules never settles
179
+ //
180
+ // Registering only what discovery admitted restores the fail-fast for both,
181
+ // and for `devDependencies`-declared names too — that half is INHERITED, not
182
+ // introduced here, and it is a real behaviour change: hang becomes throw.
183
+ // T13 pins it, and the PR body names it.
184
+ //
185
+ // The alternative — resolving in each `continue` branch — also removes the
186
+ // hang and is worse: `waitForModule` would report success for a module that
187
+ // was never loaded, silently. T23 asserts the named rejection precisely so
188
+ // that remedy cannot pass.
189
+ //
190
+ // Ordering is safe: nothing between here and the old site touches
191
+ // `modulePromises`, discovery only reads manifests, and registration still
192
+ // completes before the first `init()` can run.
193
+ for (const { name: moduleName } of discovered) {
194
+ const promise = {};
195
+ modulePromises[moduleName] = promise;
196
+ promise.ready = new Promise(resolve => promise.resolve = resolve);
197
+ }
146
198
  // Pre-flight: invariant I1, "one core". Before ANY module entry point is
147
199
  // imported, because the point of the check is the module that would NOT
148
200
  // throw — it would load, initialise, and register its hooks on a second
@@ -265,7 +265,8 @@ 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 `devDependencies`,
268
+ * `moduleName` is narrower — it is a key from the app's own `dependencies` or
269
+ * `devDependencies` (abofs/stonyx#106 rule 3: the loader scans the union),
269
270
  * so forging it is an app author forging their own diagnostic — but it is
270
271
  * rendered through the same `padEnd` table, and it caused the same two
271
272
  * symptoms: seeded with ANSI escapes and embedded newlines it FORGED A
@@ -297,7 +298,7 @@ export function findForeignCores(modules, core = undefined, report = IGNORE) {
297
298
  * nothing but its own wrap. The module name and the version are both inside
298
299
  * PADDED columns whose widths are the max over every row, so one long value
299
300
  * 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
+ * the tight default; a scoped `@stonyx/*` dependency key is far short of it,
301
302
  * so a real name is sanitised and never shortened either.
302
303
  */
303
304
  const PATH_LIMIT = 1024;
@@ -368,10 +369,21 @@ export function duplicateCoreMessage(foreign) {
368
369
  'peerDependencies range, never as an exact dependency — @stonyx/discord is the reference shape. ' +
369
370
  consumerRemedy,
370
371
  '',
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, seven 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.
371
382
  'Scope of this check: it compares physical package ROOTS only. It does not check that the ' +
372
383
  '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.',
384
+ 'this app\'s dependencies or devDependencies that carry the "stonyx-module" keyword — nothing ' +
385
+ 'else. Either placement counts: the loader scans the de-duplicated union of both maps. A copy ' +
386
+ 'dragged in by anything outside that set — a transitive dependency, a package without the ' +
387
+ 'keyword, or one not declared in this app\'s manifest at all — is not counted.',
376
388
  ].join('\n');
377
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stonyx",
3
- "version": "0.2.3-alpha.57",
3
+ "version": "0.2.3-alpha.59",
4
4
  "description": "Base stonyx framework module",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",