@moostjs/vite 0.6.25 → 0.6.27

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
@@ -101,6 +101,8 @@ moostVite({
101
101
  })
102
102
  ```
103
103
 
104
+ Note that the production server generated by `vite build` defaults the prefix to `'/api'` when the option is omitted — set `prefix` explicitly so dev (where all requests pass through Moost without it) and prod route the same way.
105
+
104
106
  ## SSR Mode
105
107
 
106
108
  Add `ssrEntry` to enable server-side rendering. The plugin handles everything automatically — SSR rendering in dev, multi-environment builds for production:
@@ -180,7 +182,7 @@ export default defineConfig({
180
182
 
181
183
  **Don't externalize:** workspace packages (`workspace:*`), anything that uses `Symbol()` as a public slot key (Moost, wooks, atscript), anything not reliably hoisted by pnpm.
182
184
 
183
- You can also opt out of bundle-everything by setting `ssr.noExternal` to an explicit list — the plugin honors it literally (appending only `/^@moostjs\/vite($|\/)/` so its own `define:` substitutions still land):
185
+ You can also opt out of bundle-everything by setting `ssr.noExternal` to an explicit list — the plugin honors it, appending `/^@moostjs\/vite($|\/)/` (so its own `define:` substitutions still land) plus, unless you externalize the runtime yourself, the moost/wooks runtime patterns described below:
184
186
 
185
187
  ```ts
