@moostjs/vite 0.6.2 → 0.6.4

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
@@ -2,13 +2,19 @@
2
2
 
3
3
  Vite dev plugin for [Moost](https://moost.org). Enables hot module replacement for Moost HTTP applications during development, automatic adapter detection, and production build configuration.
4
4
 
5
+ Supports two modes:
6
+ - **Backend mode** (default) — Moost owns the server, Vite provides HMR and TypeScript transforms
7
+ - **Middleware mode** — Vite serves the frontend (Vue, React, etc.), Moost handles API routes as middleware
8
+
5
9
  ## Installation
6
10
 
7
11
  ```bash
8
12
  npm install @moostjs/vite --save-dev
9
13
  ```
10
14
 
11
- ## Quick Start
15
+ ## Backend Mode (default)
16
+
17
+ For pure API servers where Moost handles all HTTP requests. Vite is used as a dev tool for HMR and decorator transforms.
12
18
 
13
19
  ```ts
14
20
  // vite.config.ts
@@ -19,23 +25,176 @@ export default defineConfig({
19
25
  plugins: [
20
26
  moostVite({
21
27
  entry: './src/main.ts',
22
- port: 3000,
23
28
  }),
24
29
  ],
25
30
  })
26
31
  ```
27
32
 
33
+ ```ts
34
+ // src/main.ts
35
+ import { Moost, Param } from 'moost'
36
+ import { MoostHttp, Get } from '@moostjs/event-http'
37
+
38
+ class App extends Moost {
39
+ @Get('hello/:name')
40
+ hello(@Param('name') name: string) {
41
+ return { message: `Hello ${name}!` }
42
+ }
43
+ }
44
+
45
+ const app = new App()
46
+ const http = new MoostHttp()
47
+ app.adapter(http).listen(3000)
48
+ app.init()
49
+ ```
50
+
51
+ Run with `vite dev`. The plugin patches `MoostHttp.listen()` so Moost doesn't bind a port — Vite's dev server handles HTTP instead. Controller changes trigger HMR with automatic DI cleanup.
52
+
53
+ ## Middleware Mode
54
+
55
+ For fullstack apps where Vite serves the frontend and Moost handles API routes. Unmatched requests fall through to Vite's default handler (static assets, Vue/React pages, HMR client).
56
+
57
+ ```ts
58
+ // vite.config.ts
59
+ import { defineConfig } from 'vite'
60
+ import vue from '@vitejs/plugin-vue'
61
+ import { moostVite } from '@moostjs/vite'
62
+
63
+ export default defineConfig({
64
+ plugins: [
65
+ vue(),
66
+ moostVite({
67
+ entry: './src/api/main.ts',
68
+ middleware: true,
69
+ }),
70
+ ],
71
+ })
72
+ ```
73
+
74
+ ```ts
75
+ // src/api/main.ts
76
+ import { Moost, Param } from 'moost'
77
+ import { MoostHttp, Get } from '@moostjs/event-http'
78
+
79
+ class ApiController extends Moost {
80
+ @Get('api/hello/:name')
81
+ hello(@Param('name') name: string) {
82
+ return { message: `Hello ${name}!` }
83
+ }
84
+ }
85
+
86
+ const app = new ApiController()
87
+ const http = new MoostHttp()
88
+ app.adapter(http).listen(3000)
89
+ app.init()
90
+ ```
91
+
92
+ Requests matching Moost routes (e.g. `/api/hello/world`) are handled by Moost with the full pipeline (interceptors, DI, validation). Everything else (`/`, `/about`, static assets) falls through to Vite.
93
+
94
+ The optional `prefix` option adds a fast-path check — requests not matching the prefix skip Moost entirely:
95
+
96
+ ```ts
97
+ moostVite({
98
+ entry: './src/api/main.ts',
99
+ middleware: true,
100
+ prefix: '/api',
101
+ })
102
+ ```
103
+
104
+ ## SSR Mode
105
+
106
+ Add `ssrEntry` to enable server-side rendering. The plugin handles everything automatically — SSR rendering in dev, multi-environment builds for production:
107
+
108
+ ```ts
109
+ moostVite({
110
+ entry: '/src/main.ts',
111
+ middleware: true,
112
+ prefix: '/api',
113
+ ssrEntry: '/src/entry-server.ts',
114
+ })
115
+ ```
116
+
117
+ In dev, the plugin adds an SSR fallback middleware that renders pages on the server using your `entry-server.ts`. In production, `vite build` produces three bundles in a single pass:
118
+
119
+ - **client** — optimized browser assets (`dist/client/`)
120
+ - **ssr** — server-side render function (`dist/server/ssr/`)
121
+ - **server** — production Node.js server (`dist/server/server.js`)
122
+
123
+ ### SPA Mode
124
+
125
+ Omit `ssrEntry` and Vite serves the app as a standard SPA. The production build still generates a server that serves static files and API routes — it just skips server-side rendering.
126
+
127
+ ### Custom Server Entry
128
+
129
+ By default, `vite build` auto-generates a minimal production server. If you need custom middleware (compression, auth, logging), provide your own server file:
130
+
131
+ ```ts
132
+ moostVite({
133
+ entry: '/src/main.ts',
134
+ middleware: true,
135
+ prefix: '/api',
136
+ ssrEntry: '/src/entry-server.ts',
137
+ serverEntry: './server.ts',
138
+ })
139
+ ```
140
+
141
+ ```ts
142
+ // server.ts
143
+ import { createSSRServer } from '@moostjs/vite/server'
144
+
145
+ const app = await createSSRServer()
146
+ // app.use(compression())
147
+ await app.listen()
148
+ ```
149
+
150
+ `createSSRServer` handles dev/prod branching — in dev it creates a Vite dev server, in prod it serves built assets with `sirv`.
151
+
152
+ ## SSR Local Fetch
153
+
154
+ When `ssrFetch` is enabled (default: `true`), the plugin patches `globalThis.fetch` so that local paths are routed in-process through Moost instead of making a real HTTP request. This is useful for SSR where server-side code fetches from its own API:
155
+
156
+ ```ts
157
+ // During SSR rendering:
158
+ const res = await fetch('/api/hello/world')
159
+ // → Routed in-process to Moost, no TCP round-trip
160
+ ```
161
+
162
+ If no Moost route matches, the call falls back to the original `fetch`. External URLs (e.g. `https://api.example.com`) always go through real HTTP.
163
+
164
+ Disable with `ssrFetch: false` when running behind Nitro or another framework that manages fetch routing itself.
165
+
166
+ ## Hot Module Replacement
167
+
168
+ Both modes support full HMR for Moost controllers:
169
+
170
+ 1. File change detected → Vite invalidates the module graph
171
+ 2. Moost DI container is cleaned up (stale instances ejected, dependants cascade)
172
+ 3. The SSR entry is re-imported → Moost re-initializes with updated controllers
173
+ 4. New requests use the updated code — no server restart needed
174
+
175
+ The plugin injects a `__VITE_ID` decorator on `@Injectable` and `@Controller` classes to track which file each class belongs to, enabling precise cleanup during hot reloads.
176
+
28
177
  ## Options
29
178
 
30
- | Option | Type | Default | Description |
31
- | ----------- | ------------------- | ------------- | --------------------------------- |
32
- | `entry` | `string` | — | Application entry file (required) |
33
- | `port` | `number` | `3000` | Dev server port |
34
- | `host` | `string` | `'localhost'` | Dev server host |
35
- | `outDir` | `string` | `'dist'` | Build output directory |
36
- | `format` | `'cjs' \| 'esm'` | `'esm'` | Output module format |
37
- | `sourcemap` | `boolean` | `true` | Generate source maps |
38
- | `externals` | `boolean \| object` | `true` | Configure external dependencies |
179
+ | Option | Type | Default | Description |
180
+ |---|---|---|---|
181
+ | `entry` | `string` | — | Application entry file (required) |
182
+ | `port` | `number` | `3000` | Dev server port (backend mode only) |
183
+ | `host` | `string` | `'localhost'` | Dev server host (backend mode only) |
184
+ | `outDir` | `string` | `'dist'` | Build output directory (backend mode only) |
185
+ | `format` | `'cjs' \| 'esm'` | `'esm'` | Output module format (backend mode only) |
186
+ | `sourcemap` | `boolean` | `true` | Generate source maps (backend mode only) |
187
+ | `externals` | `boolean \| object` | `true` | Configure external dependencies (backend mode only) |
188
+ | `onEject` | `function` | — | Hook to control DI instance ejection during HMR |
189
+ | `ssrFetch` | `boolean` | `true` | Enable local fetch interception for SSR |
190
+ | `middleware` | `boolean` | `false` | Run Moost as Connect middleware (Vite serves frontend) |
191
+ | `prefix` | `string` | — | URL prefix filter for middleware mode (e.g. `'/api'`) |
192
+ | `ssrEntry` | `string` | — | Vue/React SSR entry module (e.g. `'/src/entry-server.ts'`) |
193
+ | `ssrOutlet` | `string` | `'<!--ssr-outlet-->'` | HTML placeholder for SSR-rendered content |
194
+ | `ssrState` | `string` | `'<!--ssr-state-->'` | HTML placeholder for SSR state transfer script |
195
+ | `serverEntry` | `string` | — | Custom production server entry file (e.g. `'./server.ts'`) |
196
+
197
+ Options marked "backend mode only" are ignored when `middleware: true` — the user's `vite.config.ts` controls build/server configuration in middleware mode.
39
198
 
40
199
  ## License
41
200
 
package/dist/index.cjs CHANGED
@@ -21,13 +21,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  }) : target, mod));
22
22
 
