@moostjs/vite 0.6.36 → 0.6.38

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.
Files changed (3) hide show
  1. package/dist/index.cjs +132 -58
  2. package/dist/index.mjs +132 -58
  3. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -249,19 +249,63 @@ function _define_property(obj, key, value) {
249
249
  * A reference to a shared LogsStorage instance that persists
250
250
  * across init cycles in dev mode.
251
251
  */ let logsStorage;
252
+ /** Whether {@link patchMoostHandlerLogging} already installed its wrappers in this process. */ let installed = false;
252
253
  /**
253
- * Patches Moost’s .init() and .logMappedHandler() methods so that, during dev:
254
+ * The slot {@link captureMoostInit} fills: receives every `init()` promise the
255
+ * wrapper below produces. Unset in prod/test, where nothing consumes a capture.
256
+ */ let onInitCaptured;
257
+ /**
258
+ * Hands every `Moost.prototype.init()` promise to `onCapture`, letting a boot
259
+ * await the app's **initialization** instead of only its module evaluation.
260
+ *
261
+ * The documented entry shape is `app.adapter(http).listen(port)` followed by an
262
+ * **un-awaited** `app.init()`: the HTTP middleware is therefore captured *before*
263
+ * init finishes, and nothing else observes a rejecting init — a bind error, a DI
264
+ * audit error or a throwing `@MoostInit` hook would leave the dev server serving
265
+ * a half-booted app (the routes bound before the failure answer 200, the rest
266
+ * fall through to the SPA fallback) instead of the plugin's 502. That is also
267
+ * why the plugin's `bootError` gate runs BEFORE the captured middleware, and why
268
+ * the SSR fallback checks it too: `listen()` ran first and left both a middleware
269
+ * and the local-fetch hook behind.
270
+ *
271
+ * Patches the plugin's **own** `moost` import, exactly like the handler logging
272
+ * does: in dev the runtime is externalized, so the plugin and the app share one
273
+ * native `moost` instance (the premise the DI eject in `restart-cleanup` rests on
274
+ * too, and what `ssrExternalCheck` warns about when it stops holding). Unlike the
275
+ * adapters there is nothing to re-establish per reload — a static import is never
276
+ * re-evaluated — and unlike them it must NOT be pulled through the SSR module
277
+ * runner: importing `moost` there registers it as the runner's first module and
278
+ * makes subsequent hot updates re-evaluate the entry only, serving every one of
279
+ * its dependencies (controllers included) from the stale cache.
280
+ *
281
+ * There is exactly ONE `init` wrapper per process (installed by
282
+ * {@link patchMoostHandlerLogging}) and this only fills its slot, so the newest
283
+ * plugin instance wins: Vite re-imports the config on every config-file
284
+ * `server.restart()`, re-running the plugin factory, and a per-instance wrapper
285
+ * would stack on the previous one and keep the dead instance's scope alive.
286
+ *
287
+ * @param onCapture receives each captured init promise (already marked handled).
288
+ */ function captureMoostInit(onCapture) {
289
+ onInitCaptured = onCapture;
290
+ }
291
+ /**
292
+ * Patches Moost’s .init() and .logMappedHandler() methods — once per process —
293
+ * so that, during dev:
254
294
  * - Only newly added handlers are logged normally.
255
295
  * - Removed handlers are logged/stroked out to indicate removal.
256
296
  * - Prevents repeated logs from flooding the console across multiple hot reloads.
297
+ * - Every init() promise reaches the {@link captureMoostInit} slot, if filled.
257
298
  */ function patchMoostHandlerLogging() {
299
+ if (installed) return;
300
+ installed = true;
258
301
  const origInit = moost.Moost.prototype.init;
302
+ const origLogMappedHandler = moost.Moost.prototype.logMappedHandler;
259
303
  /**
260
- * Monkey-patch Moost.init:
304
+ * The logging body of the patched Moost.init:
261
305
  * 1. Start a new recording cycle in LogsStorage.
262
306
  * 2. Call the real .init().
263
307
  * 3. End the recording, logging any removed handlers.
264
- */ moost.Moost.prototype.init = async function init() {
308
+ */ const initWithLogging = async function() {
265
309
  if (!logsStorage) logsStorage = new LogsStorage();
266
310
  logsStorage.startRecording();
267
311
  await origInit.call(this);
@@ -269,7 +313,14 @@ function _define_property(obj, key, value) {
269
313
  origLogMappedHandler.call(this, item.eventName, item.classConstructor, item.method, true, "❌ ");
270
314
  });
271
315
  };
272
- const origLogMappedHandler = moost.Moost.prototype.logMappedHandler;
316
+ moost.Moost.prototype.init = function init() {
317
+ const promise = initWithLogging.call(this);
318
+ if (onInitCaptured) {
319
+ promise.catch(() => {});
320
+ onInitCaptured(promise);
321
+ }
322
+ return promise;
323
+ };
273
324
  /**
274
325
  * Monkey-patch Moost.logMappedHandler:
275
326
  * If we haven’t seen this particular route+class+method combination
@@ -289,7 +340,7 @@ function _define_property(obj, key, value) {
289
340
  /**
290
341
  * Clean up Moost’s global containers and optionally remove specific instances from the registry.
291
342
  *
292
- * Every instance actually removed from a registry is **disposed** before the
343
+ * Every instance actually removed from the registry is **disposed** before the
293
344
  * caches are dropped: its `@MoostDispose` hooks (or `Symbol.asyncDispose` /
294
345
  * `Symbol.dispose`) are awaited, so a singleton owning a connection, consumer,
295
346
  * timer or file handle releases it instead of leaking one copy per reload. An
@@ -298,46 +349,25 @@ function _define_property(obj, key, value) {
298
349
  *
299
350
  * @param {Set<string>} [cleanupInstances] A set of module IDs to remove from the registry.
300
351
  * @returns the instances that were ejected (and therefore disposed)
301
- */ async function moostRestartCleanup(adapters, onEject, cleanupInstances) {
352
+ */ async function moostRestartCleanup(onEject, cleanupInstances) {
302
353
  const logger = getLogger();
303
354
  const infact = (0, moost.getMoostInfact)();
304
- const { registry, scopes } = infact;
305
- const registries = [registry, ...Object.values(scopes)];
355
+ const { registry } = infact;
306
356
  infact._cleanup();
307
357
  const mate = (0, moost.getMoostMate)();
308
- /** Instances removed from a registry by this run — disposed below, before the caches drop. */ const ejected = [];
358
+ /** Instances removed from the registry by this run — disposed below, before the caches drop. */ const ejected = [];
309
359
  if (cleanupInstances) {
310
- for (const reg of registries) {
311
- for (const key of Object.getOwnPropertySymbols(reg)) {
312
- const instance = reg[key];
313
- const viteId = mate.read(instance)?.__vite_id;
314
- if (viteId && cleanupInstances.has(viteId)) {
315
- logger.debug(`🔃 Replacing "${constructorName(instance)}"`);
316
- delete reg[key];
317
- ejected.push(instance);
318
- }
319
- }
320
- for (const key of Object.getOwnPropertySymbols(reg)) {
321
- const instance = reg[key];
322
- scanParams(instance, (type) => {
323
- if ((type === moost.Moost || type instanceof moost.Moost || type.prototype instanceof moost.Moost) && (!onEject || onEject(instance, type))) {
324
- delete reg[key];
325
- ejected.push(instance);
326
- logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "Moost")`);
327
- return true;
328
- }
329
- for (const adapter of adapters) if (adapter.compare(type) && (!onEject || onEject(instance, type))) {
330
- delete reg[key];
331
- ejected.push(instance);
332
- logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "${adapter.constructor.name}")`);
333
- return true;
334
- }
335
- });
360
+ for (const key of Object.getOwnPropertySymbols(registry)) {
361
+ const instance = registry[key];
362
+ const viteId = mate.read(instance)?.__vite_id;
363
+ if (viteId && cleanupInstances.has(viteId)) {
364
+ logger.debug(`🔃 Replacing "${constructorName(instance)}"`);
365
+ delete registry[key];
366
+ ejected.push(instance);
336
367
  }
337
- clearDependantRegistry(reg, onEject, ejected);
338
368
  }
369
+ clearDependantRegistry(registry, onEject, ejected);
339
370
  infact.registry = registry;
340
- infact.scopes = scopes;
341
371
  }
342
372
  await disposeEjected(ejected);
343
373
  (0, moost.getMoostMate)()._cleanup();
@@ -354,19 +384,21 @@ function _define_property(obj, key, value) {
354
384
  for (const instance of disposed) logger.debug(`♻️ Disposed "${constructorName(instance)}"`);
355
385
  for (const e of errors) logger.warn(`⚠️ Dispose hook "${constructorName(e.instance)}.${String(e.method)}" failed — the reload continues`);
356
386
  }
357
- function clearDependantRegistry(registry, onEject, ejected) {
387
+ /**
388
+ * Ejects, to a fixed point, every instance whose constructor dependency is not
389
+ * in the registry: an instance ejected in one pass ejects its consumers in the
390
+ * next (Moost → Database → Repository → Worker loses the whole chain). An
391
+ * `onEject` veto keeps the instance present, and with it its consumers.
392
+ */ function clearDependantRegistry(registry, onEject, ejected) {
358
393
  const logger = getLogger();
359
- const objSet = /* @__PURE__ */ new Set();
360
394
  let somethingIsDeleted = true;
361
395
  while (somethingIsDeleted) {
362
396
  somethingIsDeleted = false;
363
- for (const key of Object.getOwnPropertySymbols(registry)) {
397
+ const keys = Object.getOwnPropertySymbols(registry);
398
+ const present = new Set(keys.map((key) => registry[key].constructor));
399
+ for (const key of keys) {
364
400
  const instance = registry[key];
365
- objSet.add(Object.getPrototypeOf(instance).constructor);
366
- }
367
- for (const key of Object.getOwnPropertySymbols(registry)) {
368
- const instance = registry[key];
369
- if (checkAndEject(instance, objSet, onEject, registry, key, logger)) {
401
+ if (checkAndEject(instance, present, onEject, registry, key, logger)) {
370
402
  ejected.push(instance);
371
403
  somethingIsDeleted = true;
372
404
  }
@@ -378,7 +410,7 @@ function checkAndEject(instance, objSet, onEject, registry, key, logger) {
378
410
  scanParams(instance, (type) => {
379
411
  if (!objSet.has(type) && (!onEject || onEject(instance, type))) {
380
412
  delete registry[key];
381
- logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on "${type.name}" which is not in registry)`);
413
+ logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on "${type.name}", rebuilt by this reload)`);
382
414
  ejected = true;
383
415
  return true;
384
416
  }
@@ -643,6 +675,11 @@ function moostVite(options) {
643
675
  */ let bootGeneration = 0;
644
676
  let bootingGeneration = 0;
645
677
  /** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
678
+ /**
679
+ * The `app.init()` promises the current boot handed us (see `captureMoostInit`) —
680
+ * several when one entry boots several apps, none for a boot that never calls
681
+ * `init()`. Reset by `beginBoot()`, consumed by `settleBootInit()`.
682
+ */ let bootInitPromises = [];
646
683
  /** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
647
684
  /** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
648
685
  const adapters = isTest ? [] : [
@@ -712,6 +749,41 @@ function moostVite(options) {
712
749
  reloadRequired = true;
713
750
  };
714
751
  patchMoostHandlerLogging();
752
+ if (!isTest && !isProd) captureMoostInit((promise) => {
753
+ if (bootingGeneration !== bootGeneration) {
754
+ logger.error(`⚠️ A stale Moost boot called init() (boot generation ${bootingGeneration}, latest ${bootGeneration}) — an HMR reload race; a follow-up reload will replace it.`);
755
+ return;
756
+ }
757
+ bootInitPromises.push(promise);
758
+ });
759
+ /** Marks the boot about to run as authoritative for the current generation. */ const beginBoot = () => {
760
+ bootingGeneration = bootGeneration;
761
+ bootInitPromises = [];
762
+ };
763
+ /**
764
+ * Finishes a boot: awaits the `init()` promise(s) the entry that just executed
765
+ * handed us, so the boot is only "done" once the app is fully initialized —
766
+ * every controller bound, every `@MoostInit` hook run (see `captureMoostInit`).
767
+ * Owns `bootError`: cleared on success, set and logged when an init rejected;
768
+ * returns whether the boot succeeded.
769
+ */ const settleBootInit = async () => {
770
+ const initPromises = bootInitPromises;
771
+ bootInitPromises = [];
772
+ try {
773
+ await Promise.all(initPromises);
774
+ bootError = null;
775
+ return true;
776
+ } catch (error) {
777
+ bootError = error;
778
+ logger.error(`✖️ Moost app init failed: ${error.message}`);
779
+ return false;
780
+ }
781
+ };
782
+ /** Answers a request while `bootError` is set (see the gate in configureServer). */ const answerBootError = (res) => {
783
+ res.statusCode = 502;
784
+ res.setHeader("Content-Type", "text/plain");
785
+ res.end(`Moost app failed to load: ${bootError.message}`);
786
+ };
715
787
  /** Set by the middleware-mode `config` hook: the consumer externalized the runtime themselves. */ let runtimeExternalized = false;
716
788
  let resolvedConfig;
717
789
  const pluginConfig = {
@@ -875,7 +947,7 @@ function moostVite(options) {
875
947
  reloadRequired = false;
876
948
  reloadPromise = null;
877
949
  pendingCleanup = null;
878
- bootingGeneration = bootGeneration;
950
+ beginBoot();
879
951
  const runReload = () => {
880
952
  reloadRequired = false;
881
953
  console.log();
@@ -883,16 +955,15 @@ function moostVite(options) {
883
955
  console.log();
884
956
  return (async () => {
885
957
  try {
886
- bootingGeneration = bootGeneration;
958
+ beginBoot();
887
959
  const cleanupInstances = pendingCleanup ?? void 0;
888
960
  pendingCleanup = null;
889
- await moostRestartCleanup(adapters, options.onEject, cleanupInstances);
961
+ await moostRestartCleanup(options.onEject, cleanupInstances);
890
962
  for (const adapter of adapters) if (adapter.detected) await adapter.init();
891
963
  const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
892
964
  if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
893
965
  await ssrImport(options.entry);
894
- bootError = null;
895
- if (httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
966
+ if (await settleBootInit() && httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
896
967
  } catch (error) {
897
968
  bootError = error;
898
969
  logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
@@ -909,11 +980,16 @@ function moostVite(options) {
909
980
  }
910
981
  };
911
982
  for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
912
- await moostRestartCleanup(adapters, options.onEject);
983
+ await moostRestartCleanup(options.onEject);
913
984
  await ssrImport(options.entry);
985
+ await settleBootInit();
914
986
  server.middlewares.use(async (req, res, next) => {
915
987
  await drainReload();
916
988
  if (options.middleware && prefixes && !matchesPrefix(req.url || "", prefixes)) return next();
989
+ if (bootError) {
990
+ answerBootError(res);
991
+ return;
992
+ }
917
993
  if (moostMiddleware) {
918
994
  if (options.middleware) {
919
995
  pendingNextMap.set(req, next);
@@ -922,12 +998,6 @@ function moostVite(options) {
922
998
  }
923
999
  return moostMiddleware(req, res);
924
1000
  }
925
- if (bootError) {
926
- res.statusCode = 502;
927
- res.setHeader("Content-Type", "text/plain");
928
- res.end(`Moost app failed to load: ${bootError.message}`);
929
- return;
930
- }
931
1001
  next();
932
1002
  });
933
1003
  },
@@ -967,6 +1037,10 @@ function moostVite(options) {
967
1037
  const url = req.originalUrl || req.url || "/";
968
1038
  try {
969
1039
  await drainReload();
1040
+ if (bootError) {
1041
+ answerBootError(res);
1042
+ return;
1043
+ }
970
1044
  let template = await fs.readFile((0, node_path.resolve)(server.config.root, "index.html"), "utf8");
971
1045
  template = await server.transformIndexHtml(url, template);
972
1046
  const { render } = await server.ssrLoadModule(options.ssrEntry);
package/dist/index.mjs CHANGED
@@ -220,19 +220,63 @@ function _define_property(obj, key, value) {
220
220
  * A reference to a shared LogsStorage instance that persists
221
221
  * across init cycles in dev mode.
222
222
  */ let logsStorage;
223
+ /** Whether {@link patchMoostHandlerLogging} already installed its wrappers in this process. */ let installed = false;
223
224
  /**
224
- * Patches Moost’s .init() and .logMappedHandler() methods so that, during dev:
225
+ * The slot {@link captureMoostInit} fills: receives every `init()` promise the
226
+ * wrapper below produces. Unset in prod/test, where nothing consumes a capture.
227
+ */ let onInitCaptured;
228
+ /**
229
+ * Hands every `Moost.prototype.init()` promise to `onCapture`, letting a boot
230
+ * await the app's **initialization** instead of only its module evaluation.
231
+ *
232
+ * The documented entry shape is `app.adapter(http).listen(port)` followed by an
233
+ * **un-awaited** `app.init()`: the HTTP middleware is therefore captured *before*
234
+ * init finishes, and nothing else observes a rejecting init — a bind error, a DI
235
+ * audit error or a throwing `@MoostInit` hook would leave the dev server serving
236
+ * a half-booted app (the routes bound before the failure answer 200, the rest
237
+ * fall through to the SPA fallback) instead of the plugin's 502. That is also
238
+ * why the plugin's `bootError` gate runs BEFORE the captured middleware, and why
239
+ * the SSR fallback checks it too: `listen()` ran first and left both a middleware
240
+ * and the local-fetch hook behind.
241
+ *
242
+ * Patches the plugin's **own** `moost` import, exactly like the handler logging
243
+ * does: in dev the runtime is externalized, so the plugin and the app share one
244
+ * native `moost` instance (the premise the DI eject in `restart-cleanup` rests on
245
+ * too, and what `ssrExternalCheck` warns about when it stops holding). Unlike the
246
+ * adapters there is nothing to re-establish per reload — a static import is never
247
+ * re-evaluated — and unlike them it must NOT be pulled through the SSR module
248
+ * runner: importing `moost` there registers it as the runner's first module and
249
+ * makes subsequent hot updates re-evaluate the entry only, serving every one of
250
+ * its dependencies (controllers included) from the stale cache.
251
+ *
252
+ * There is exactly ONE `init` wrapper per process (installed by
253
+ * {@link patchMoostHandlerLogging}) and this only fills its slot, so the newest
254
+ * plugin instance wins: Vite re-imports the config on every config-file
255
+ * `server.restart()`, re-running the plugin factory, and a per-instance wrapper
256
+ * would stack on the previous one and keep the dead instance's scope alive.
257
+ *
258
+ * @param onCapture receives each captured init promise (already marked handled).
259
+ */ function captureMoostInit(onCapture) {
260
+ onInitCaptured = onCapture;
261
+ }
262
+ /**
263
+ * Patches Moost’s .init() and .logMappedHandler() methods — once per process —
264
+ * so that, during dev:
225
265
  * - Only newly added handlers are logged normally.
226
266
  * - Removed handlers are logged/stroked out to indicate removal.
227
267
  * - Prevents repeated logs from flooding the console across multiple hot reloads.
268
+ * - Every init() promise reaches the {@link captureMoostInit} slot, if filled.
228
269
  */ function patchMoostHandlerLogging() {
270
+ if (installed) return;
271
+ installed = true;
229
272
  const origInit = Moost.prototype.init;
273
+ const origLogMappedHandler = Moost.prototype.logMappedHandler;
230
274
  /**
231
- * Monkey-patch Moost.init:
275
+ * The logging body of the patched Moost.init:
232
276
  * 1. Start a new recording cycle in LogsStorage.
233
277
  * 2. Call the real .init().
234
278
  * 3. End the recording, logging any removed handlers.
235
- */ Moost.prototype.init = async function init() {
279
+ */ const initWithLogging = async function() {
236
280
  if (!logsStorage) logsStorage = new LogsStorage();
237
281
  logsStorage.startRecording();
238
282
  await origInit.call(this);
@@ -240,7 +284,14 @@ function _define_property(obj, key, value) {
240
284
  origLogMappedHandler.call(this, item.eventName, item.classConstructor, item.method, true, "❌ ");
241
285
  });
242
286
  };
243
- const origLogMappedHandler = Moost.prototype.logMappedHandler;
287
+ Moost.prototype.init = function init() {
288
+ const promise = initWithLogging.call(this);
289
+ if (onInitCaptured) {
290
+ promise.catch(() => {});
291
+ onInitCaptured(promise);
292
+ }
293
+ return promise;
294
+ };
244
295
  /**
245
296
  * Monkey-patch Moost.logMappedHandler:
246
297
  * If we haven’t seen this particular route+class+method combination
@@ -260,7 +311,7 @@ function _define_property(obj, key, value) {
260
311
  /**
261
312
  * Clean up Moost’s global containers and optionally remove specific instances from the registry.
262
313
  *
263
- * Every instance actually removed from a registry is **disposed** before the
314
+ * Every instance actually removed from the registry is **disposed** before the
264
315
  * caches are dropped: its `@MoostDispose` hooks (or `Symbol.asyncDispose` /
265
316
  * `Symbol.dispose`) are awaited, so a singleton owning a connection, consumer,
266
317
  * timer or file handle releases it instead of leaking one copy per reload. An
@@ -269,46 +320,25 @@ function _define_property(obj, key, value) {
269
320
  *
270
321
  * @param {Set<string>} [cleanupInstances] A set of module IDs to remove from the registry.
271
322
  * @returns the instances that were ejected (and therefore disposed)
272
- */ async function moostRestartCleanup(adapters, onEject, cleanupInstances) {
323
+ */ async function moostRestartCleanup(onEject, cleanupInstances) {
273
324
  const logger = getLogger();
274
325
  const infact = getMoostInfact();
275
- const { registry, scopes } = infact;
276
- const registries = [registry, ...Object.values(scopes)];
326
+ const { registry } = infact;
277
327
  infact._cleanup();
278
328
  const mate = getMoostMate();
279
- /** Instances removed from a registry by this run — disposed below, before the caches drop. */ const ejected = [];
329
+ /** Instances removed from the registry by this run — disposed below, before the caches drop. */ const ejected = [];
280
330
  if (cleanupInstances) {
281
- for (const reg of registries) {
282
- for (const key of Object.getOwnPropertySymbols(reg)) {
283
- const instance = reg[key];
284
- const viteId = mate.read(instance)?.__vite_id;
285
- if (viteId && cleanupInstances.has(viteId)) {
286
- logger.debug(`🔃 Replacing "${constructorName(instance)}"`);
287
- delete reg[key];
288
- ejected.push(instance);
289
- }
290
- }
291
- for (const key of Object.getOwnPropertySymbols(reg)) {
292
- const instance = reg[key];
293
- scanParams(instance, (type) => {
294
- if ((type === Moost || type instanceof Moost || type.prototype instanceof Moost) && (!onEject || onEject(instance, type))) {
295
- delete reg[key];
296
- ejected.push(instance);
297
- logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "Moost")`);
298
- return true;
299
- }
300
- for (const adapter of adapters) if (adapter.compare(type) && (!onEject || onEject(instance, type))) {
301
- delete reg[key];
302
- ejected.push(instance);
303
- logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on re-instantiated "${adapter.constructor.name}")`);
304
- return true;
305
- }
306
- });
331
+ for (const key of Object.getOwnPropertySymbols(registry)) {
332
+ const instance = registry[key];
333
+ const viteId = mate.read(instance)?.__vite_id;
334
+ if (viteId && cleanupInstances.has(viteId)) {
335
+ logger.debug(`🔃 Replacing "${constructorName(instance)}"`);
336
+ delete registry[key];
337
+ ejected.push(instance);
307
338
  }
308
- clearDependantRegistry(reg, onEject, ejected);
309
339
  }
340
+ clearDependantRegistry(registry, onEject, ejected);
310
341
  infact.registry = registry;
311
- infact.scopes = scopes;
312
342
  }
313
343
  await disposeEjected(ejected);
314
344
  getMoostMate()._cleanup();
@@ -325,19 +355,21 @@ function _define_property(obj, key, value) {
325
355
  for (const instance of disposed) logger.debug(`♻️ Disposed "${constructorName(instance)}"`);
326
356
  for (const e of errors) logger.warn(`⚠️ Dispose hook "${constructorName(e.instance)}.${String(e.method)}" failed — the reload continues`);
327
357
  }
328
- function clearDependantRegistry(registry, onEject, ejected) {
358
+ /**
359
+ * Ejects, to a fixed point, every instance whose constructor dependency is not
360
+ * in the registry: an instance ejected in one pass ejects its consumers in the
361
+ * next (Moost → Database → Repository → Worker loses the whole chain). An
362
+ * `onEject` veto keeps the instance present, and with it its consumers.
363
+ */ function clearDependantRegistry(registry, onEject, ejected) {
329
364
  const logger = getLogger();
330
- const objSet = /* @__PURE__ */ new Set();
331
365
  let somethingIsDeleted = true;
332
366
  while (somethingIsDeleted) {
333
367
  somethingIsDeleted = false;
334
- for (const key of Object.getOwnPropertySymbols(registry)) {
368
+ const keys = Object.getOwnPropertySymbols(registry);
369
+ const present = new Set(keys.map((key) => registry[key].constructor));
370
+ for (const key of keys) {
335
371
  const instance = registry[key];
336
- objSet.add(Object.getPrototypeOf(instance).constructor);
337
- }
338
- for (const key of Object.getOwnPropertySymbols(registry)) {
339
- const instance = registry[key];
340
- if (checkAndEject(instance, objSet, onEject, registry, key, logger)) {
372
+ if (checkAndEject(instance, present, onEject, registry, key, logger)) {
341
373
  ejected.push(instance);
342
374
  somethingIsDeleted = true;
343
375
  }
@@ -349,7 +381,7 @@ function checkAndEject(instance, objSet, onEject, registry, key, logger) {
349
381
  scanParams(instance, (type) => {
350
382
  if (!objSet.has(type) && (!onEject || onEject(instance, type))) {
351
383
  delete registry[key];
352
- logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on "${type.name}" which is not in registry)`);
384
+ logger.debug(`✖️ Ejecting "${constructorName(instance)}" (depends on "${type.name}", rebuilt by this reload)`);
353
385
  ejected = true;
354
386
  return true;
355
387
  }
@@ -612,6 +644,11 @@ function moostVite(options) {
612
644
  */ let bootGeneration = 0;
613
645
  let bootingGeneration = 0;
614
646
  /** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
647
+ /**
648
+ * The `app.init()` promises the current boot handed us (see `captureMoostInit`) —
649
+ * several when one entry boots several apps, none for a boot that never calls
650
+ * `init()`. Reset by `beginBoot()`, consumed by `settleBootInit()`.
651
+ */ let bootInitPromises = [];
615
652
  /** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
616
653
  /** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
617
654
  const adapters = [
@@ -681,6 +718,41 @@ function moostVite(options) {
681
718
  reloadRequired = true;
682
719
  };
683
720
  patchMoostHandlerLogging();
721
+ captureMoostInit((promise) => {
722
+ if (bootingGeneration !== bootGeneration) {
723
+ logger.error(`⚠️ A stale Moost boot called init() (boot generation ${bootingGeneration}, latest ${bootGeneration}) — an HMR reload race; a follow-up reload will replace it.`);
724
+ return;
725
+ }
726
+ bootInitPromises.push(promise);
727
+ });
728
+ /** Marks the boot about to run as authoritative for the current generation. */ const beginBoot = () => {
729
+ bootingGeneration = bootGeneration;
730
+ bootInitPromises = [];
731
+ };
732
+ /**
733
+ * Finishes a boot: awaits the `init()` promise(s) the entry that just executed
734
+ * handed us, so the boot is only "done" once the app is fully initialized —
735
+ * every controller bound, every `@MoostInit` hook run (see `captureMoostInit`).
736
+ * Owns `bootError`: cleared on success, set and logged when an init rejected;
737
+ * returns whether the boot succeeded.
738
+ */ const settleBootInit = async () => {
739
+ const initPromises = bootInitPromises;
740
+ bootInitPromises = [];
741
+ try {
742
+ await Promise.all(initPromises);
743
+ bootError = null;
744
+ return true;
745
+ } catch (error) {
746
+ bootError = error;
747
+ logger.error(`✖️ Moost app init failed: ${error.message}`);
748
+ return false;
749
+ }
750
+ };
751
+ /** Answers a request while `bootError` is set (see the gate in configureServer). */ const answerBootError = (res) => {
752
+ res.statusCode = 502;
753
+ res.setHeader("Content-Type", "text/plain");
754
+ res.end(`Moost app failed to load: ${bootError.message}`);
755
+ };
684
756
  /** Set by the middleware-mode `config` hook: the consumer externalized the runtime themselves. */ let runtimeExternalized = false;
685
757
  let resolvedConfig;
686
758
  return [{
@@ -844,7 +916,7 @@ function moostVite(options) {
844
916
  reloadRequired = false;
845
917
  reloadPromise = null;
846
918
  pendingCleanup = null;
847
- bootingGeneration = bootGeneration;
919
+ beginBoot();
848
920
  const runReload = () => {
849
921
  reloadRequired = false;
850
922
  console.log();
@@ -852,16 +924,15 @@ function moostVite(options) {
852
924
  console.log();
853
925
  return (async () => {
854
926
  try {
855
- bootingGeneration = bootGeneration;
927
+ beginBoot();
856
928
  const cleanupInstances = pendingCleanup ?? void 0;
857
929
  pendingCleanup = null;
858
- await moostRestartCleanup(adapters, options.onEject, cleanupInstances);
930
+ await moostRestartCleanup(options.onEject, cleanupInstances);
859
931
  for (const adapter of adapters) if (adapter.detected) await adapter.init();
860
932
  const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
861
933
  if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
862
934
  await ssrImport(options.entry);
863
- bootError = null;
864
- if (httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
935
+ if (await settleBootInit() && httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
865
936
  } catch (error) {
866
937
  bootError = error;
867
938
  logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
@@ -878,11 +949,16 @@ function moostVite(options) {
878
949
  }
879
950
  };
880
951
  for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
881
- await moostRestartCleanup(adapters, options.onEject);
952
+ await moostRestartCleanup(options.onEject);
882
953
  await ssrImport(options.entry);
954
+ await settleBootInit();
883
955
  server.middlewares.use(async (req, res, next) => {
884
956
  await drainReload();
885
957
  if (options.middleware && prefixes && !matchesPrefix(req.url || "", prefixes)) return next();
958
+ if (bootError) {
959
+ answerBootError(res);
960
+ return;
961
+ }
886
962
  if (moostMiddleware) {
887
963
  if (options.middleware) {
888
964
  pendingNextMap.set(req, next);
@@ -891,12 +967,6 @@ function moostVite(options) {
891
967
  }
892
968
  return moostMiddleware(req, res);
893
969
  }
894
- if (bootError) {
895
- res.statusCode = 502;
896
- res.setHeader("Content-Type", "text/plain");
897
- res.end(`Moost app failed to load: ${bootError.message}`);
898
- return;
899
- }
900
970
  next();
901
971
  });
902
972
  },
@@ -928,6 +998,10 @@ function moostVite(options) {
928
998
  const url = req.originalUrl || req.url || "/";
929
999
  try {
930
1000
  await drainReload();
1001
+ if (bootError) {
1002
+ answerBootError(res);
1003
+ return;
1004
+ }
931
1005
  let template = await fs.readFile(resolve(server.config.root, "index.html"), "utf8");
932
1006
  template = await server.transformIndexHtml(url, template);
933
1007
  const { render } = await server.ssrLoadModule(options.ssrEntry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moostjs/vite",
3
- "version": "0.6.36",
3
+ "version": "0.6.38",
4
4
  "description": "Vite Dev plugin for moostjs",
5
5
  "keywords": [
6
6
  "composables",
@@ -58,8 +58,8 @@
58
58
  "peerDependencies": {
59
59
  "sirv": "^3.0.0",
60
60
  "vite": "^8.0.0",
61
- "@moostjs/event-http": "^0.6.36",
62
- "moost": "^0.6.36"
61
+ "@moostjs/event-http": "^0.6.38",
62
+ "moost": "^0.6.38"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "sirv": {