@moostjs/vite 0.6.36 → 0.6.37

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 +107 -14
  2. package/dist/index.mjs +107 -14
  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
@@ -643,6 +694,11 @@ function moostVite(options) {
643
694
  */ let bootGeneration = 0;
644
695
  let bootingGeneration = 0;
645
696
  /** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
697
+ /**
698
+ * The `app.init()` promises the current boot handed us (see `captureMoostInit`) —
699
+ * several when one entry boots several apps, none for a boot that never calls
700
+ * `init()`. Reset by `beginBoot()`, consumed by `settleBootInit()`.
701
+ */ let bootInitPromises = [];
646
702
  /** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
647
703
  /** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
648
704
  const adapters = isTest ? [] : [
@@ -712,6 +768,41 @@ function moostVite(options) {
712
768
  reloadRequired = true;
713
769
  };
714
770
  patchMoostHandlerLogging();
771
+ if (!isTest && !isProd) captureMoostInit((promise) => {
772
+ if (bootingGeneration !== bootGeneration) {
773
+ logger.error(`⚠️ A stale Moost boot called init() (boot generation ${bootingGeneration}, latest ${bootGeneration}) — an HMR reload race; a follow-up reload will replace it.`);
774
+ return;
775
+ }
776
+ bootInitPromises.push(promise);
777
+ });
778
+ /** Marks the boot about to run as authoritative for the current generation. */ const beginBoot = () => {
779
+ bootingGeneration = bootGeneration;
780
+ bootInitPromises = [];
781
+ };
782
+ /**
783
+ * Finishes a boot: awaits the `init()` promise(s) the entry that just executed
784
+ * handed us, so the boot is only "done" once the app is fully initialized —
785
+ * every controller bound, every `@MoostInit` hook run (see `captureMoostInit`).
786
+ * Owns `bootError`: cleared on success, set and logged when an init rejected;
787
+ * returns whether the boot succeeded.
788
+ */ const settleBootInit = async () => {
789
+ const initPromises = bootInitPromises;
790
+ bootInitPromises = [];
791
+ try {
792
+ await Promise.all(initPromises);
793
+ bootError = null;
794
+ return true;
795
+ } catch (error) {
796
+ bootError = error;
797
+ logger.error(`✖️ Moost app init failed: ${error.message}`);
798
+ return false;
799
+ }
800
+ };
801
+ /** Answers a request while `bootError` is set (see the gate in configureServer). */ const answerBootError = (res) => {
802
+ res.statusCode = 502;
803
+ res.setHeader("Content-Type", "text/plain");
804
+ res.end(`Moost app failed to load: ${bootError.message}`);
805
+ };
715
806
  /** Set by the middleware-mode `config` hook: the consumer externalized the runtime themselves. */ let runtimeExternalized = false;
716
807
  let resolvedConfig;
717
808
  const pluginConfig = {
@@ -875,7 +966,7 @@ function moostVite(options) {
875
966
  reloadRequired = false;
876
967
  reloadPromise = null;
877
968
  pendingCleanup = null;
878
- bootingGeneration = bootGeneration;
969
+ beginBoot();
879
970
  const runReload = () => {
880
971
  reloadRequired = false;
881
972
  console.log();
@@ -883,7 +974,7 @@ function moostVite(options) {
883
974
  console.log();
884
975
  return (async () => {
885
976
  try {
886
- bootingGeneration = bootGeneration;
977
+ beginBoot();
887
978
  const cleanupInstances = pendingCleanup ?? void 0;
888
979
  pendingCleanup = null;
889
980
  await moostRestartCleanup(adapters, options.onEject, cleanupInstances);
@@ -891,8 +982,7 @@ function moostVite(options) {
891
982
  const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
892
983
  if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
893
984
  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().`);
985
+ if (await settleBootInit() && httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
896
986
  } catch (error) {
897
987
  bootError = error;
898
988
  logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
@@ -911,9 +1001,14 @@ function moostVite(options) {
911
1001
  for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
912
1002
  await moostRestartCleanup(adapters, options.onEject);
913
1003
  await ssrImport(options.entry);
1004
+ await settleBootInit();
914
1005
  server.middlewares.use(async (req, res, next) => {
915
1006
  await drainReload();
916
1007
  if (options.middleware && prefixes && !matchesPrefix(req.url || "", prefixes)) return next();
1008
+ if (bootError) {
1009
+ answerBootError(res);
1010
+ return;
1011
+ }
917
1012
  if (moostMiddleware) {
918
1013
  if (options.middleware) {
919
1014
  pendingNextMap.set(req, next);
@@ -922,12 +1017,6 @@ function moostVite(options) {
922
1017
  }
923
1018
  return moostMiddleware(req, res);
924
1019
  }
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
1020
  next();
932
1021
  });
933
1022
  },
@@ -967,6 +1056,10 @@ function moostVite(options) {
967
1056
  const url = req.originalUrl || req.url || "/";
968
1057
  try {
969
1058
  await drainReload();
1059
+ if (bootError) {
1060
+ answerBootError(res);
1061
+ return;
1062
+ }
970
1063
  let template = await fs.readFile((0, node_path.resolve)(server.config.root, "index.html"), "utf8");
971
1064
  template = await server.transformIndexHtml(url, template);
972
1065
  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
@@ -612,6 +663,11 @@ function moostVite(options) {
612
663
  */ let bootGeneration = 0;
613
664
  let bootingGeneration = 0;
614
665
  /** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
666
+ /**
667
+ * The `app.init()` promises the current boot handed us (see `captureMoostInit`) —
668
+ * several when one entry boots several apps, none for a boot that never calls
669
+ * `init()`. Reset by `beginBoot()`, consumed by `settleBootInit()`.
670
+ */ let bootInitPromises = [];
615
671
  /** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
616
672
  /** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
617
673
  const adapters = [
@@ -681,6 +737,41 @@ function moostVite(options) {
681
737
  reloadRequired = true;
682
738
  };
683
739
  patchMoostHandlerLogging();
740
+ captureMoostInit((promise) => {
741
+ if (bootingGeneration !== bootGeneration) {
742
+ logger.error(`⚠️ A stale Moost boot called init() (boot generation ${bootingGeneration}, latest ${bootGeneration}) — an HMR reload race; a follow-up reload will replace it.`);
743
+ return;
744
+ }
745
+ bootInitPromises.push(promise);
746
+ });
747
+ /** Marks the boot about to run as authoritative for the current generation. */ const beginBoot = () => {
748
+ bootingGeneration = bootGeneration;
749
+ bootInitPromises = [];
750
+ };
751
+ /**
752
+ * Finishes a boot: awaits the `init()` promise(s) the entry that just executed
753
+ * handed us, so the boot is only "done" once the app is fully initialized —
754
+ * every controller bound, every `@MoostInit` hook run (see `captureMoostInit`).
755
+ * Owns `bootError`: cleared on success, set and logged when an init rejected;
756
+ * returns whether the boot succeeded.
757
+ */ const settleBootInit = async () => {
758
+ const initPromises = bootInitPromises;
759
+ bootInitPromises = [];
760
+ try {
761
+ await Promise.all(initPromises);
762
+ bootError = null;
763
+ return true;
764
+ } catch (error) {
765
+ bootError = error;
766
+ logger.error(`✖️ Moost app init failed: ${error.message}`);
767
+ return false;
768
+ }
769
+ };
770
+ /** Answers a request while `bootError` is set (see the gate in configureServer). */ const answerBootError = (res) => {
771
+ res.statusCode = 502;
772
+ res.setHeader("Content-Type", "text/plain");
773
+ res.end(`Moost app failed to load: ${bootError.message}`);
774
+ };
684
775
  /** Set by the middleware-mode `config` hook: the consumer externalized the runtime themselves. */ let runtimeExternalized = false;
685
776
  let resolvedConfig;
686
777
  return [{
@@ -844,7 +935,7 @@ function moostVite(options) {
844
935
  reloadRequired = false;
845
936
  reloadPromise = null;
846
937
  pendingCleanup = null;
847
- bootingGeneration = bootGeneration;
938
+ beginBoot();
848
939
  const runReload = () => {
849
940
  reloadRequired = false;
850
941
  console.log();
@@ -852,7 +943,7 @@ function moostVite(options) {
852
943
  console.log();
853
944
  return (async () => {
854
945
  try {
855
- bootingGeneration = bootGeneration;
946
+ beginBoot();
856
947
  const cleanupInstances = pendingCleanup ?? void 0;
857
948
  pendingCleanup = null;
858
949
  await moostRestartCleanup(adapters, options.onEject, cleanupInstances);
@@ -860,8 +951,7 @@ function moostVite(options) {
860
951
  const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
861
952
  if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
862
953
  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().`);
954
+ if (await settleBootInit() && httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
865
955
  } catch (error) {
866
956
  bootError = error;
867
957
  logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
@@ -880,9 +970,14 @@ function moostVite(options) {
880
970
  for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
881
971
  await moostRestartCleanup(adapters, options.onEject);
882
972
  await ssrImport(options.entry);
973
+ await settleBootInit();
883
974
  server.middlewares.use(async (req, res, next) => {
884
975
  await drainReload();
885
976
  if (options.middleware && prefixes && !matchesPrefix(req.url || "", prefixes)) return next();
977
+ if (bootError) {
978
+ answerBootError(res);
979
+ return;
980
+ }
886
981
  if (moostMiddleware) {
887
982
  if (options.middleware) {
888
983
  pendingNextMap.set(req, next);
@@ -891,12 +986,6 @@ function moostVite(options) {
891
986
  }
892
987
  return moostMiddleware(req, res);
893
988
  }
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
989
  next();
901
990
  });
902
991
  },
@@ -928,6 +1017,10 @@ function moostVite(options) {
928
1017
  const url = req.originalUrl || req.url || "/";
929
1018
  try {
930
1019
  await drainReload();
1020
+ if (bootError) {
1021
+ answerBootError(res);
1022
+ return;
1023
+ }
931
1024
  let template = await fs.readFile(resolve(server.config.root, "index.html"), "utf8");
932
1025
  template = await server.transformIndexHtml(url, template);
933
1026
  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.37",
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.37",
62
+ "moost": "^0.6.37"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "sirv": {