23
23
  //#endregion
24
- const magic_string = __toESM(require("magic-string"));
25
24
  const node_fs = __toESM(require("node:fs"));
25
+ const node_path = __toESM(require("node:path"));
26
+ const vite = __toESM(require("vite"));
27
+ const magic_string = __toESM(require("magic-string"));
26
28
  const node_module = __toESM(require("node:module"));
27
29
  const moost = __toESM(require("moost"));
28
30
 
29
31
  //#region packages/vite/src/utils.ts
30
32
  const PLUGIN_NAME = "moost-vite";
33
+ const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
34
+ const DEFAULT_SSR_STATE = "<!--ssr-state-->";
35
+ function entryBasename(entry) {
36
+ return entry.split("/").pop().replace(/\.ts$/, ".js");
37
+ }
31
38
  /**
32
39
  * Recursively gathers all importer modules upstream from a given module.
33
40
  *
@@ -45,10 +52,14 @@ const logger = (0, moost.createLogger)({ level: 99 }).createTopic("\x1B[2m\x1B[3
45
52
  function getLogger() {
46
53
  return logger;
47
54
  }
55
+ function escapeRegex(str) {
56
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
57
+ }
48
58
  function getExternals({ node, workspace }) {
49
59
  const pkg = JSON.parse((0, node_fs.readFileSync)("package.json", "utf8").toString());
50
- const externals = workspace ? Object.keys(pkg.dependencies || {}) : Object.entries(pkg.dependencies || {}).filter(([key, ver]) => !ver.startsWith("workspace:")).map((i) => i[0]);
51
- if (node) externals.push(...node_module.builtinModules, ...node_module.builtinModules.map((m) => `node:${m}`));
60
+ const packageNames = workspace ? Object.keys(pkg.dependencies || {}) : Object.entries(pkg.dependencies || {}).filter(([key, ver]) => !ver.startsWith("workspace:")).map((i) => i[0]);
61
+ const externals = packageNames.map((name) => /* @__PURE__ */ new RegExp(`^${escapeRegex(name)}(/|$)`));
62
+ if (node) externals.push(...node_module.builtinModules.map((m) => /* @__PURE__ */ new RegExp(`^${escapeRegex(m)}(/|$)`)), ...node_module.builtinModules.map((m) => /* @__PURE__ */ new RegExp(`^node:${escapeRegex(m)}(/|$)`)));
52
63
  return externals;
53
64
  }
54
65
 
@@ -72,7 +83,7 @@ function getExternals({ node, workspace }) {
72
83
  const constructorName$1 = `Moost${adapter.charAt(0).toUpperCase() + adapter.slice(1)}`;
73
84
  getLogger().log(`🔍 Extracting Adapter "${constructorName$1}"`);
74
85
  this.constructor = module$1[constructorName$1];
75
- if (onInit && this.constructor) onInit(this.constructor);
86
+ if (onInit && this.constructor) onInit(this.constructor, module$1);
76
87
  },