186
188
  ssr: {
@@ -225,12 +227,12 @@ The plugin injects a `__VITE_ID` decorator on `@Injectable` and `@Controller` cl
225
227
  | `host` | `string` | `'localhost'` | Dev server host (backend mode only) |
226
228
  | `outDir` | `string` | `'dist'` | Build output directory (backend mode only) |
227
229
  | `format` | `'cjs' \| 'esm'` | `'esm'` | Output module format (backend mode only) |
228
- | `sourcemap` | `boolean` | `true` | Generate source maps (backend mode only) |
230
+ | `sourcemap` | `boolean` | `true` | Generate source maps (both modes; in middleware mode applies to the `dist/server` SSR build) |
229
231
  | `externals` | `boolean \| object` | `true` | Configure external dependencies (backend mode only) |
230
232
  | `onEject` | `function` | — | Hook to control DI instance ejection during HMR |
231
233
  | `ssrFetch` | `boolean` | `true` | Enable local fetch interception for SSR |
232
234
  | `middleware` | `boolean` | `false` | Run Moost as Connect middleware (Vite serves frontend) |
233
- | `prefix` | `string` | — | URL prefix filter for middleware mode (e.g. `'/api'`) |
235
+ | `prefix` | `string` | `'/api'` (prod server) | URL prefix filter for middleware mode (e.g. `'/api'`); the production server defaults to `'/api'` when omitted |
234
236
  | `ssrEntry` | `string` | — | Vue/React SSR entry module (e.g. `'/src/entry-server.ts'`) |
235
237
  | `ssrOutlet` | `string` | `'<!--ssr-outlet-->'` | HTML placeholder for SSR-rendered content |
236
238
  | `ssrState` | `string` | `'<!--ssr-state-->'` | HTML placeholder for SSR state transfer script |
package/dist/index.cjs CHANGED
@@ -42,6 +42,31 @@ function entryBasename(entry) {
42
42
  return entry.split("/").pop().replace(/\.ts$/, ".js");
43
43
  }
44
44
  /**
45
+ * Normalizes the `prefix` option (single mount or list of mounts) into a list of
46
+ * normalized entries: leading slash ensured, trailing slash stripped.
47
+ * Returns `undefined` when no prefix is configured (empty input included) —
48
+ * the "route everything through Moost first" mode.
49
+ */ function normalizePrefixes(prefix) {
50
+ if (!prefix) return;
51
+ const normalized = (Array.isArray(prefix) ? prefix : [prefix]).filter(Boolean).map((entry) => {
52
+ let value = entry;
53
+ if (!value.startsWith("/")) value = `/${value}`;
54
+ if (value.endsWith("/")) value = value.slice(0, -1);
55
+ return value;
56
+ });
57
+ return normalized.length > 0 ? normalized : void 0;
58
+ }
59
+ /**
60
+ * Checks whether a request url falls under any of the normalized prefix mounts:
61
+ * exact match, a path segment below it, or the mount itself with a query string.
62
+ */ function matchesPrefix(url, prefixes) {
63
+ return prefixes.some((prefix) => {
64
+ if (!url.startsWith(prefix)) return false;
65
+ const next = url[prefix.length];
66
+ return next === void 0 || next === "/" || next === "?";
67
+ });
68
+ }
69
+ /**
45
70
  * Recursively gathers all importer modules upstream from a given module.
46
71
  *
47
72
  * @param {EnvironmentModuleNode | undefined} moduleNode - The starting module node.
@@ -340,13 +365,7 @@ function moostVite(options) {
340
365
  const isTest = process.env.NODE_ENV === "test";
341
366
  const isProd = process.env.NODE_ENV === "production";
342
367
  const externals = options.externals ?? true;
343
- let prefix = options.prefix;
344
- if (prefix) {
345
- if (!prefix.startsWith("/")) prefix = `/${prefix}`;
346
- if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
347
- }
348
- const prefixSlash = prefix ? `${prefix}/` : void 0;
349
- const prefixQuery = prefix ? `${prefix}?` : void 0;
368
+ const prefixes = normalizePrefixes(options.prefix);
350
369
  let moostMiddleware = null;
351
370
  let localFetchTeardown = null;
352
371
  /** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
@@ -379,6 +398,31 @@ function moostVite(options) {
379
398
  * Drains any pending Moost app reload before a request is served. Assigned once
380
399
  * the SSR module runner exists (see configureServer); a no-op until then.
381
400
  */ let drainReload = () => Promise.resolve();
401
+ /**
402
+ * Last Moost entry import failure. While set, the dev middleware answers matching
403
+ * requests with 502 instead of letting them fall through to the SPA/SSR fallback
404
+ * (which would serve index.html for API routes). Cleared on a successful reload;
405
+ * the next hot update schedules the retry.
406
+ */ let bootError = null;
407
+ /**
408
+ * Looks up the Moost entry node with the verbatim `options.entry` string — the
409
+ * module graph keys (and memoizes) unresolved urls as-is, so the same spelling
410
+ * that was passed to `runner.import()` is guaranteed to return the very node
411
+ * that import created.
412
+ */ const getEntryModule = (moduleGraph) => moduleGraph.getModuleByUrl(options.entry);
413
+ /**
414
+ * Drops the captured Moost app so the next request triggers a full reload:
415
+ * releases the middleware + local fetch and ejects DI instances affected by
416
+ * the changed modules.
417
+ */ const ejectApp = (cleanupInstances) => {
418
+ moostMiddleware = null;
419
+ if (localFetchTeardown) {
420
+ localFetchTeardown();
421
+ localFetchTeardown = null;
422
+ }
423
+ moostRestartCleanup(adapters, options.onEject, cleanupInstances);
424
+ reloadRequired = true;
425
+ };
382
426
  patchMoostHandlerLogging();
383
427
  const pluginConfig = {
384
428
  name: PLUGIN_NAME,
@@ -390,7 +434,7 @@ function moostVite(options) {
390
434
  const serverDefines = {
391
435
  "process.env.MOOST_DEFERRED_ENV": "\"production\"",
392
436
  __MOOST_ENTRY__: JSON.stringify(`./${entryKey}.js`),
393
- __MOOST_PREFIX__: JSON.stringify(prefix || "/api")
437
+ __MOOST_PREFIX__: JSON.stringify(prefixes ?? null)
394
438
  };
395
439
  const ssrInput = {
396
440
  server: options.serverEntry || generatedServerEntry(cfg.root),
@@ -487,7 +531,7 @@ function moostVite(options) {
487
531
  entry: options.entry,
488
532
  ssrEntry: options.ssrEntry,
489
533
  serverEntry: options.serverEntry,
490
- prefix,
534
+ prefix: prefixes,
491
535
  port: options.port,
492
536
  ssrOutlet: options.ssrOutlet,
493
537
  ssrState: options.ssrState
@@ -523,16 +567,27 @@ function moostVite(options) {
523
567
  };
524
568
  },
525
569
  async configureServer(server) {
526
- const runner = (0, vite.createServerModuleRunner)(server.environments.ssr);
570
+ const runner = (0, vite.createServerModuleRunner)(server.environments.ssr, { hmr: false });
527
571
  const ssrImport = (id) => runner.import(id);
572
+ bootError = null;
573
+ reloadRequired = false;
574
+ reloadPromise = null;
528
575
  const runReload = () => {
529
576
  reloadRequired = false;
530
577
  console.log();
531
578
  logger.debug("🚀 Reloading Moost App...");
532
579
  console.log();
533
580
  return (async () => {
534
- for (const adapter of adapters) if (adapter.detected) await adapter.init();
535
- await ssrImport(options.entry);
581
+ try {
582
+ for (const adapter of adapters) if (adapter.detected) await adapter.init();
583
+ const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
584
+ if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
585
+ await ssrImport(options.entry);
586
+ bootError = null;
587
+ } catch (error) {
588
+ bootError = error;
589
+ logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
590
+ }
536
591
  await new Promise((resolve) => setTimeout(resolve, 1));
537
592
  })().finally(() => {
538
593
  reloadPromise = null;
@@ -549,10 +604,7 @@ function moostVite(options) {
549
604
  await ssrImport(options.entry);
550
605
  server.middlewares.use(async (req, res, next) => {
551
606
  await drainReload();
552
- if (options.middleware && prefix) {
553
- const url = req.url || "";
554
- if (url !== prefix && !url.startsWith(prefixSlash) && !url.startsWith(prefixQuery)) return next();
555
- }
607
+ if (options.middleware && prefixes && !matchesPrefix(req.url || "", prefixes)) return next();
556
608
  if (moostMiddleware) {
557
609
  if (options.middleware) {
558
610
  pendingNextMap.set(req, next);
@@ -561,30 +613,29 @@ function moostVite(options) {
561
613
  }
562
614
  return moostMiddleware(req, res);
563
615
  }
616
+ if (bootError) {
617
+ res.statusCode = 502;
618
+ res.setHeader("Content-Type", "text/plain");
619
+ res.end(`Moost app failed to load: ${bootError.message}`);
620
+ return;
621
+ }
564
622
  next();
565
623
  });
566
624
  },
567
- hotUpdate({ file }) {
568
- if (file.endsWith(".ts")) {
569
- const modules = this.environment.moduleGraph.getModulesByFile(file);
570
- if (modules) {
571
- logger.debug(`🔃 Hot update: ${file}`);
572
- const cleanupInstances = /* @__PURE__ */ new Set();
573
- for (const mod of modules) {
574
- const allImporters = gatherAllImporters(mod);
575
- for (const impModule of allImporters) if (impModule.id) cleanupInstances.add(impModule.id);
576
- this.environment.moduleGraph.invalidateModule(mod);
577
- }
578
- moostMiddleware = null;
579
- if (localFetchTeardown) {
580
- localFetchTeardown();
581
- localFetchTeardown = null;
582
- }
583
- moostRestartCleanup(adapters, options.onEject, cleanupInstances);
584
- reloadRequired = true;
585
- }
586
- return [];
587
- }
625
+ async hotUpdate({ file, modules }) {
626
+ if (this.environment.name !== "ssr" || modules.length === 0) return;
627
+ const { moduleGraph } = this.environment;
628
+ const entryModule = await getEntryModule(moduleGraph);
629
+ const importerSets = modules.map((mod) => gatherAllImporters(mod));
630
+ if (entryModule && !importerSets.some((importers) => importers.has(entryModule))) return;
631
+ logger.debug(`🔃 Hot update: ${file}`);
632
+ const cleanupInstances = /* @__PURE__ */ new Set();
633
+ for (const importers of importerSets) for (const impModule of importers) if (impModule.id) cleanupInstances.add(impModule.id);
634
+ for (const mod of modules) moduleGraph.invalidateModule(mod);
635
+ if (entryModule) moduleGraph.invalidateModule(entryModule);
636
+ else moduleGraph.invalidateAll();
637
+ ejectApp(cleanupInstances);
638
+ return [];
588
639
  }
589
640
  };
590
641
  if (isProd || isTest) {
@@ -606,7 +657,7 @@ function moostVite(options) {
606
657
  const url = req.originalUrl || req.url || "/";
607
658
  try {
608
659
  await drainReload();
609
- let template = await fs.readFile((0, node_path.resolve)(process.cwd(), "index.html"), "utf8");
660
+ let template = await fs.readFile((0, node_path.resolve)(server.config.root, "index.html"), "utf8");
610
661
  template = await server.transformIndexHtml(url, template);
611
662
  const { render } = await server.ssrLoadModule(options.ssrEntry);
612
663
  const { html: appHtml, state } = await render(url);
package/dist/index.d.ts CHANGED
@@ -78,9 +78,10 @@ interface TMoostViteDevOptions {
78
78
  ssrFetch?: boolean;
79
79
  /**
80
80
  * Run Moost as Connect middleware instead of taking over the server.
81
- * When enabled, Moost handles only matching routes (guarded by `prefix`),
82
- * and unmatched requests fall through to Vite's default handler
83
- * (static assets, Vue/React pages, HMR client).
81
+ * When enabled, Moost runs first and unmatched requests fall through to
82
+ * Vite's default handler (static assets, Vue/React pages, HMR client).
83
+ * With `prefix` configured, requests outside the mount(s) skip the Moost
84
+ * router entirely (fast path).
84
85
  *
85
86
  * Use this for fullstack apps where Vite serves the frontend (Vue, React, etc.)
86
87
  * and Moost handles API routes.
@@ -89,13 +90,16 @@ interface TMoostViteDevOptions {
89
90
  */
90
91
  middleware?: boolean;
91
92
  /**
92
- * URL prefix for Moost API routes in middleware mode.
93
- * Only requests starting with this prefix are dispatched to Moost.
93
+ * URL mount(s) for Moost routes in middleware mode — a single prefix or a
94
+ * list of prefixes. Requests outside every mount skip the Moost router and
95
+ * go straight to the frontend handler (an optimization/guard, in both dev
96
+ * and the generated prod server). When omitted, every request enters Moost
97
+ * first and unmatched routes fall through to the frontend.
94
98
  * Ignored when `middleware` is `false`.
95
99
  *
96
- * Example: `'/api'`
100
+ * Example: `'/api'` or `['/api', '/.well-known']`
97
101
  */
98
- prefix?: string;
102
+ prefix?: string | string[];
99
103
  /**
100
104
  * Vue/React SSR entry module path. Used by the SSR server helper
101
105
  * (`@moostjs/vite/server`) for server-side rendering.
package/dist/index.mjs CHANGED
@@ -13,6 +13,31 @@ function entryBasename(entry) {
13
13
  return entry.split("/").pop().replace(/\.ts$/, ".js");
14
14
  }
15
15
  /**
16
+ * Normalizes the `prefix` option (single mount or list of mounts) into a list of
17
+ * normalized entries: leading slash ensured, trailing slash stripped.
18
+ * Returns `undefined` when no prefix is configured (empty input included) —
19
+ * the "route everything through Moost first" mode.
20
+ */ function normalizePrefixes(prefix) {
21
+ if (!prefix) return;
22
+ const normalized = (Array.isArray(prefix) ? prefix : [prefix]).filter(Boolean).map((entry) => {
23
+ let value = entry;
24
+ if (!value.startsWith("/")) value = `/${value}`;
25
+ if (value.endsWith("/")) value = value.slice(0, -1);
26
+ return value;
27
+ });
28
+ return normalized.length > 0 ? normalized : void 0;
29
+ }
30
+ /**
31
+ * Checks whether a request url falls under any of the normalized prefix mounts:
32
+ * exact match, a path segment below it, or the mount itself with a query string.
33
+ */ function matchesPrefix(url, prefixes) {
34
+ return prefixes.some((prefix) => {
35
+ if (!url.startsWith(prefix)) return false;
36
+ const next = url[prefix.length];
37
+ return next === void 0 || next === "/" || next === "?";
38
+ });
39
+ }
40
+ /**
16
41
  * Recursively gathers all importer modules upstream from a given module.
17
42
  *
18
43
  * @param {EnvironmentModuleNode | undefined} moduleNode - The starting module node.
@@ -309,13 +334,7 @@ function generatedServerEntry(root) {
309
334
  }
310
335
  function moostVite(options) {
311
336
  const externals = options.externals ?? true;
312
- let prefix = options.prefix;
313
- if (prefix) {
314
- if (!prefix.startsWith("/")) prefix = `/${prefix}`;
315
- if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
316
- }
317
- const prefixSlash = prefix ? `${prefix}/` : void 0;
318
- const prefixQuery = prefix ? `${prefix}?` : void 0;
337
+ const prefixes = normalizePrefixes(options.prefix);
319
338
  let moostMiddleware = null;
320
339
  let localFetchTeardown = null;
321
340
  /** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
@@ -348,6 +367,31 @@ function moostVite(options) {
348
367
  * Drains any pending Moost app reload before a request is served. Assigned once
349
368
  * the SSR module runner exists (see configureServer); a no-op until then.
350
369
  */ let drainReload = () => Promise.resolve();
370
+ /**
371
+ * Last Moost entry import failure. While set, the dev middleware answers matching
372
+ * requests with 502 instead of letting them fall through to the SPA/SSR fallback
373
+ * (which would serve index.html for API routes). Cleared on a successful reload;
374
+ * the next hot update schedules the retry.
375
+ */ let bootError = null;
376
+ /**
377
+ * Looks up the Moost entry node with the verbatim `options.entry` string — the
378
+ * module graph keys (and memoizes) unresolved urls as-is, so the same spelling
379
+ * that was passed to `runner.import()` is guaranteed to return the very node
380
+ * that import created.
381
+ */ const getEntryModule = (moduleGraph) => moduleGraph.getModuleByUrl(options.entry);
382
+ /**
383
+ * Drops the captured Moost app so the next request triggers a full reload:
384
+ * releases the middleware + local fetch and ejects DI instances affected by
385
+ * the changed modules.
386
+ */ const ejectApp = (cleanupInstances) => {
387
+ moostMiddleware = null;
388
+ if (localFetchTeardown) {
389
+ localFetchTeardown();
390
+ localFetchTeardown = null;
391
+ }
392
+ moostRestartCleanup(adapters, options.onEject, cleanupInstances);
393
+ reloadRequired = true;
394
+ };
351
395
  patchMoostHandlerLogging();
352
396
  return [{
353
397
  name: PLUGIN_NAME,
@@ -359,7 +403,7 @@ function moostVite(options) {
359
403
  const serverDefines = {
360
404
  "process.env.MOOST_DEFERRED_ENV": "\"production\"",
361
405
  __MOOST_ENTRY__: JSON.stringify(`./${entryKey}.js`),
362
- __MOOST_PREFIX__: JSON.stringify(prefix || "/api")
406
+ __MOOST_PREFIX__: JSON.stringify(prefixes ?? null)
363
407
  };
364
408
  const ssrInput = {
365
409
  server: options.serverEntry || generatedServerEntry(cfg.root),
@@ -456,7 +500,7 @@ function moostVite(options) {
456
500
  entry: options.entry,
457
501
  ssrEntry: options.ssrEntry,
458
502
  serverEntry: options.serverEntry,
459
- prefix,
503
+ prefix: prefixes,
460
504
  port: options.port,
461
505
  ssrOutlet: options.ssrOutlet,
462
506
  ssrState: options.ssrState
@@ -492,16 +536,27 @@ function moostVite(options) {
492
536
  };
493
537
  },
494
538
  async configureServer(server) {
495
- const runner = createServerModuleRunner(server.environments.ssr);
539
+ const runner = createServerModuleRunner(server.environments.ssr, { hmr: false });
496
540
  const ssrImport = (id) => runner.import(id);
541
+ bootError = null;
542
+ reloadRequired = false;
543
+ reloadPromise = null;
497
544
  const runReload = () => {
498
545
  reloadRequired = false;
499
546
  console.log();
500
547
  logger.debug("🚀 Reloading Moost App...");
501
548
  console.log();
502
549
  return (async () => {
503
- for (const adapter of adapters) if (adapter.detected) await adapter.init();
504
- await ssrImport(options.entry);
550
+ try {
551
+ for (const adapter of adapters) if (adapter.detected) await adapter.init();
552
+ const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
553
+ if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
554
+ await ssrImport(options.entry);
555
+ bootError = null;
556
+ } catch (error) {
557
+ bootError = error;
558
+ logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
559
+ }
505
560
  await new Promise((resolve) => setTimeout(resolve, 1));
506
561
  })().finally(() => {
507
562
  reloadPromise = null;
@@ -518,10 +573,7 @@ function moostVite(options) {
518
573
  await ssrImport(options.entry);
519
574
  server.middlewares.use(async (req, res, next) => {
520
575
  await drainReload();
521
- if (options.middleware && prefix) {
522
- const url = req.url || "";
523
- if (url !== prefix && !url.startsWith(prefixSlash) && !url.startsWith(prefixQuery)) return next();
524
- }
576
+ if (options.middleware && prefixes && !matchesPrefix(req.url || "", prefixes)) return next();
525
577
  if (moostMiddleware) {
526
578
  if (options.middleware) {
527
579
  pendingNextMap.set(req, next);
@@ -530,30 +582,29 @@ function moostVite(options) {
530
582
  }
531
583
  return moostMiddleware(req, res);
532
584
  }
585
+ if (bootError) {
586
+ res.statusCode = 502;
587
+ res.setHeader("Content-Type", "text/plain");
588
+ res.end(`Moost app failed to load: ${bootError.message}`);
589
+ return;
590
+ }
533
591
  next();
534
592
  });
535
593
  },
536
- hotUpdate({ file }) {
537
- if (file.endsWith(".ts")) {
538
- const modules = this.environment.moduleGraph.getModulesByFile(file);
539
- if (modules) {
540
- logger.debug(`🔃 Hot update: ${file}`);
541
- const cleanupInstances = /* @__PURE__ */ new Set();
542
- for (const mod of modules) {
543
- const allImporters = gatherAllImporters(mod);
544
- for (const impModule of allImporters) if (impModule.id) cleanupInstances.add(impModule.id);
545
- this.environment.moduleGraph.invalidateModule(mod);
546
- }
547
- moostMiddleware = null;
548
- if (localFetchTeardown) {
549
- localFetchTeardown();
550
- localFetchTeardown = null;
551
- }
552
- moostRestartCleanup(adapters, options.onEject, cleanupInstances);
553
- reloadRequired = true;
554
- }
555
- return [];
556
- }
594
+ async hotUpdate({ file, modules }) {
595
+ if (this.environment.name !== "ssr" || modules.length === 0) return;
596
+ const { moduleGraph } = this.environment;
597
+ const entryModule = await getEntryModule(moduleGraph);
598
+ const importerSets = modules.map((mod) => gatherAllImporters(mod));
599
+ if (entryModule && !importerSets.some((importers) => importers.has(entryModule))) return;
600
+ logger.debug(`🔃 Hot update: ${file}`);
601
+ const cleanupInstances = /* @__PURE__ */ new Set();
602
+ for (const importers of importerSets) for (const impModule of importers) if (impModule.id) cleanupInstances.add(impModule.id);
603
+ for (const mod of modules) moduleGraph.invalidateModule(mod);
604
+ if (entryModule) moduleGraph.invalidateModule(entryModule);
605
+ else moduleGraph.invalidateAll();
606
+ ejectApp(cleanupInstances);
607
+ return [];
557
608
  }
558
609
  }, options.middleware && options.ssrEntry ? {
559
610
  name: `${PLUGIN_NAME}:ssr-fallback`,
@@ -567,7 +618,7 @@ function moostVite(options) {
567
618
  const url = req.originalUrl || req.url || "/";
568
619
  try {
569
620
  await drainReload();
570
- let template = await fs.readFile(resolve(process.cwd(), "index.html"), "utf8");
621
+ let template = await fs.readFile(resolve(server.config.root, "index.html"), "utf8");
571
622
  template = await server.transformIndexHtml(url, template);
572
623
  const { render } = await server.ssrLoadModule(options.ssrEntry);
573
624
  const { html: appHtml, state } = await render(url);
@@ -5,6 +5,31 @@ let moost = require("moost");
5
5
  const PLUGIN_NAME = "moost-vite";
6
6
  const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
7
7
  const DEFAULT_SSR_STATE = "<!--ssr-state-->";
8
+ /**
9
+ * Normalizes the `prefix` option (single mount or list of mounts) into a list of
10
+ * normalized entries: leading slash ensured, trailing slash stripped.
11
+ * Returns `undefined` when no prefix is configured (empty input included) —
12
+ * the "route everything through Moost first" mode.
13
+ */ function normalizePrefixes(prefix) {
14
+ if (!prefix) return;
15
+ const normalized = (Array.isArray(prefix) ? prefix : [prefix]).filter(Boolean).map((entry) => {
16
+ let value = entry;
17
+ if (!value.startsWith("/")) value = `/${value}`;
18
+ if (value.endsWith("/")) value = value.slice(0, -1);
19
+ return value;
20
+ });
21
+ return normalized.length > 0 ? normalized : void 0;
22
+ }
23
+ /**
24
+ * Checks whether a request url falls under any of the normalized prefix mounts:
25
+ * exact match, a path segment below it, or the mount itself with a query string.
26
+ */ function matchesPrefix(url, prefixes) {
27
+ return prefixes.some((prefix) => {
28
+ if (!url.startsWith(prefix)) return false;
29
+ const next = url[prefix.length];
30
+ return next === void 0 || next === "/" || next === "?";
31
+ });
32
+ }
8
33
  const logger = (0, moost.createLogger)({ level: 99 }).createTopic("\x1B[2m\x1B[36m" + PLUGIN_NAME);
9
34
 
10
35
  //#endregion
@@ -33,7 +58,7 @@ async function createSSRServer(options) {
33
58
  if (req.method !== "GET") return next();
34
59
  const url = req.originalUrl || req.url || "/";
35
60
  try {
36
- let template = await fs.readFile(path.resolve(process.cwd(), "index.html"), "utf8");
61
+ let template = await fs.readFile(path.resolve(vite.config.root, "index.html"), "utf8");
37
62
  template = await vite.transformIndexHtml(url, template);
38
63
  const { render } = await vite.ssrLoadModule(ssrEntry);
39
64
  const { html: appHtml, state } = await render(url);
@@ -66,10 +91,7 @@ async function createSSRServer(options) {
66
91
  const clientDir = opts.clientDir || path.resolve(__dirname, "../client");
67
92
  const ssrOutlet = opts.ssrOutlet || (__MOOST_SSR_OUTLET__ !== void 0 ? __MOOST_SSR_OUTLET__ : "<!--ssr-outlet-->");
68
93
  const ssrState = opts.ssrState || (__MOOST_SSR_STATE__ !== void 0 ? __MOOST_SSR_STATE__ : "<!--ssr-state-->");
69
- let prefix = opts.prefix || __MOOST_PREFIX__;
70
- if (!prefix.startsWith("/")) prefix = `/${prefix}`;
71
- if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
72
- const prefixSlash = `${prefix}/`;
94
+ const prefixes = normalizePrefixes(opts.prefix ?? __MOOST_PREFIX__);
73
95
  const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
74
96
  const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf8");
75
97
  let render = null;
@@ -83,7 +105,7 @@ async function createSSRServer(options) {
83
105
  let captured = false;
84
106
  const origListen = MoostHttp.prototype.listen;
85
107
  MoostHttp.prototype.listen = function(...args) {
86
- moostHandler = this.getServerCb();
108
+ moostHandler = this.getServerCb((req, res) => serveStatic(req, res));
87
109
  moostHttpRef.instance = this;
88
110
  captured = true;
89
111
  setTimeout(() => args.filter((a) => typeof a === "function").forEach((a) => a()), 1);
@@ -108,6 +130,32 @@ async function createSSRServer(options) {
108
130
  if (enableLocalFetch && moostHttpRef.instance) enableLocalFetch(moostHttpRef.instance);
109
131
  const sirv = (await import("sirv")).default;
110
132
  const serve = sirv(path.resolve(clientDir), { extensions: [] });
133
+ const serveStatic = (req, res) => {
134
+ const url = req.url || "/";
135
+ serve(req, res, async () => {
136
+ if (req.method !== "GET") {
137
+ res.statusCode = 404;
138
+ res.end();
139
+ return;
140
+ }
141
+ try {
142
+ if (render) {
143
+ const { html: appHtml, state } = await render(url);
144
+ res.statusCode = 200;
145
+ res.setHeader("Content-Type", "text/html");
146
+ res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}<\/script>` : ""));
147
+ } else {
148
+ res.statusCode = 200;
149
+ res.setHeader("Content-Type", "text/html");
150
+ res.end(template);
151
+ }
152
+ } catch (error) {
153
+ console.error(error);
154
+ res.statusCode = 500;
155
+ res.end(error.message);
156
+ }
157
+ });
158
+ };
111
159
  return {
112
160
  use(mw) {
113
161
  userMiddlewares.push(mw);
@@ -122,33 +170,11 @@ async function createSSRServer(options) {
122
170
  userMiddlewares[i++](req, res, runNext);
123
171
  return;
124
172
  }
125
- if (moostHandler && (url.startsWith(prefixSlash) || url === prefix || url.startsWith(`${prefix}?`))) {
173
+ if (moostHandler && (!prefixes || matchesPrefix(url, prefixes))) {
126
174
  moostHandler(req, res);
127
175
  return;
128
176
  }
129
- serve(req, res, async () => {
130
- if (req.method !== "GET") {
131
- res.statusCode = 404;
132
- res.end();
133
- return;
134
- }
135
- try {
136
- if (render) {
137
- const { html: appHtml, state } = await render(url);
138
- res.statusCode = 200;
139
- res.setHeader("Content-Type", "text/html");
140
- res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}<\/script>` : ""));
141
- } else {
142
- res.statusCode = 200;
143
- res.setHeader("Content-Type", "text/html");
144
- res.end(template);
145
- }
146
- } catch (error) {
147
- console.error(error);
148
- res.statusCode = 500;
149
- res.end(error.message);
150
- }
151
- });
177
+ serveStatic(req, res);
152
178
  };
153
179
  runNext();
154
180
  }).listen(p, () => {
@@ -6,8 +6,13 @@ interface TSSRServerOptions {
6
6
  entry?: (() => Promise<unknown>) | string;
7
7
  /** Override: SSR entry module path (e.g., '/src/entry-server.ts') */
8
8
  ssrEntry?: string;
9
- /** Override: URL prefix for API routes (e.g., '/api') */
10
- prefix?: string;
9
+ /**
10
+ * Override: URL mount(s) for Moost routes (e.g., '/api' or ['/api', '/.well-known']).
11
+ * Requests outside every mount skip the Moost router (optimization/guard).
12
+ * When omitted (and not baked by the plugin), every request enters Moost first
13
+ * and unmatched routes fall through to static/SSR — same as dev.
14
+ */
15
+ prefix?: string | string[];
11
16
  /** Override: port number (default: process.env.PORT || 3000) */
12
17
  port?: number;
13
18
  /** Override: client build directory (default: 'dist/client') */
@@ -6,6 +6,31 @@ import { createLogger } from "moost";
6
6
  const PLUGIN_NAME = "moost-vite";
7
7
  const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
8
8
  const DEFAULT_SSR_STATE = "<!--ssr-state-->";
9
+ /**
10
+ * Normalizes the `prefix` option (single mount or list of mounts) into a list of
11
+ * normalized entries: leading slash ensured, trailing slash stripped.
12
+ * Returns `undefined` when no prefix is configured (empty input included) —
13
+ * the "route everything through Moost first" mode.
14
+ */ function normalizePrefixes(prefix) {
15
+ if (!prefix) return;
16
+ const normalized = (Array.isArray(prefix) ? prefix : [prefix]).filter(Boolean).map((entry) => {
17
+ let value = entry;
18
+ if (!value.startsWith("/")) value = `/${value}`;
19
+ if (value.endsWith("/")) value = value.slice(0, -1);
20
+ return value;
21
+ });
22
+ return normalized.length > 0 ? normalized : void 0;
23
+ }
24
+ /**
25
+ * Checks whether a request url falls under any of the normalized prefix mounts:
26
+ * exact match, a path segment below it, or the mount itself with a query string.
27
+ */ function matchesPrefix(url, prefixes) {
28
+ return prefixes.some((prefix) => {
29
+ if (!url.startsWith(prefix)) return false;
30
+ const next = url[prefix.length];
31
+ return next === void 0 || next === "/" || next === "?";
32
+ });
33
+ }
9
34
  const logger = createLogger({ level: 99 }).createTopic("\x1B[2m\x1B[36m" + PLUGIN_NAME);
10
35
 
11
36
  //#endregion
@@ -34,7 +59,7 @@ async function createSSRServer(options) {
34
59
  if (req.method !== "GET") return next();
35
60
  const url = req.originalUrl || req.url || "/";
36
61
  try {
37
- let template = await fs.readFile(path.resolve(process.cwd(), "index.html"), "utf8");
62
+ let template = await fs.readFile(path.resolve(vite.config.root, "index.html"), "utf8");
38
63
  template = await vite.transformIndexHtml(url, template);
39
64
  const { render } = await vite.ssrLoadModule(ssrEntry);
40
65
  const { html: appHtml, state } = await render(url);
@@ -67,10 +92,7 @@ async function createSSRServer(options) {
67
92
  const clientDir = opts.clientDir || path.resolve(import.meta.dirname, "../client");
68
93
  const ssrOutlet = opts.ssrOutlet || (__MOOST_SSR_OUTLET__ !== void 0 ? __MOOST_SSR_OUTLET__ : "<!--ssr-outlet-->");
69
94
  const ssrState = opts.ssrState || (__MOOST_SSR_STATE__ !== void 0 ? __MOOST_SSR_STATE__ : "<!--ssr-state-->");
70
- let prefix = opts.prefix || __MOOST_PREFIX__;
71
- if (!prefix.startsWith("/")) prefix = `/${prefix}`;
72
- if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
73
- const prefixSlash = `${prefix}/`;
95
+ const prefixes = normalizePrefixes(opts.prefix ?? __MOOST_PREFIX__);
74
96
  const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
75
97
  const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf8");
76
98
  let render = null;
@@ -84,7 +106,7 @@ async function createSSRServer(options) {
84
106
  let captured = false;
85
107
  const origListen = MoostHttp.prototype.listen;
86
108
  MoostHttp.prototype.listen = function(...args) {
87
- moostHandler = this.getServerCb();
109
+ moostHandler = this.getServerCb((req, res) => serveStatic(req, res));
88
110
  moostHttpRef.instance = this;
89
111
  captured = true;
90
112
  setTimeout(() => args.filter((a) => typeof a === "function").forEach((a) => a()), 1);
@@ -109,6 +131,32 @@ async function createSSRServer(options) {
109
131
  if (enableLocalFetch && moostHttpRef.instance) enableLocalFetch(moostHttpRef.instance);
110
132
  const sirv = (await import("sirv")).default;
111
133
  const serve = sirv(path.resolve(clientDir), { extensions: [] });
134
+ const serveStatic = (req, res) => {
135
+ const url = req.url || "/";
136
+ serve(req, res, async () => {
137
+ if (req.method !== "GET") {
138
+ res.statusCode = 404;
139
+ res.end();
140
+ return;
141
+ }
142
+ try {
143
+ if (render) {
144
+ const { html: appHtml, state } = await render(url);
145
+ res.statusCode = 200;
146
+ res.setHeader("Content-Type", "text/html");
147
+ res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}<\/script>` : ""));
148
+ } else {
149
+ res.statusCode = 200;
150
+ res.setHeader("Content-Type", "text/html");
151
+ res.end(template);
152
+ }
153
+ } catch (error) {
154
+ console.error(error);
155
+ res.statusCode = 500;
156
+ res.end(error.message);
157
+ }
158
+ });
159
+ };
112
160
  return {
113
161
  use(mw) {
114
162
  userMiddlewares.push(mw);
@@ -123,33 +171,11 @@ async function createSSRServer(options) {
123
171
  userMiddlewares[i++](req, res, runNext);
124
172
  return;
125
173
  }
126
- if (moostHandler && (url.startsWith(prefixSlash) || url === prefix || url.startsWith(`${prefix}?`))) {
174
+ if (moostHandler && (!prefixes || matchesPrefix(url, prefixes))) {
127
175
  moostHandler(req, res);
128
176
  return;
129
177
  }
130
- serve(req, res, async () => {
131
- if (req.method !== "GET") {
132
- res.statusCode = 404;
133
- res.end();
134
- return;
135
- }
136
- try {
137
- if (render) {
138
- const { html: appHtml, state } = await render(url);
139
- res.statusCode = 200;
140
- res.setHeader("Content-Type", "text/html");
141
- res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}<\/script>` : ""));
142
- } else {
143
- res.statusCode = 200;
144
- res.setHeader("Content-Type", "text/html");
145
- res.end(template);
146
- }
147
- } catch (error) {
148
- console.error(error);
149
- res.statusCode = 500;
150
- res.end(error.message);
151
- }
152
- });
178
+ serveStatic(req, res);
153
179
  };
154
180
  runNext();
155
181
  }).listen(p, () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moostjs/vite",
3
- "version": "0.6.25",
3
+ "version": "0.6.27",
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.25",
62
- "moost": "^0.6.25"
61
+ "@moostjs/event-http": "^0.6.27",
62
+ "moost": "^0.6.27"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "sirv": {