77
88
  compare(c) {
78
89
  if (this.detected && this.constructor) return this.constructor === c || c instanceof this.constructor || c.prototype instanceof this.constructor;
@@ -311,16 +322,44 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
311
322
  *
312
323
  * @param {TMoostViteDevOptions} options - Configuration options for the Moost Vite plugin.
313
324
  * @returns {PluginOption} The configured Vite plugin.
314
- */ function moostVite(options) {
325
+ */ const DEFAULT_SERVER_ENTRY_CODE = `import { createSSRServer } from '@moostjs/vite/server'
326
+ const app = await createSSRServer()
327
+ await app.listen()
328
+ `;
329
+ function generatedServerEntry(root) {
330
+ const dir = (0, node_path.resolve)(root || process.cwd(), "node_modules", ".moost-vite");
331
+ (0, node_fs.mkdirSync)(dir, { recursive: true });
332
+ const entryPath = (0, node_path.resolve)(dir, "server-entry.mjs");
333
+ (0, node_fs.writeFileSync)(entryPath, DEFAULT_SERVER_ENTRY_CODE);
334
+ return entryPath;
335
+ }
336
+ function moostVite(options) {
315
337
  const isTest = process.env.NODE_ENV === "test";
316
338
  const isProd = process.env.NODE_ENV === "production";
317
339
  const externals = options.externals ?? true;
340
+ let prefix = options.prefix;
341
+ if (prefix) {
342
+ if (!prefix.startsWith("/")) prefix = `/${prefix}`;
343
+ if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
344
+ }
345
+ const prefixSlash = prefix ? prefix + "/" : void 0;
346
+ const prefixQuery = prefix ? prefix + "?" : void 0;
318
347
  let moostMiddleware = null;
348
+ let localFetchTeardown = null;
349
+ /** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
319
350
  const adapters = isTest ? [] : [
320
- createAdapterDetector("http", (MoostHttp) => {
351
+ createAdapterDetector("http", (MoostHttp, moduleExports) => {
321
352
  MoostHttp.prototype.listen = function(...args) {
322
353
  logger$1.log(`🔌 Overtaking HTTP.listen`);
323
- moostMiddleware = this.getServerCb();
354
+ if (options.middleware) moostMiddleware = this.getServerCb((req) => {
355
+ pendingNextMap.get(req)?.();
356
+ });
357
+ else moostMiddleware = this.getServerCb();
358
+ if (options.ssrFetch !== false && moduleExports?.enableLocalFetch) {
359
+ if (localFetchTeardown) localFetchTeardown();
360
+ localFetchTeardown = moduleExports.enableLocalFetch(this);
361
+ logger$1.log(`🔀 Local fetch enabled for SSR`);
362
+ }
324
363
  setTimeout(() => {
325
364
  args.filter((a) => typeof a === "function").forEach((a) => a());
326
365
  }, 1);
@@ -337,17 +376,69 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
337
376
  name: PLUGIN_NAME,
338
377
  enforce: "pre",
339
378
  config(cfg) {
379
+ if (options.middleware) {
380
+ const defaultExternals = getExternals({
381
+ node: true,
382
+ workspace: true
383
+ });
384
+ const serverDefines = {
385
+ "process.env.MOOST_DEFERRED_ENV": "\"production\"",
386
+ __MOOST_ENTRY__: JSON.stringify(options.entry),
387
+ __MOOST_PREFIX__: JSON.stringify(prefix || "/api")
388
+ };
389
+ const serverEntry = options.serverEntry || generatedServerEntry(cfg.root);
390
+ const ssrInput = { server: serverEntry };
391
+ if (options.ssrEntry) {
392
+ const ssrBasename = entryBasename(options.ssrEntry);
393
+ ssrInput[`ssr/${ssrBasename.replace(/\.js$/, "")}`] = options.ssrEntry;
394
+ serverDefines.__MOOST_SSR_ENTRY__ = JSON.stringify(`./ssr/${ssrBasename}`);
395
+ serverDefines.__MOOST_SSR_OUTLET__ = JSON.stringify(options.ssrOutlet || DEFAULT_SSR_OUTLET);
396
+ serverDefines.__MOOST_SSR_STATE__ = JSON.stringify(options.ssrState || DEFAULT_SSR_STATE);
397
+ }
398
+ const outDir = cfg.build?.outDir || "dist";
399
+ return {
400
+ ...options.ssrEntry && { appType: "custom" },
401
+ builder: { async buildApp(builder) {
402
+ (0, node_fs.rmSync)((0, node_path.resolve)(cfg.root || process.cwd(), outDir), {
403
+ recursive: true,
404
+ force: true
405
+ });
406
+ for (const env of Object.values(builder.environments)) await builder.build(env);
407
+ } },
408
+ environments: {
409
+ client: { build: {
410
+ outDir: `${outDir}/client`,
411
+ emptyOutDir: false,
412
+ ...options.ssrEntry && { ssrManifest: true }
413
+ } },
414
+ ssr: {
415
+ build: {
416
+ outDir: `${outDir}/server`,
417
+ emptyOutDir: false,
418
+ minify: false,
419
+ sourcemap: !!(options.sourcemap ?? true),
420
+ rollupOptions: {
421
+ input: ssrInput,
422
+ external: (id) => {
423
+ if (id === "@moostjs/vite/server") return false;
424
+ return defaultExternals.some((ext) => ext.test(id));
425
+ },
426
+ output: { format: "esm" }
427
+ }
428
+ },
429
+ define: serverDefines
430
+ }
431
+ }
432
+ };
433
+ }
340
434
  const entry = cfg.build?.rollupOptions?.input || options.entry;
341
- const outfile = typeof entry === "string" ? entry.split("/").pop().replace(/\.ts$/, ".js") : void 0;
435
+ const outfile = typeof entry === "string" ? entryBasename(entry) : void 0;
342
436
  return {
343
437
  server: {
344
438
  port: cfg.server?.port || options.port || 3e3,
345
439
  host: cfg.server?.host || options.host
346
440
  },
347
- optimizeDeps: {
348
- noDiscovery: cfg.optimizeDeps?.noDiscovery === void 0 ? true : cfg.optimizeDeps.noDiscovery,
349
- exclude: cfg.optimizeDeps?.exclude || ["@swc/core"]
350
- },
441
+ optimizeDeps: { noDiscovery: cfg.optimizeDeps?.noDiscovery === void 0 ? true : cfg.optimizeDeps.noDiscovery },
351
442
  build: {
352
443
  target: cfg.build?.target || "node",
353
444
  outDir: cfg.build?.outDir || options.outDir || "dist",
@@ -369,6 +460,17 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
369
460
  }
370
461
  };
371
462
  },
463
+ configResolved(config) {
464
+ config.__moostViteOptions = {
465
+ entry: options.entry,
466
+ ssrEntry: options.ssrEntry,
467
+ serverEntry: options.serverEntry,
468
+ prefix,
469
+ port: options.port,
470
+ ssrOutlet: options.ssrOutlet,
471
+ ssrState: options.ssrState
472
+ };
473
+ },
372
474
  async transform(code, id) {
373
475
  if (!id.endsWith(".ts")) return null;
374
476
  for (const adapter of adapters) if (!adapter.detected && adapter.regex.test(code)) await adapter.init();
@@ -399,19 +501,32 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
399
501
  };
400
502
  },
401
503
  async configureServer(server) {
402
- for (const adapter of adapters) adapter.ssrLoadModule = (id) => server.ssrLoadModule(id);
504
+ const runner = (0, vite.createServerModuleRunner)(server.environments.ssr);
505
+ const ssrImport = (id) => runner.import(id);
506
+ for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
403
507
  moostRestartCleanup(adapters, options.onEject);
404
- await server.ssrLoadModule(options.entry);
508
+ await ssrImport(options.entry);
405
509
  server.middlewares.use(async (req, res, next) => {
406
510
  if (reloadRequired) {
407
511
  reloadRequired = false;
408
512
  console.log();
409
513
  logger$1.debug("🚀 Reloading Moost App...");
410
514
  console.log();
411
- await server.ssrLoadModule(options.entry);
412
- await new Promise((resolve) => setTimeout(resolve, 1));
515
+ await ssrImport(options.entry);
516
+ await new Promise((resolve$1) => setTimeout(resolve$1, 1));
517
+ }
518
+ if (options.middleware && prefix) {
519
+ const url = req.url || "";
520
+ if (url !== prefix && !url.startsWith(prefixSlash) && !url.startsWith(prefixQuery)) return next();
521
+ }
522
+ if (moostMiddleware) {
523
+ if (options.middleware) {
524
+ pendingNextMap.set(req, next);
525
+ moostMiddleware(req, res);
526
+ return;
527
+ }
528
+ return moostMiddleware(req, res);
413
529
  }
414
- if (moostMiddleware) return moostMiddleware(req, res);
415
530
  next();
416
531
  });
417
532
  },
@@ -427,6 +542,10 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
427
542
  this.environment.moduleGraph.invalidateModule(mod);
428
543
  }
429
544
  moostMiddleware = null;
545
+ if (localFetchTeardown) {
546
+ localFetchTeardown();
547
+ localFetchTeardown = null;
548
+ }
430
549
  moostRestartCleanup(adapters, options.onEject, cleanupInstances);
431
550
  reloadRequired = true;
432
551
  }
@@ -441,7 +560,35 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
441
560
  delete pluginConfig.transform;
442
561
  delete pluginConfig.hotUpdate;
443
562
  }
444
- return pluginConfig;
563
+ const ssrFallbackPlugin = !isProd && !isTest && options.middleware && options.ssrEntry ? {
564
+ name: `${PLUGIN_NAME}:ssr-fallback`,
565
+ async configureServer(server) {
566
+ const ssrOutlet = options.ssrOutlet || DEFAULT_SSR_OUTLET;
567
+ const ssrState = options.ssrState || DEFAULT_SSR_STATE;
568
+ const fs = await import("node:fs/promises");
569
+ return () => {
570
+ server.middlewares.use(async (req, res, next) => {
571
+ if (req.method !== "GET") return next();
572
+ const url = req.originalUrl || req.url || "/";
573
+ try {
574
+ let template = await fs.readFile((0, node_path.resolve)(process.cwd(), "index.html"), "utf-8");
575
+ template = await server.transformIndexHtml(url, template);
576
+ const { render } = await server.ssrLoadModule(options.ssrEntry);
577
+ const { html: appHtml, state } = await render(url);
578
+ res.statusCode = 200;
579
+ res.setHeader("Content-Type", "text/html");
580
+ res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
581
+ } catch (e) {
582
+ server.ssrFixStacktrace(e);
583
+ console.error(e);
584
+ res.statusCode = 500;
585
+ res.end(e.message);
586
+ }
587
+ });
588
+ };
589
+ }
590
+ } : null;
591
+ return [pluginConfig, ssrFallbackPlugin];
445
592
  }
446
593
 
447
594
  //#endregion
package/dist/index.d.ts CHANGED
@@ -65,26 +65,62 @@ interface TMoostViteDevOptions {
65
65
  workspace?: boolean;
66
66
  } | boolean;
67
67
  onEject?: (instance: object, dependency: Function) => boolean;
68
+ /**
69
+ * Whether to enable local fetch interception for SSR.
70
+ * When enabled, `fetch('/path')` calls are routed to the Moost HTTP adapter
71
+ * in-process instead of making a network request.
72
+ *
73
+ * Set to `false` when running behind Nitro or another framework that
74
+ * manages fetch routing itself.
75
+ *
76
+ * Default: `true`.
77
+ */
78
+ ssrFetch?: boolean;
79
+ /**
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).
84
+ *
85
+ * Use this for fullstack apps where Vite serves the frontend (Vue, React, etc.)
86
+ * and Moost handles API routes.
87
+ *
88
+ * Default: `false`.
89
+ */
90
+ middleware?: boolean;
91
+ /**
92
+ * URL prefix for Moost API routes in middleware mode.
93
+ * Only requests starting with this prefix are dispatched to Moost.
94
+ * Ignored when `middleware` is `false`.
95
+ *
96
+ * Example: `'/api'`
97
+ */
98
+ prefix?: string;
99
+ /**
100
+ * Vue/React SSR entry module path. Used by the SSR server helper
101
+ * (`@moostjs/vite/server`) for server-side rendering.
102
+ *
103
+ * Example: `'/src/entry-server.ts'`
104
+ */
105
+ ssrEntry?: string;
106
+ /**
107
+ * HTML placeholder for SSR-rendered content.
108
+ * Default: `'<!--ssr-outlet-->'`
109
+ */
110
+ ssrOutlet?: string;
111
+ /**
112
+ * HTML placeholder for SSR state transfer script.
113
+ * Default: `'<!--ssr-state-->'`
114
+ */
115
+ ssrState?: string;
116
+ /**
117
+ * Path to a custom server entry file (e.g., `'./server.ts'`).
118
+ * When provided, this file is used as the production server build entry.
119
+ * When omitted in middleware mode, the plugin auto-generates a minimal
120
+ * production server that serves static files + Moost API (+ SSR if configured).
121
+ */
122
+ serverEntry?: string;
68
123
  }
69
- /**
70
- * The main Vite plugin for integrating Moost applications.
71
- *
72
- * Features:
73
- * - **Adapter Detection**: Detects Moost adapter usage (`http`, `cli`, `wf`) and applies relevant configurations.
74
- * - **Dev Mode Middleware**:
75
- * - Patches `MoostHttp.prototype.listen` to register a custom middleware for serving the app via Vite's dev server instead of binding to a port.
76
- * - Handles Moost state cleanup and hot module replacement (HMR) during development.
77
- * - **Class Tracking**: Injects a `__VITE_ID()` decorator into exported classes to enable tracking and cleanup during hot reloads.
78
- * - **Externals Support**:
79
- * - Allows marking Node.js built-in modules and dependencies from `package.json` (optionally excluding workspace dependencies) as external during builds.
80
- * - Configured via the `externals` option, which helps reduce bundle size and ensures compatibility with runtime environments.
81
- * - **Build and Test Friendly**:
82
- * - Avoids interfering with Vitest runs by skipping dev-specific behaviors during tests.
83
- * - Ensures proper SSR entry setup and Rollup configurations for production builds.
84
- *
85
- * @param {TMoostViteDevOptions} options - Configuration options for the Moost Vite plugin.
86
- * @returns {PluginOption} The configured Vite plugin.
87
- */
88
124
  declare function moostVite(options: TMoostViteDevOptions): PluginOption;
89
125
 
90
126
  export { moostVite };
package/dist/index.mjs CHANGED
@@ -1,10 +1,17 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { createServerModuleRunner } from "vite";
1
4
  import MagicString from "magic-string";
2
- import { readFileSync } from "node:fs";
3
5
  import { builtinModules } from "node:module";
4
6
  import { Moost, clearGlobalWooks, createLogger, getMoostInfact, getMoostMate } from "moost";
5
7
 
6
8
  //#region packages/vite/src/utils.ts
7
9
  const PLUGIN_NAME = "moost-vite";
10
+ const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
11
+ const DEFAULT_SSR_STATE = "<!--ssr-state-->";
12
+ function entryBasename(entry) {
13
+ return entry.split("/").pop().replace(/\.ts$/, ".js");
14
+ }
8
15
  /**
9
16
  * Recursively gathers all importer modules upstream from a given module.
10
17
  *
@@ -22,10 +29,14 @@ const logger = createLogger({ level: 99 }).createTopic("\x1B[2m\x1B[36m" + PLUGI
22
29
  function getLogger() {
23
30
  return logger;
24
31
  }
32
+ function escapeRegex(str) {
33
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34
+ }
25
35
  function getExternals({ node, workspace }) {
26
36
  const pkg = JSON.parse(readFileSync("package.json", "utf8").toString());
27
- const externals = workspace ? Object.keys(pkg.dependencies || {}) : Object.entries(pkg.dependencies || {}).filter(([key, ver]) => !ver.startsWith("workspace:")).map((i) => i[0]);
28
- if (node) externals.push(...builtinModules, ...builtinModules.map((m) => `node:${m}`));
37
+ const packageNames = workspace ? Object.keys(pkg.dependencies || {}) : Object.entries(pkg.dependencies || {}).filter(([key, ver]) => !ver.startsWith("workspace:")).map((i) => i[0]);
38
+ const externals = packageNames.map((name) => /* @__PURE__ */ new RegExp(`^${escapeRegex(name)}(/|$)`));
39
+ if (node) externals.push(...builtinModules.map((m) => /* @__PURE__ */ new RegExp(`^${escapeRegex(m)}(/|$)`)), ...builtinModules.map((m) => /* @__PURE__ */ new RegExp(`^node:${escapeRegex(m)}(/|$)`)));
29
40
  return externals;
30
41
  }
31
42
 
@@ -49,7 +60,7 @@ function getExternals({ node, workspace }) {
49
60
  const constructorName$1 = `Moost${adapter.charAt(0).toUpperCase() + adapter.slice(1)}`;
50
61
  getLogger().log(`🔍 Extracting Adapter "${constructorName$1}"`);
51
62
  this.constructor = module[constructorName$1];
52
- if (onInit && this.constructor) onInit(this.constructor);
63
+ if (onInit && this.constructor) onInit(this.constructor, module);
53
64
  },
54
65
  compare(c) {
55
66
  if (this.detected && this.constructor) return this.constructor === c || c instanceof this.constructor || c.prototype instanceof this.constructor;
@@ -288,16 +299,44 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
288
299
  *
289
300
  * @param {TMoostViteDevOptions} options - Configuration options for the Moost Vite plugin.
290
301
  * @returns {PluginOption} The configured Vite plugin.
291
- */ function moostVite(options) {
302
+ */ const DEFAULT_SERVER_ENTRY_CODE = `import { createSSRServer } from '@moostjs/vite/server'
303
+ const app = await createSSRServer()
304
+ await app.listen()
305
+ `;
306
+ function generatedServerEntry(root) {
307
+ const dir = resolve(root || process.cwd(), "node_modules", ".moost-vite");
308
+ mkdirSync(dir, { recursive: true });
309
+ const entryPath = resolve(dir, "server-entry.mjs");
310
+ writeFileSync(entryPath, DEFAULT_SERVER_ENTRY_CODE);
311
+ return entryPath;
312
+ }
313
+ function moostVite(options) {
292
314
  const isTest = false;
293
315
  const isProd = false;
294
316
  const externals = options.externals ?? true;
317
+ let prefix = options.prefix;
318
+ if (prefix) {
319
+ if (!prefix.startsWith("/")) prefix = `/${prefix}`;
320
+ if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
321
+ }
322
+ const prefixSlash = prefix ? prefix + "/" : void 0;
323
+ const prefixQuery = prefix ? prefix + "?" : void 0;
295
324
  let moostMiddleware = null;
325
+ let localFetchTeardown = null;
326
+ /** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
296
327
  const adapters = isTest ? [] : [
297
- createAdapterDetector("http", (MoostHttp) => {
328
+ createAdapterDetector("http", (MoostHttp, moduleExports) => {
298
329
  MoostHttp.prototype.listen = function(...args) {
299
330
  logger$1.log(`🔌 Overtaking HTTP.listen`);
300
- moostMiddleware = this.getServerCb();
331
+ if (options.middleware) moostMiddleware = this.getServerCb((req) => {
332
+ pendingNextMap.get(req)?.();
333
+ });
334
+ else moostMiddleware = this.getServerCb();
335
+ if (options.ssrFetch !== false && moduleExports?.enableLocalFetch) {
336
+ if (localFetchTeardown) localFetchTeardown();
337
+ localFetchTeardown = moduleExports.enableLocalFetch(this);
338
+ logger$1.log(`🔀 Local fetch enabled for SSR`);
339
+ }
301
340
  setTimeout(() => {
302
341
  args.filter((a) => typeof a === "function").forEach((a) => a());
303
342
  }, 1);
@@ -314,17 +353,69 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
314
353
  name: PLUGIN_NAME,
315
354
  enforce: "pre",
316
355
  config(cfg) {
356
+ if (options.middleware) {
357
+ const defaultExternals = getExternals({
358
+ node: true,
359
+ workspace: true
360
+ });
361
+ const serverDefines = {
362
+ "process.env.MOOST_DEFERRED_ENV": "\"production\"",
363
+ __MOOST_ENTRY__: JSON.stringify(options.entry),
364
+ __MOOST_PREFIX__: JSON.stringify(prefix || "/api")
365
+ };
366
+ const serverEntry = options.serverEntry || generatedServerEntry(cfg.root);
367
+ const ssrInput = { server: serverEntry };
368
+ if (options.ssrEntry) {
369
+ const ssrBasename = entryBasename(options.ssrEntry);
370
+ ssrInput[`ssr/${ssrBasename.replace(/\.js$/, "")}`] = options.ssrEntry;
371
+ serverDefines.__MOOST_SSR_ENTRY__ = JSON.stringify(`./ssr/${ssrBasename}`);
372
+ serverDefines.__MOOST_SSR_OUTLET__ = JSON.stringify(options.ssrOutlet || DEFAULT_SSR_OUTLET);
373
+ serverDefines.__MOOST_SSR_STATE__ = JSON.stringify(options.ssrState || DEFAULT_SSR_STATE);
374
+ }
375
+ const outDir = cfg.build?.outDir || "dist";
376
+ return {
377
+ ...options.ssrEntry && { appType: "custom" },
378
+ builder: { async buildApp(builder) {
379
+ rmSync(resolve(cfg.root || process.cwd(), outDir), {
380
+ recursive: true,
381
+ force: true
382
+ });
383
+ for (const env of Object.values(builder.environments)) await builder.build(env);
384
+ } },
385
+ environments: {
386
+ client: { build: {
387
+ outDir: `${outDir}/client`,
388
+ emptyOutDir: false,
389
+ ...options.ssrEntry && { ssrManifest: true }
390
+ } },
391
+ ssr: {
392
+ build: {
393
+ outDir: `${outDir}/server`,
394
+ emptyOutDir: false,
395
+ minify: false,
396
+ sourcemap: !!(options.sourcemap ?? true),
397
+ rollupOptions: {
398
+ input: ssrInput,
399
+ external: (id) => {
400
+ if (id === "@moostjs/vite/server") return false;
401
+ return defaultExternals.some((ext) => ext.test(id));
402
+ },
403
+ output: { format: "esm" }
404
+ }
405
+ },
406
+ define: serverDefines
407
+ }
408
+ }
409
+ };
410
+ }
317
411
  const entry = cfg.build?.rollupOptions?.input || options.entry;
318
- const outfile = typeof entry === "string" ? entry.split("/").pop().replace(/\.ts$/, ".js") : void 0;
412
+ const outfile = typeof entry === "string" ? entryBasename(entry) : void 0;
319
413
  return {
320
414
  server: {
321
415
  port: cfg.server?.port || options.port || 3e3,
322
416
  host: cfg.server?.host || options.host
323
417
  },
324
- optimizeDeps: {
325
- noDiscovery: cfg.optimizeDeps?.noDiscovery === void 0 ? true : cfg.optimizeDeps.noDiscovery,
326
- exclude: cfg.optimizeDeps?.exclude || ["@swc/core"]
327
- },
418
+ optimizeDeps: { noDiscovery: cfg.optimizeDeps?.noDiscovery === void 0 ? true : cfg.optimizeDeps.noDiscovery },
328
419
  build: {
329
420
  target: cfg.build?.target || "node",
330
421
  outDir: cfg.build?.outDir || options.outDir || "dist",
@@ -346,6 +437,17 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
346
437
  }
347
438
  };
348
439
  },
440
+ configResolved(config) {
441
+ config.__moostViteOptions = {
442
+ entry: options.entry,
443
+ ssrEntry: options.ssrEntry,
444
+ serverEntry: options.serverEntry,
445
+ prefix,
446
+ port: options.port,
447
+ ssrOutlet: options.ssrOutlet,
448
+ ssrState: options.ssrState
449
+ };
450
+ },
349
451
  async transform(code, id) {
350
452
  if (!id.endsWith(".ts")) return null;
351
453
  for (const adapter of adapters) if (!adapter.detected && adapter.regex.test(code)) await adapter.init();
@@ -376,19 +478,32 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
376
478
  };
377
479
  },
378
480
  async configureServer(server) {
379
- for (const adapter of adapters) adapter.ssrLoadModule = (id) => server.ssrLoadModule(id);
481
+ const runner = createServerModuleRunner(server.environments.ssr);
482
+ const ssrImport = (id) => runner.import(id);
483
+ for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
380
484
  moostRestartCleanup(adapters, options.onEject);
381
- await server.ssrLoadModule(options.entry);
485
+ await ssrImport(options.entry);
382
486
  server.middlewares.use(async (req, res, next) => {
383
487
  if (reloadRequired) {
384
488
  reloadRequired = false;
385
489
  console.log();
386
490
  logger$1.debug("🚀 Reloading Moost App...");
387
491
  console.log();
388
- await server.ssrLoadModule(options.entry);
389
- await new Promise((resolve) => setTimeout(resolve, 1));
492
+ await ssrImport(options.entry);
493
+ await new Promise((resolve$1) => setTimeout(resolve$1, 1));
494
+ }
495
+ if (options.middleware && prefix) {
496
+ const url = req.url || "";
497
+ if (url !== prefix && !url.startsWith(prefixSlash) && !url.startsWith(prefixQuery)) return next();
498
+ }
499
+ if (moostMiddleware) {
500
+ if (options.middleware) {
501
+ pendingNextMap.set(req, next);
502
+ moostMiddleware(req, res);
503
+ return;
504
+ }
505
+ return moostMiddleware(req, res);
390
506
  }
391
- if (moostMiddleware) return moostMiddleware(req, res);
392
507
  next();
393
508
  });
394
509
  },
@@ -404,6 +519,10 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
404
519
  this.environment.moduleGraph.invalidateModule(mod);
405
520
  }
406
521
  moostMiddleware = null;
522
+ if (localFetchTeardown) {
523
+ localFetchTeardown();
524
+ localFetchTeardown = null;
525
+ }
407
526
  moostRestartCleanup(adapters, options.onEject, cleanupInstances);
408
527
  reloadRequired = true;
409
528
  }
@@ -418,7 +537,35 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
418
537
  delete pluginConfig.transform;
419
538
  delete pluginConfig.hotUpdate;
420
539
  }
421
- return pluginConfig;
540
+ const ssrFallbackPlugin = !isProd && !isTest && options.middleware && options.ssrEntry ? {
541
+ name: `${PLUGIN_NAME}:ssr-fallback`,
542
+ async configureServer(server) {
543
+ const ssrOutlet = options.ssrOutlet || DEFAULT_SSR_OUTLET;
544
+ const ssrState = options.ssrState || DEFAULT_SSR_STATE;
545
+ const fs = await import("node:fs/promises");
546
+ return () => {
547
+ server.middlewares.use(async (req, res, next) => {
548
+ if (req.method !== "GET") return next();
549
+ const url = req.originalUrl || req.url || "/";
550
+ try {
551
+ let template = await fs.readFile(resolve(process.cwd(), "index.html"), "utf-8");
552
+ template = await server.transformIndexHtml(url, template);
553
+ const { render } = await server.ssrLoadModule(options.ssrEntry);
554
+ const { html: appHtml, state } = await render(url);
555
+ res.statusCode = 200;
556
+ res.setHeader("Content-Type", "text/html");
557
+ res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
558
+ } catch (e) {
559
+ server.ssrFixStacktrace(e);
560
+ console.error(e);
561
+ res.statusCode = 500;
562
+ res.end(e.message);
563
+ }
564
+ });
565
+ };
566
+ }
567
+ } : null;
568
+ return [pluginConfig, ssrFallbackPlugin];
422
569
  }
423
570
 
424
571
  //#endregion
@@ -0,0 +1,179 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ require("node:fs");
25
+ require("node:module");
26
+ const moost = __toESM(require("moost"));
27
+
28
+ //#region packages/vite/src/utils.ts
29
+ const PLUGIN_NAME = "moost-vite";
30
+ const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
31
+ const DEFAULT_SSR_STATE = "<!--ssr-state-->";
32
+ const logger = (0, moost.createLogger)({ level: 99 }).createTopic("\x1B[2m\x1B[36m" + PLUGIN_NAME);
33
+
34
+ //#endregion
35
+ //#region packages/vite/src/prod-server.ts
36
+ function stripUndefined(obj) {
37
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
38
+ }
39
+ async function createSSRServer(options) {
40
+ const userMiddlewares = [];
41
+ const opts = options ? stripUndefined(options) : {};
42
+ const isProd = process.env.MOOST_DEFERRED_ENV === "production";
43
+ if (!isProd) {
44
+ const { createServer: createViteServer } = await import("vite");
45
+ const vite = await createViteServer();
46
+ const pluginOpts = vite.config.__moostViteOptions || {};
47
+ const config = {
48
+ ...pluginOpts,
49
+ ...opts
50
+ };
51
+ const ssrEntry = config.ssrEntry;
52
+ const ssrOutlet$1 = config.ssrOutlet || DEFAULT_SSR_OUTLET;
53
+ const ssrState$1 = config.ssrState || DEFAULT_SSR_STATE;
54
+ let ssrFallback = null;
55
+ if (ssrEntry) {
56
+ const fs$1 = await import("node:fs/promises");
57
+ const path$1 = await import("node:path");
58
+ ssrFallback = async (req, res, next) => {
59
+ if (req.method !== "GET") return next();
60
+ const url = req.originalUrl || req.url || "/";
61
+ try {
62
+ let template$1 = await fs$1.readFile(path$1.resolve(process.cwd(), "index.html"), "utf-8");
63
+ template$1 = await vite.transformIndexHtml(url, template$1);
64
+ const { render: render$1 } = await vite.ssrLoadModule(ssrEntry);
65
+ const { html: appHtml, state } = await render$1(url);
66
+ res.statusCode = 200;
67
+ res.setHeader("Content-Type", "text/html");
68
+ res.end(template$1.replace(ssrOutlet$1, appHtml).replace(ssrState$1, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
69
+ } catch (e) {
70
+ vite.ssrFixStacktrace(e);
71
+ console.error(e);
72
+ res.statusCode = 500;
73
+ res.end(e.message);
74
+ }
75
+ };
76
+ }
77
+ return {
78
+ use(mw) {
79
+ userMiddlewares.push(mw);
80
+ },
81
+ async listen(_port) {
82
+ for (const mw of userMiddlewares) vite.middlewares.use(mw);
83
+ if (ssrFallback) vite.middlewares.use(ssrFallback);
84
+ await vite.listen();
85
+ vite.printUrls();
86
+ }
87
+ };
88
+ }
89
+ const fs = await import("node:fs/promises");
90
+ const path = await import("node:path");
91
+ const { createServer: createHttpServer } = await import("node:http");
92
+ const clientDir = opts.clientDir || path.resolve(__dirname, "../client");
93
+ const ssrOutlet = opts.ssrOutlet || (typeof __MOOST_SSR_OUTLET__ !== "undefined" ? __MOOST_SSR_OUTLET__ : "<!--ssr-outlet-->");
94
+ const ssrState = opts.ssrState || (typeof __MOOST_SSR_STATE__ !== "undefined" ? __MOOST_SSR_STATE__ : "<!--ssr-state-->");
95
+ let prefix = opts.prefix || __MOOST_PREFIX__;
96
+ if (!prefix.startsWith("/")) prefix = `/${prefix}`;
97
+ if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
98
+ const prefixSlash = prefix + "/";
99
+ const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
100
+ const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf-8");
101
+ let render = null;
102
+ const hasSsr = typeof __MOOST_SSR_ENTRY__ !== "undefined" && !!__MOOST_SSR_ENTRY__;
103
+ if (hasSsr) {
104
+ const ssrModule = await import(
105
+ /* @vite-ignore */
106
+ __MOOST_SSR_ENTRY__);
107
+ render = ssrModule.render;
108
+ }
109
+ const { MoostHttp, enableLocalFetch } = await import("@moostjs/event-http");
110
+ let moostHandler = null;
111
+ let moostHttpInstance = null;
112
+ const origListen = MoostHttp.prototype.listen;
113
+ MoostHttp.prototype.listen = function(...args) {
114
+ moostHandler = this.getServerCb();
115
+ moostHttpInstance = this;
116
+ setTimeout(() => args.filter((a) => typeof a === "function").forEach((a) => a()), 1);
117
+ return Promise.resolve();
118
+ };
119
+ if (typeof opts.entry === "function") await opts.entry();
120
+ else if (typeof opts.entry === "string") await import(
121
+ /* @vite-ignore */
122
+ opts.entry);
123
+ else await import(__MOOST_ENTRY__);
124
+ MoostHttp.prototype.listen = origListen;
125
+ if (enableLocalFetch && moostHttpInstance) enableLocalFetch(moostHttpInstance);
126
+ const sirv = (await import("sirv")).default;
127
+ const serve = sirv(path.resolve(clientDir), { extensions: [] });
128
+ return {
129
+ use(mw) {
130
+ userMiddlewares.push(mw);
131
+ },
132
+ async listen(port) {
133
+ const p = port || defaultPort;
134
+ createHttpServer((req, res) => {
135
+ const url = req.url || "/";
136
+ let i = 0;
137
+ const runNext = () => {
138
+ if (i < userMiddlewares.length) {
139
+ userMiddlewares[i++](req, res, runNext);
140
+ return;
141
+ }
142
+ if (moostHandler && (url.startsWith(prefixSlash) || url === prefix || url.startsWith(prefix + "?"))) {
143
+ moostHandler(req, res);
144
+ return;
145
+ }
146
+ serve(req, res, async () => {
147
+ if (req.method !== "GET") {
148
+ res.statusCode = 404;
149
+ res.end();
150
+ return;
151
+ }
152
+ try {
153
+ if (render) {
154
+ const { html: appHtml, state } = await render(url);
155
+ res.statusCode = 200;
156
+ res.setHeader("Content-Type", "text/html");
157
+ res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
158
+ } else {
159
+ res.statusCode = 200;
160
+ res.setHeader("Content-Type", "text/html");
161
+ res.end(template);
162
+ }
163
+ } catch (e) {
164
+ console.error(e);
165
+ res.statusCode = 500;
166
+ res.end(e.message);
167
+ }
168
+ });
169
+ };
170
+ runNext();
171
+ }).listen(p, () => {
172
+ console.log(`Server running at http://localhost:${p}`);
173
+ });
174
+ }
175
+ };
176
+ }
177
+
178
+ //#endregion
179
+ exports.createSSRServer = createSSRServer;
@@ -0,0 +1,29 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+
3
+ type TMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
4
+ interface TSSRServerOptions {
5
+ /** Override: lazy import for the Moost app entry. */
6
+ entry?: (() => Promise<unknown>) | string;
7
+ /** Override: SSR entry module path (e.g., '/src/entry-server.ts') */
8
+ ssrEntry?: string;
9
+ /** Override: URL prefix for API routes (e.g., '/api') */
10
+ prefix?: string;
11
+ /** Override: port number (default: process.env.PORT || 3000) */
12
+ port?: number;
13
+ /** Override: client build directory (default: 'dist/client') */
14
+ clientDir?: string;
15
+ /** Override: HTML placeholder for SSR content */
16
+ ssrOutlet?: string;
17
+ /** Override: HTML placeholder for SSR state */
18
+ ssrState?: string;
19
+ }
20
+ interface TSSRServer {
21
+ /** Add Connect-compatible middleware (runs in both dev and prod) */
22
+ use(middleware: TMiddleware): void;
23
+ /** Start the server */
24
+ listen(port?: number): Promise<void>;
25
+ }
26
+ declare function createSSRServer(options?: TSSRServerOptions): Promise<TSSRServer>;
27
+
28
+ export { createSSRServer };
29
+ export type { TSSRServer, TSSRServerOptions };
@@ -0,0 +1,156 @@
1
+ import "node:fs";
2
+ import "node:module";
3
+ import { createLogger } from "moost";
4
+
5
+ //#region packages/vite/src/utils.ts
6
+ const PLUGIN_NAME = "moost-vite";
7
+ const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
8
+ const DEFAULT_SSR_STATE = "<!--ssr-state-->";
9
+ const logger = createLogger({ level: 99 }).createTopic("\x1B[2m\x1B[36m" + PLUGIN_NAME);
10
+
11
+ //#endregion
12
+ //#region packages/vite/src/prod-server.ts
13
+ function stripUndefined(obj) {
14
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
15
+ }
16
+ async function createSSRServer(options) {
17
+ const userMiddlewares = [];
18
+ const opts = options ? stripUndefined(options) : {};
19
+ const isProd = process.env.MOOST_DEFERRED_ENV === "production";
20
+ if (!isProd) {
21
+ const { createServer: createViteServer } = await import("vite");
22
+ const vite = await createViteServer();
23
+ const pluginOpts = vite.config.__moostViteOptions || {};
24
+ const config = {
25
+ ...pluginOpts,
26
+ ...opts
27
+ };
28
+ const ssrEntry = config.ssrEntry;
29
+ const ssrOutlet$1 = config.ssrOutlet || DEFAULT_SSR_OUTLET;
30
+ const ssrState$1 = config.ssrState || DEFAULT_SSR_STATE;
31
+ let ssrFallback = null;
32
+ if (ssrEntry) {
33
+ const fs$1 = await import("node:fs/promises");
34
+ const path$1 = await import("node:path");
35
+ ssrFallback = async (req, res, next) => {
36
+ if (req.method !== "GET") return next();
37
+ const url = req.originalUrl || req.url || "/";
38
+ try {
39
+ let template$1 = await fs$1.readFile(path$1.resolve(process.cwd(), "index.html"), "utf-8");
40
+ template$1 = await vite.transformIndexHtml(url, template$1);
41
+ const { render: render$1 } = await vite.ssrLoadModule(ssrEntry);
42
+ const { html: appHtml, state } = await render$1(url);
43
+ res.statusCode = 200;
44
+ res.setHeader("Content-Type", "text/html");
45
+ res.end(template$1.replace(ssrOutlet$1, appHtml).replace(ssrState$1, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
46
+ } catch (e) {
47
+ vite.ssrFixStacktrace(e);
48
+ console.error(e);
49
+ res.statusCode = 500;
50
+ res.end(e.message);
51
+ }
52
+ };
53
+ }
54
+ return {
55
+ use(mw) {
56
+ userMiddlewares.push(mw);
57
+ },
58
+ async listen(_port) {
59
+ for (const mw of userMiddlewares) vite.middlewares.use(mw);
60
+ if (ssrFallback) vite.middlewares.use(ssrFallback);
61
+ await vite.listen();
62
+ vite.printUrls();
63
+ }
64
+ };
65
+ }
66
+ const fs = await import("node:fs/promises");
67
+ const path = await import("node:path");
68
+ const { createServer: createHttpServer } = await import("node:http");
69
+ const clientDir = opts.clientDir || path.resolve(import.meta.dirname, "../client");
70
+ const ssrOutlet = opts.ssrOutlet || (typeof __MOOST_SSR_OUTLET__ !== "undefined" ? __MOOST_SSR_OUTLET__ : "<!--ssr-outlet-->");
71
+ const ssrState = opts.ssrState || (typeof __MOOST_SSR_STATE__ !== "undefined" ? __MOOST_SSR_STATE__ : "<!--ssr-state-->");
72
+ let prefix = opts.prefix || __MOOST_PREFIX__;
73
+ if (!prefix.startsWith("/")) prefix = `/${prefix}`;
74
+ if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
75
+ const prefixSlash = prefix + "/";
76
+ const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
77
+ const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf-8");
78
+ let render = null;
79
+ const hasSsr = typeof __MOOST_SSR_ENTRY__ !== "undefined" && !!__MOOST_SSR_ENTRY__;
80
+ if (hasSsr) {
81
+ const ssrModule = await import(
82
+ /* @vite-ignore */
83
+ __MOOST_SSR_ENTRY__);
84
+ render = ssrModule.render;
85
+ }
86
+ const { MoostHttp, enableLocalFetch } = await import("@moostjs/event-http");
87
+ let moostHandler = null;
88
+ let moostHttpInstance = null;
89
+ const origListen = MoostHttp.prototype.listen;
90
+ MoostHttp.prototype.listen = function(...args) {
91
+ moostHandler = this.getServerCb();
92
+ moostHttpInstance = this;
93
+ setTimeout(() => args.filter((a) => typeof a === "function").forEach((a) => a()), 1);
94
+ return Promise.resolve();
95
+ };
96
+ if (typeof opts.entry === "function") await opts.entry();
97
+ else if (typeof opts.entry === "string") await import(
98
+ /* @vite-ignore */
99
+ opts.entry);
100
+ else await import(__MOOST_ENTRY__);
101
+ MoostHttp.prototype.listen = origListen;
102
+ if (enableLocalFetch && moostHttpInstance) enableLocalFetch(moostHttpInstance);
103
+ const sirv = (await import("sirv")).default;
104
+ const serve = sirv(path.resolve(clientDir), { extensions: [] });
105
+ return {
106
+ use(mw) {
107
+ userMiddlewares.push(mw);
108
+ },
109
+ async listen(port) {
110
+ const p = port || defaultPort;
111
+ createHttpServer((req, res) => {
112
+ const url = req.url || "/";
113
+ let i = 0;
114
+ const runNext = () => {
115
+ if (i < userMiddlewares.length) {
116
+ userMiddlewares[i++](req, res, runNext);
117
+ return;
118
+ }
119
+ if (moostHandler && (url.startsWith(prefixSlash) || url === prefix || url.startsWith(prefix + "?"))) {
120
+ moostHandler(req, res);
121
+ return;
122
+ }
123
+ serve(req, res, async () => {
124
+ if (req.method !== "GET") {
125
+ res.statusCode = 404;
126
+ res.end();
127
+ return;
128
+ }
129
+ try {
130
+ if (render) {
131
+ const { html: appHtml, state } = await render(url);
132
+ res.statusCode = 200;
133
+ res.setHeader("Content-Type", "text/html");
134
+ res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
135
+ } else {
136
+ res.statusCode = 200;
137
+ res.setHeader("Content-Type", "text/html");
138
+ res.end(template);
139
+ }
140
+ } catch (e) {
141
+ console.error(e);
142
+ res.statusCode = 500;
143
+ res.end(e.message);
144
+ }
145
+ });
146
+ };
147
+ runNext();
148
+ }).listen(p, () => {
149
+ console.log(`Server running at http://localhost:${p}`);
150
+ });
151
+ }
152
+ };
153
+ }
154
+
155
+ //#endregion
156
+ export { createSSRServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moostjs/vite",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Vite Dev plugin for moostjs",
5
5
  "keywords": [
6
6
  "composables",
@@ -36,8 +36,19 @@
36
36
  "types": "./dist/index.d.ts",
37
37
  "import": "./dist/index.mjs",
38
38
  "require": "./dist/index.cjs"
39
+ },
40
+ "./server": {
41
+ "types": "./dist/prod-server.d.ts",
42
+ "import": "./dist/prod-server.mjs",
43
+ "require": "./dist/prod-server.cjs"
39
44
  }
40
45
  },
46
+ "build": {
47
+ "entries": [
48
+ "src/index.ts",
49
+ "src/prod-server.ts"
50
+ ]
51
+ },
41
52
  "dependencies": {
42
53
  "magic-string": "^0.30.21"
43
54
  },
@@ -45,9 +56,15 @@
45
56
  "vitest": "3.2.4"
46
57
  },
47
58
  "peerDependencies": {
48
- "vite": "^7.0.0",
49
- "@moostjs/event-http": "^0.6.2",
50
- "moost": "^0.6.2"
59
+ "sirv": "^3.0.0",
60
+ "vite": "^8.0.0",
61
+ "@moostjs/event-http": "^0.6.4",
62
+ "moost": "^0.6.4"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "sirv": {
66
+ "optional": true
67
+ }
51
68
  },
52
69
  "scripts": {
53
70
  "pub": "pnpm publish --access public",