@moostjs/vite 0.6.2 → 0.6.3
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 +118 -11
- package/dist/index.cjs +114 -14
- package/dist/index.d.ts +48 -0
- package/dist/index.mjs +114 -14
- package/dist/prod-server.cjs +177 -0
- package/dist/prod-server.d.ts +29 -0
- package/dist/prod-server.mjs +154 -0
- package/package.json +21 -4
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
|
-
##
|
|
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,124 @@ 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 Local Fetch
|
|
105
|
+
|
|
106
|
+
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:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// During SSR rendering:
|
|
110
|
+
const res = await fetch('/api/hello/world')
|
|
111
|
+
// → Routed in-process to Moost, no TCP round-trip
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
Disable with `ssrFetch: false` when running behind Nitro or another framework that manages fetch routing itself.
|
|
117
|
+
|
|
118
|
+
## Hot Module Replacement
|
|
119
|
+
|
|
120
|
+
Both modes support full HMR for Moost controllers:
|
|
121
|
+
|
|
122
|
+
1. File change detected → Vite invalidates the module graph
|
|
123
|
+
2. Moost DI container is cleaned up (stale instances ejected, dependants cascade)
|
|
124
|
+
3. The SSR entry is re-imported → Moost re-initializes with updated controllers
|
|
125
|
+
4. New requests use the updated code — no server restart needed
|
|
126
|
+
|
|
127
|
+
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.
|
|
128
|
+
|
|
28
129
|
## Options
|
|
29
130
|
|
|
30
|
-
| Option
|
|
31
|
-
|
|
32
|
-
| `entry`
|
|
33
|
-
| `port`
|
|
34
|
-
| `host`
|
|
35
|
-
| `outDir`
|
|
36
|
-
| `format`
|
|
37
|
-
| `sourcemap` | `boolean`
|
|
38
|
-
| `externals` | `boolean \| object` | `true`
|
|
131
|
+
| Option | Type | Default | Description |
|
|
132
|
+
|---|---|---|---|
|
|
133
|
+
| `entry` | `string` | — | Application entry file (required) |
|
|
134
|
+
| `port` | `number` | `3000` | Dev server port (backend mode only) |
|
|
135
|
+
| `host` | `string` | `'localhost'` | Dev server host (backend mode only) |
|
|
136
|
+
| `outDir` | `string` | `'dist'` | Build output directory (backend mode only) |
|
|
137
|
+
| `format` | `'cjs' \| 'esm'` | `'esm'` | Output module format (backend mode only) |
|
|
138
|
+
| `sourcemap` | `boolean` | `true` | Generate source maps (backend mode only) |
|
|
139
|
+
| `externals` | `boolean \| object` | `true` | Configure external dependencies (backend mode only) |
|
|
140
|
+
| `onEject` | `function` | — | Hook to control DI instance ejection during HMR |
|
|
141
|
+
| `ssrFetch` | `boolean` | `true` | Enable local fetch interception for SSR |
|
|
142
|
+
| `middleware` | `boolean` | `false` | Run Moost as Connect middleware (Vite serves frontend) |
|
|
143
|
+
| `prefix` | `string` | — | URL prefix filter for middleware mode (e.g. `'/api'`) |
|
|
144
|
+
|
|
145
|
+
Options marked "backend mode only" are ignored when `middleware: true` — the user's `vite.config.ts` controls build/server configuration in middleware mode.
|
|
39
146
|
|
|
40
147
|
## License
|
|
41
148
|
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
}) : target, mod));
|
|
22
22
|
|
|
23
23
|
//#endregion
|
|
24
|
+
const vite = __toESM(require("vite"));
|
|
24
25
|
const magic_string = __toESM(require("magic-string"));
|
|
25
26
|
const node_fs = __toESM(require("node:fs"));
|
|
26
27
|
const node_module = __toESM(require("node:module"));
|
|
@@ -28,6 +29,11 @@ const moost = __toESM(require("moost"));
|
|
|
28
29
|
|
|
29
30
|
//#region packages/vite/src/utils.ts
|
|
30
31
|
const PLUGIN_NAME = "moost-vite";
|
|
32
|
+
const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
33
|
+
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
34
|
+
function entryBasename(entry) {
|
|
35
|
+
return entry.split("/").pop().replace(/\.ts$/, ".js");
|
|
36
|
+
}
|
|
31
37
|
/**
|
|
32
38
|
* Recursively gathers all importer modules upstream from a given module.
|
|
33
39
|
*
|
|
@@ -45,10 +51,14 @@ const logger = (0, moost.createLogger)({ level: 99 }).createTopic("\x1B[2m\x1B[3
|
|
|
45
51
|
function getLogger() {
|
|
46
52
|
return logger;
|
|
47
53
|
}
|
|
54
|
+
function escapeRegex(str) {
|
|
55
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
56
|
+
}
|
|
48
57
|
function getExternals({ node, workspace }) {
|
|
49
58
|
const pkg = JSON.parse((0, node_fs.readFileSync)("package.json", "utf8").toString());
|
|
50
|
-
const
|
|
51
|
-
|
|
59
|
+
const packageNames = workspace ? Object.keys(pkg.dependencies || {}) : Object.entries(pkg.dependencies || {}).filter(([key, ver]) => !ver.startsWith("workspace:")).map((i) => i[0]);
|
|
60
|
+
const externals = packageNames.map((name) => /* @__PURE__ */ new RegExp(`^${escapeRegex(name)}(/|$)`));
|
|
61
|
+
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
62
|
return externals;
|
|
53
63
|
}
|
|
54
64
|
|
|
@@ -72,7 +82,7 @@ function getExternals({ node, workspace }) {
|
|
|
72
82
|
const constructorName$1 = `Moost${adapter.charAt(0).toUpperCase() + adapter.slice(1)}`;
|
|
73
83
|
getLogger().log(`🔍 [2mExtracting Adapter "${constructorName$1}"`);
|
|
74
84
|
this.constructor = module$1[constructorName$1];
|
|
75
|
-
if (onInit && this.constructor) onInit(this.constructor);
|
|
85
|
+
if (onInit && this.constructor) onInit(this.constructor, module$1);
|
|
76
86
|
},
|
|
77
87
|
compare(c) {
|
|
78
88
|
if (this.detected && this.constructor) return this.constructor === c || c instanceof this.constructor || c.prototype instanceof this.constructor;
|
|
@@ -315,12 +325,29 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
315
325
|
const isTest = process.env.NODE_ENV === "test";
|
|
316
326
|
const isProd = process.env.NODE_ENV === "production";
|
|
317
327
|
const externals = options.externals ?? true;
|
|
328
|
+
let prefix = options.prefix;
|
|
329
|
+
if (prefix) {
|
|
330
|
+
if (!prefix.startsWith("/")) prefix = `/${prefix}`;
|
|
331
|
+
if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
|
|
332
|
+
}
|
|
333
|
+
const prefixSlash = prefix ? prefix + "/" : void 0;
|
|
334
|
+
const prefixQuery = prefix ? prefix + "?" : void 0;
|
|
318
335
|
let moostMiddleware = null;
|
|
336
|
+
let localFetchTeardown = null;
|
|
337
|
+
/** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
|
|
319
338
|
const adapters = isTest ? [] : [
|
|
320
|
-
createAdapterDetector("http", (MoostHttp) => {
|
|
339
|
+
createAdapterDetector("http", (MoostHttp, moduleExports) => {
|
|
321
340
|
MoostHttp.prototype.listen = function(...args) {
|
|
322
341
|
logger$1.log(`🔌 [2mOvertaking HTTP.listen`);
|
|
323
|
-
moostMiddleware = this.getServerCb()
|
|
342
|
+
if (options.middleware) moostMiddleware = this.getServerCb((req) => {
|
|
343
|
+
pendingNextMap.get(req)?.();
|
|
344
|
+
});
|
|
345
|
+
else moostMiddleware = this.getServerCb();
|
|
346
|
+
if (options.ssrFetch !== false && moduleExports?.enableLocalFetch) {
|
|
347
|
+
if (localFetchTeardown) localFetchTeardown();
|
|
348
|
+
localFetchTeardown = moduleExports.enableLocalFetch(this);
|
|
349
|
+
logger$1.log(`🔀 [2mLocal fetch enabled for SSR`);
|
|
350
|
+
}
|
|
324
351
|
setTimeout(() => {
|
|
325
352
|
args.filter((a) => typeof a === "function").forEach((a) => a());
|
|
326
353
|
}, 1);
|
|
@@ -337,17 +364,63 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
337
364
|
name: PLUGIN_NAME,
|
|
338
365
|
enforce: "pre",
|
|
339
366
|
config(cfg) {
|
|
367
|
+
if (options.middleware) {
|
|
368
|
+
const defaultExternals = getExternals({
|
|
369
|
+
node: true,
|
|
370
|
+
workspace: true
|
|
371
|
+
});
|
|
372
|
+
const serverDefines = {
|
|
373
|
+
"process.env.MOOST_DEFERRED_ENV": "\"production\"",
|
|
374
|
+
__MOOST_ENTRY__: JSON.stringify(options.entry),
|
|
375
|
+
__MOOST_PREFIX__: JSON.stringify(prefix || "/api")
|
|
376
|
+
};
|
|
377
|
+
const environments = {
|
|
378
|
+
client: { build: { outDir: cfg.build?.outDir || "dist/client" } },
|
|
379
|
+
server: {
|
|
380
|
+
build: {
|
|
381
|
+
outDir: "dist",
|
|
382
|
+
emptyOutDir: false,
|
|
383
|
+
ssr: "server.ts",
|
|
384
|
+
minify: false,
|
|
385
|
+
sourcemap: !!(options.sourcemap ?? true),
|
|
386
|
+
rollupOptions: {
|
|
387
|
+
external: (id) => {
|
|
388
|
+
if (id === "@moostjs/vite/server") return false;
|
|
389
|
+
return defaultExternals.some((ext) => ext.test(id));
|
|
390
|
+
},
|
|
391
|
+
output: { format: "esm" }
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
define: serverDefines
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
if (options.ssrEntry) {
|
|
398
|
+
const ssrEntryBasename = entryBasename(options.ssrEntry);
|
|
399
|
+
environments.client.build.ssrManifest = true;
|
|
400
|
+
environments.ssr = { build: {
|
|
401
|
+
outDir: "dist/ssr",
|
|
402
|
+
ssr: options.ssrEntry
|
|
403
|
+
} };
|
|
404
|
+
serverDefines.__MOOST_SSR_ENTRY__ = JSON.stringify(`./ssr/${ssrEntryBasename}`);
|
|
405
|
+
serverDefines.__MOOST_SSR_OUTLET__ = JSON.stringify(options.ssrOutlet || DEFAULT_SSR_OUTLET);
|
|
406
|
+
serverDefines.__MOOST_SSR_STATE__ = JSON.stringify(options.ssrState || DEFAULT_SSR_STATE);
|
|
407
|
+
}
|
|
408
|
+
const envNames = Object.keys(environments);
|
|
409
|
+
return {
|
|
410
|
+
builder: { async buildApp(builder) {
|
|
411
|
+
for (const name of envNames) if (builder.environments[name]) await builder.build(builder.environments[name]);
|
|
412
|
+
} },
|
|
413
|
+
environments
|
|
414
|
+
};
|
|
415
|
+
}
|
|
340
416
|
const entry = cfg.build?.rollupOptions?.input || options.entry;
|
|
341
|
-
const outfile = typeof entry === "string" ? entry
|
|
417
|
+
const outfile = typeof entry === "string" ? entryBasename(entry) : void 0;
|
|
342
418
|
return {
|
|
343
419
|
server: {
|
|
344
420
|
port: cfg.server?.port || options.port || 3e3,
|
|
345
421
|
host: cfg.server?.host || options.host
|
|
346
422
|
},
|
|
347
|
-
optimizeDeps: {
|
|
348
|
-
noDiscovery: cfg.optimizeDeps?.noDiscovery === void 0 ? true : cfg.optimizeDeps.noDiscovery,
|
|
349
|
-
exclude: cfg.optimizeDeps?.exclude || ["@swc/core"]
|
|
350
|
-
},
|
|
423
|
+
optimizeDeps: { noDiscovery: cfg.optimizeDeps?.noDiscovery === void 0 ? true : cfg.optimizeDeps.noDiscovery },
|
|
351
424
|
build: {
|
|
352
425
|
target: cfg.build?.target || "node",
|
|
353
426
|
outDir: cfg.build?.outDir || options.outDir || "dist",
|
|
@@ -369,6 +442,16 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
369
442
|
}
|
|
370
443
|
};
|
|
371
444
|
},
|
|
445
|
+
configResolved(config) {
|
|
446
|
+
config.__moostViteOptions = {
|
|
447
|
+
entry: options.entry,
|
|
448
|
+
ssrEntry: options.ssrEntry,
|
|
449
|
+
prefix,
|
|
450
|
+
port: options.port,
|
|
451
|
+
ssrOutlet: options.ssrOutlet,
|
|
452
|
+
ssrState: options.ssrState
|
|
453
|
+
};
|
|
454
|
+
},
|
|
372
455
|
async transform(code, id) {
|
|
373
456
|
if (!id.endsWith(".ts")) return null;
|
|
374
457
|
for (const adapter of adapters) if (!adapter.detected && adapter.regex.test(code)) await adapter.init();
|
|
@@ -399,19 +482,32 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
399
482
|
};
|
|
400
483
|
},
|
|
401
484
|
async configureServer(server) {
|
|
402
|
-
|
|
485
|
+
const runner = (0, vite.createServerModuleRunner)(server.environments.ssr);
|
|
486
|
+
const ssrImport = (id) => runner.import(id);
|
|
487
|
+
for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
|
|
403
488
|
moostRestartCleanup(adapters, options.onEject);
|
|
404
|
-
await
|
|
489
|
+
await ssrImport(options.entry);
|
|
405
490
|
server.middlewares.use(async (req, res, next) => {
|
|
406
491
|
if (reloadRequired) {
|
|
407
492
|
reloadRequired = false;
|
|
408
493
|
console.log();
|
|
409
494
|
logger$1.debug("🚀 Reloading Moost App...");
|
|
410
495
|
console.log();
|
|
411
|
-
await
|
|
496
|
+
await ssrImport(options.entry);
|
|
412
497
|
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
413
498
|
}
|
|
414
|
-
if (
|
|
499
|
+
if (options.middleware && prefix) {
|
|
500
|
+
const url = req.url || "";
|
|
501
|
+
if (url !== prefix && !url.startsWith(prefixSlash) && !url.startsWith(prefixQuery)) return next();
|
|
502
|
+
}
|
|
503
|
+
if (moostMiddleware) {
|
|
504
|
+
if (options.middleware) {
|
|
505
|
+
pendingNextMap.set(req, next);
|
|
506
|
+
moostMiddleware(req, res);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
return moostMiddleware(req, res);
|
|
510
|
+
}
|
|
415
511
|
next();
|
|
416
512
|
});
|
|
417
513
|
},
|
|
@@ -427,6 +523,10 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
427
523
|
this.environment.moduleGraph.invalidateModule(mod);
|
|
428
524
|
}
|
|
429
525
|
moostMiddleware = null;
|
|
526
|
+
if (localFetchTeardown) {
|
|
527
|
+
localFetchTeardown();
|
|
528
|
+
localFetchTeardown = null;
|
|
529
|
+
}
|
|
430
530
|
moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
431
531
|
reloadRequired = true;
|
|
432
532
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -65,6 +65,54 @@ 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;
|
|
68
116
|
}
|
|
69
117
|
/**
|
|
70
118
|
* The main Vite plugin for integrating Moost applications.
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createServerModuleRunner } from "vite";
|
|
1
2
|
import MagicString from "magic-string";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import { builtinModules } from "node:module";
|
|
@@ -5,6 +6,11 @@ import { Moost, clearGlobalWooks, createLogger, getMoostInfact, getMoostMate } f
|
|
|
5
6
|
|
|
6
7
|
//#region packages/vite/src/utils.ts
|
|
7
8
|
const PLUGIN_NAME = "moost-vite";
|
|
9
|
+
const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
10
|
+
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
11
|
+
function entryBasename(entry) {
|
|
12
|
+
return entry.split("/").pop().replace(/\.ts$/, ".js");
|
|
13
|
+
}
|
|
8
14
|
/**
|
|
9
15
|
* Recursively gathers all importer modules upstream from a given module.
|
|
10
16
|
*
|
|
@@ -22,10 +28,14 @@ const logger = createLogger({ level: 99 }).createTopic("\x1B[2m\x1B[36m" + PLUGI
|
|
|
22
28
|
function getLogger() {
|
|
23
29
|
return logger;
|
|
24
30
|
}
|
|
31
|
+
function escapeRegex(str) {
|
|
32
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
}
|
|
25
34
|
function getExternals({ node, workspace }) {
|
|
26
35
|
const pkg = JSON.parse(readFileSync("package.json", "utf8").toString());
|
|
27
|
-
const
|
|
28
|
-
|
|
36
|
+
const packageNames = workspace ? Object.keys(pkg.dependencies || {}) : Object.entries(pkg.dependencies || {}).filter(([key, ver]) => !ver.startsWith("workspace:")).map((i) => i[0]);
|
|
37
|
+
const externals = packageNames.map((name) => /* @__PURE__ */ new RegExp(`^${escapeRegex(name)}(/|$)`));
|
|
38
|
+
if (node) externals.push(...builtinModules.map((m) => /* @__PURE__ */ new RegExp(`^${escapeRegex(m)}(/|$)`)), ...builtinModules.map((m) => /* @__PURE__ */ new RegExp(`^node:${escapeRegex(m)}(/|$)`)));
|
|
29
39
|
return externals;
|
|
30
40
|
}
|
|
31
41
|
|
|
@@ -49,7 +59,7 @@ function getExternals({ node, workspace }) {
|
|
|
49
59
|
const constructorName$1 = `Moost${adapter.charAt(0).toUpperCase() + adapter.slice(1)}`;
|
|
50
60
|
getLogger().log(`🔍 [2mExtracting Adapter "${constructorName$1}"`);
|
|
51
61
|
this.constructor = module[constructorName$1];
|
|
52
|
-
if (onInit && this.constructor) onInit(this.constructor);
|
|
62
|
+
if (onInit && this.constructor) onInit(this.constructor, module);
|
|
53
63
|
},
|
|
54
64
|
compare(c) {
|
|
55
65
|
if (this.detected && this.constructor) return this.constructor === c || c instanceof this.constructor || c.prototype instanceof this.constructor;
|
|
@@ -292,12 +302,29 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
292
302
|
const isTest = false;
|
|
293
303
|
const isProd = false;
|
|
294
304
|
const externals = options.externals ?? true;
|
|
305
|
+
let prefix = options.prefix;
|
|
306
|
+
if (prefix) {
|
|
307
|
+
if (!prefix.startsWith("/")) prefix = `/${prefix}`;
|
|
308
|
+
if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
|
|
309
|
+
}
|
|
310
|
+
const prefixSlash = prefix ? prefix + "/" : void 0;
|
|
311
|
+
const prefixQuery = prefix ? prefix + "?" : void 0;
|
|
295
312
|
let moostMiddleware = null;
|
|
313
|
+
let localFetchTeardown = null;
|
|
314
|
+
/** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
|
|
296
315
|
const adapters = isTest ? [] : [
|
|
297
|
-
createAdapterDetector("http", (MoostHttp) => {
|
|
316
|
+
createAdapterDetector("http", (MoostHttp, moduleExports) => {
|
|
298
317
|
MoostHttp.prototype.listen = function(...args) {
|
|
299
318
|
logger$1.log(`🔌 [2mOvertaking HTTP.listen`);
|
|
300
|
-
moostMiddleware = this.getServerCb()
|
|
319
|
+
if (options.middleware) moostMiddleware = this.getServerCb((req) => {
|
|
320
|
+
pendingNextMap.get(req)?.();
|
|
321
|
+
});
|
|
322
|
+
else moostMiddleware = this.getServerCb();
|
|
323
|
+
if (options.ssrFetch !== false && moduleExports?.enableLocalFetch) {
|
|
324
|
+
if (localFetchTeardown) localFetchTeardown();
|
|
325
|
+
localFetchTeardown = moduleExports.enableLocalFetch(this);
|
|
326
|
+
logger$1.log(`🔀 [2mLocal fetch enabled for SSR`);
|
|
327
|
+
}
|
|
301
328
|
setTimeout(() => {
|
|
302
329
|
args.filter((a) => typeof a === "function").forEach((a) => a());
|
|
303
330
|
}, 1);
|
|
@@ -314,17 +341,63 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
314
341
|
name: PLUGIN_NAME,
|
|
315
342
|
enforce: "pre",
|
|
316
343
|
config(cfg) {
|
|
344
|
+
if (options.middleware) {
|
|
345
|
+
const defaultExternals = getExternals({
|
|
346
|
+
node: true,
|
|
347
|
+
workspace: true
|
|
348
|
+
});
|
|
349
|
+
const serverDefines = {
|
|
350
|
+
"process.env.MOOST_DEFERRED_ENV": "\"production\"",
|
|
351
|
+
__MOOST_ENTRY__: JSON.stringify(options.entry),
|
|
352
|
+
__MOOST_PREFIX__: JSON.stringify(prefix || "/api")
|
|
353
|
+
};
|
|
354
|
+
const environments = {
|
|
355
|
+
client: { build: { outDir: cfg.build?.outDir || "dist/client" } },
|
|
356
|
+
server: {
|
|
357
|
+
build: {
|
|
358
|
+
outDir: "dist",
|
|
359
|
+
emptyOutDir: false,
|
|
360
|
+
ssr: "server.ts",
|
|
361
|
+
minify: false,
|
|
362
|
+
sourcemap: !!(options.sourcemap ?? true),
|
|
363
|
+
rollupOptions: {
|
|
364
|
+
external: (id) => {
|
|
365
|
+
if (id === "@moostjs/vite/server") return false;
|
|
366
|
+
return defaultExternals.some((ext) => ext.test(id));
|
|
367
|
+
},
|
|
368
|
+
output: { format: "esm" }
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
define: serverDefines
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
if (options.ssrEntry) {
|
|
375
|
+
const ssrEntryBasename = entryBasename(options.ssrEntry);
|
|
376
|
+
environments.client.build.ssrManifest = true;
|
|
377
|
+
environments.ssr = { build: {
|
|
378
|
+
outDir: "dist/ssr",
|
|
379
|
+
ssr: options.ssrEntry
|
|
380
|
+
} };
|
|
381
|
+
serverDefines.__MOOST_SSR_ENTRY__ = JSON.stringify(`./ssr/${ssrEntryBasename}`);
|
|
382
|
+
serverDefines.__MOOST_SSR_OUTLET__ = JSON.stringify(options.ssrOutlet || DEFAULT_SSR_OUTLET);
|
|
383
|
+
serverDefines.__MOOST_SSR_STATE__ = JSON.stringify(options.ssrState || DEFAULT_SSR_STATE);
|
|
384
|
+
}
|
|
385
|
+
const envNames = Object.keys(environments);
|
|
386
|
+
return {
|
|
387
|
+
builder: { async buildApp(builder) {
|
|
388
|
+
for (const name of envNames) if (builder.environments[name]) await builder.build(builder.environments[name]);
|
|
389
|
+
} },
|
|
390
|
+
environments
|
|
391
|
+
};
|
|
392
|
+
}
|
|
317
393
|
const entry = cfg.build?.rollupOptions?.input || options.entry;
|
|
318
|
-
const outfile = typeof entry === "string" ? entry
|
|
394
|
+
const outfile = typeof entry === "string" ? entryBasename(entry) : void 0;
|
|
319
395
|
return {
|
|
320
396
|
server: {
|
|
321
397
|
port: cfg.server?.port || options.port || 3e3,
|
|
322
398
|
host: cfg.server?.host || options.host
|
|
323
399
|
},
|
|
324
|
-
optimizeDeps: {
|
|
325
|
-
noDiscovery: cfg.optimizeDeps?.noDiscovery === void 0 ? true : cfg.optimizeDeps.noDiscovery,
|
|
326
|
-
exclude: cfg.optimizeDeps?.exclude || ["@swc/core"]
|
|
327
|
-
},
|
|
400
|
+
optimizeDeps: { noDiscovery: cfg.optimizeDeps?.noDiscovery === void 0 ? true : cfg.optimizeDeps.noDiscovery },
|
|
328
401
|
build: {
|
|
329
402
|
target: cfg.build?.target || "node",
|
|
330
403
|
outDir: cfg.build?.outDir || options.outDir || "dist",
|
|
@@ -346,6 +419,16 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
346
419
|
}
|
|
347
420
|
};
|
|
348
421
|
},
|
|
422
|
+
configResolved(config) {
|
|
423
|
+
config.__moostViteOptions = {
|
|
424
|
+
entry: options.entry,
|
|
425
|
+
ssrEntry: options.ssrEntry,
|
|
426
|
+
prefix,
|
|
427
|
+
port: options.port,
|
|
428
|
+
ssrOutlet: options.ssrOutlet,
|
|
429
|
+
ssrState: options.ssrState
|
|
430
|
+
};
|
|
431
|
+
},
|
|
349
432
|
async transform(code, id) {
|
|
350
433
|
if (!id.endsWith(".ts")) return null;
|
|
351
434
|
for (const adapter of adapters) if (!adapter.detected && adapter.regex.test(code)) await adapter.init();
|
|
@@ -376,19 +459,32 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
376
459
|
};
|
|
377
460
|
},
|
|
378
461
|
async configureServer(server) {
|
|
379
|
-
|
|
462
|
+
const runner = createServerModuleRunner(server.environments.ssr);
|
|
463
|
+
const ssrImport = (id) => runner.import(id);
|
|
464
|
+
for (const adapter of adapters) adapter.ssrLoadModule = ssrImport;
|
|
380
465
|
moostRestartCleanup(adapters, options.onEject);
|
|
381
|
-
await
|
|
466
|
+
await ssrImport(options.entry);
|
|
382
467
|
server.middlewares.use(async (req, res, next) => {
|
|
383
468
|
if (reloadRequired) {
|
|
384
469
|
reloadRequired = false;
|
|
385
470
|
console.log();
|
|
386
471
|
logger$1.debug("🚀 Reloading Moost App...");
|
|
387
472
|
console.log();
|
|
388
|
-
await
|
|
473
|
+
await ssrImport(options.entry);
|
|
389
474
|
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
390
475
|
}
|
|
391
|
-
if (
|
|
476
|
+
if (options.middleware && prefix) {
|
|
477
|
+
const url = req.url || "";
|
|
478
|
+
if (url !== prefix && !url.startsWith(prefixSlash) && !url.startsWith(prefixQuery)) return next();
|
|
479
|
+
}
|
|
480
|
+
if (moostMiddleware) {
|
|
481
|
+
if (options.middleware) {
|
|
482
|
+
pendingNextMap.set(req, next);
|
|
483
|
+
moostMiddleware(req, res);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
return moostMiddleware(req, res);
|
|
487
|
+
}
|
|
392
488
|
next();
|
|
393
489
|
});
|
|
394
490
|
},
|
|
@@ -404,6 +500,10 @@ const REG_REPLACE_EXPORT_CLASS = /(^\s*@(Injectable|Controller)\()/gm;
|
|
|
404
500
|
this.environment.moduleGraph.invalidateModule(mod);
|
|
405
501
|
}
|
|
406
502
|
moostMiddleware = null;
|
|
503
|
+
if (localFetchTeardown) {
|
|
504
|
+
localFetchTeardown();
|
|
505
|
+
localFetchTeardown = null;
|
|
506
|
+
}
|
|
407
507
|
moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
408
508
|
reloadRequired = true;
|
|
409
509
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
if (!ssrEntry) throw new Error("createSSRServer: ssrEntry is required. Set it in moostVite() options or pass it directly.");
|
|
55
|
+
const fs$1 = await import("node:fs/promises");
|
|
56
|
+
const path$1 = await import("node:path");
|
|
57
|
+
const ssrFallback = async (req, res, next) => {
|
|
58
|
+
if (req.method !== "GET") return next();
|
|
59
|
+
const url = req.originalUrl || req.url || "/";
|
|
60
|
+
try {
|
|
61
|
+
let template$1 = await fs$1.readFile(path$1.resolve(process.cwd(), "index.html"), "utf-8");
|
|
62
|
+
template$1 = await vite.transformIndexHtml(url, template$1);
|
|
63
|
+
const { render: render$1 } = await vite.ssrLoadModule(ssrEntry);
|
|
64
|
+
const { html: appHtml, state } = await render$1(url);
|
|
65
|
+
res.statusCode = 200;
|
|
66
|
+
res.setHeader("Content-Type", "text/html");
|
|
67
|
+
res.end(template$1.replace(ssrOutlet$1, appHtml).replace(ssrState$1, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
|
|
68
|
+
} catch (e) {
|
|
69
|
+
vite.ssrFixStacktrace(e);
|
|
70
|
+
console.error(e);
|
|
71
|
+
res.statusCode = 500;
|
|
72
|
+
res.end(e.message);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
use(mw) {
|
|
77
|
+
userMiddlewares.push(mw);
|
|
78
|
+
},
|
|
79
|
+
async listen(_port) {
|
|
80
|
+
for (const mw of userMiddlewares) vite.middlewares.use(mw);
|
|
81
|
+
vite.middlewares.use(ssrFallback);
|
|
82
|
+
await vite.listen();
|
|
83
|
+
vite.printUrls();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const fs = await import("node:fs/promises");
|
|
88
|
+
const path = await import("node:path");
|
|
89
|
+
const { createServer: createHttpServer } = await import("node:http");
|
|
90
|
+
const clientDir = opts.clientDir || "dist/client";
|
|
91
|
+
const ssrOutlet = opts.ssrOutlet || (typeof __MOOST_SSR_OUTLET__ !== "undefined" ? __MOOST_SSR_OUTLET__ : "<!--ssr-outlet-->");
|
|
92
|
+
const ssrState = opts.ssrState || (typeof __MOOST_SSR_STATE__ !== "undefined" ? __MOOST_SSR_STATE__ : "<!--ssr-state-->");
|
|
93
|
+
let prefix = opts.prefix || __MOOST_PREFIX__;
|
|
94
|
+
if (!prefix.startsWith("/")) prefix = `/${prefix}`;
|
|
95
|
+
if (prefix.endsWith("/")) prefix = prefix.slice(0, -1);
|
|
96
|
+
const prefixSlash = prefix + "/";
|
|
97
|
+
const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
|
|
98
|
+
const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf-8");
|
|
99
|
+
let render = null;
|
|
100
|
+
const hasSsr = typeof __MOOST_SSR_ENTRY__ !== "undefined" && !!__MOOST_SSR_ENTRY__;
|
|
101
|
+
if (hasSsr) {
|
|
102
|
+
const ssrModule = await import(
|
|
103
|
+
/* @vite-ignore */
|
|
104
|
+
__MOOST_SSR_ENTRY__);
|
|
105
|
+
render = ssrModule.render;
|
|
106
|
+
}
|
|
107
|
+
const { MoostHttp, enableLocalFetch } = await import("@moostjs/event-http");
|
|
108
|
+
let moostHandler = null;
|
|
109
|
+
let moostHttpInstance = null;
|
|
110
|
+
const origListen = MoostHttp.prototype.listen;
|
|
111
|
+
MoostHttp.prototype.listen = function(...args) {
|
|
112
|
+
moostHandler = this.getServerCb();
|
|
113
|
+
moostHttpInstance = this;
|
|
114
|
+
setTimeout(() => args.filter((a) => typeof a === "function").forEach((a) => a()), 1);
|
|
115
|
+
return Promise.resolve();
|
|
116
|
+
};
|
|
117
|
+
if (typeof opts.entry === "function") await opts.entry();
|
|
118
|
+
else if (typeof opts.entry === "string") await import(
|
|
119
|
+
/* @vite-ignore */
|
|
120
|
+
opts.entry);
|
|
121
|
+
else await import(__MOOST_ENTRY__);
|
|
122
|
+
MoostHttp.prototype.listen = origListen;
|
|
123
|
+
if (enableLocalFetch && moostHttpInstance) enableLocalFetch(moostHttpInstance);
|
|
124
|
+
const sirv = (await import("sirv")).default;
|
|
125
|
+
const serve = sirv(path.resolve(clientDir), { extensions: [] });
|
|
126
|
+
return {
|
|
127
|
+
use(mw) {
|
|
128
|
+
userMiddlewares.push(mw);
|
|
129
|
+
},
|
|
130
|
+
async listen(port) {
|
|
131
|
+
const p = port || defaultPort;
|
|
132
|
+
createHttpServer((req, res) => {
|
|
133
|
+
const url = req.url || "/";
|
|
134
|
+
let i = 0;
|
|
135
|
+
const runNext = () => {
|
|
136
|
+
if (i < userMiddlewares.length) {
|
|
137
|
+
userMiddlewares[i++](req, res, runNext);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (moostHandler && (url.startsWith(prefixSlash) || url === prefix || url.startsWith(prefix + "?"))) {
|
|
141
|
+
moostHandler(req, res);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
serve(req, res, async () => {
|
|
145
|
+
if (req.method !== "GET") {
|
|
146
|
+
res.statusCode = 404;
|
|
147
|
+
res.end();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
if (render) {
|
|
152
|
+
const { html: appHtml, state } = await render(url);
|
|
153
|
+
res.statusCode = 200;
|
|
154
|
+
res.setHeader("Content-Type", "text/html");
|
|
155
|
+
res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
|
|
156
|
+
} else {
|
|
157
|
+
res.statusCode = 200;
|
|
158
|
+
res.setHeader("Content-Type", "text/html");
|
|
159
|
+
res.end(template);
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error(e);
|
|
163
|
+
res.statusCode = 500;
|
|
164
|
+
res.end(e.message);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
runNext();
|
|
169
|
+
}).listen(p, () => {
|
|
170
|
+
console.log(`Server running at http://localhost:${p}`);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
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,154 @@
|
|
|
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
|
+
if (!ssrEntry) throw new Error("createSSRServer: ssrEntry is required. Set it in moostVite() options or pass it directly.");
|
|
32
|
+
const fs$1 = await import("node:fs/promises");
|
|
33
|
+
const path$1 = await import("node:path");
|
|
34
|
+
const ssrFallback = async (req, res, next) => {
|
|
35
|
+
if (req.method !== "GET") return next();
|
|
36
|
+
const url = req.originalUrl || req.url || "/";
|
|
37
|
+
try {
|
|
38
|
+
let template$1 = await fs$1.readFile(path$1.resolve(process.cwd(), "index.html"), "utf-8");
|
|
39
|
+
template$1 = await vite.transformIndexHtml(url, template$1);
|
|
40
|
+
const { render: render$1 } = await vite.ssrLoadModule(ssrEntry);
|
|
41
|
+
const { html: appHtml, state } = await render$1(url);
|
|
42
|
+
res.statusCode = 200;
|
|
43
|
+
res.setHeader("Content-Type", "text/html");
|
|
44
|
+
res.end(template$1.replace(ssrOutlet$1, appHtml).replace(ssrState$1, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
vite.ssrFixStacktrace(e);
|
|
47
|
+
console.error(e);
|
|
48
|
+
res.statusCode = 500;
|
|
49
|
+
res.end(e.message);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
use(mw) {
|
|
54
|
+
userMiddlewares.push(mw);
|
|
55
|
+
},
|
|
56
|
+
async listen(_port) {
|
|
57
|
+
for (const mw of userMiddlewares) vite.middlewares.use(mw);
|
|
58
|
+
vite.middlewares.use(ssrFallback);
|
|
59
|
+
await vite.listen();
|
|
60
|
+
vite.printUrls();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const fs = await import("node:fs/promises");
|
|
65
|
+
const path = await import("node:path");
|
|
66
|
+
const { createServer: createHttpServer } = await import("node:http");
|
|
67
|
+
const clientDir = opts.clientDir || "dist/client";
|
|
68
|
+
const ssrOutlet = opts.ssrOutlet || (typeof __MOOST_SSR_OUTLET__ !== "undefined" ? __MOOST_SSR_OUTLET__ : "<!--ssr-outlet-->");
|
|
69
|
+
const ssrState = opts.ssrState || (typeof __MOOST_SSR_STATE__ !== "undefined" ? __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 + "/";
|
|
74
|
+
const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
|
|
75
|
+
const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf-8");
|
|
76
|
+
let render = null;
|
|
77
|
+
const hasSsr = typeof __MOOST_SSR_ENTRY__ !== "undefined" && !!__MOOST_SSR_ENTRY__;
|
|
78
|
+
if (hasSsr) {
|
|
79
|
+
const ssrModule = await import(
|
|
80
|
+
/* @vite-ignore */
|
|
81
|
+
__MOOST_SSR_ENTRY__);
|
|
82
|
+
render = ssrModule.render;
|
|
83
|
+
}
|
|
84
|
+
const { MoostHttp, enableLocalFetch } = await import("@moostjs/event-http");
|
|
85
|
+
let moostHandler = null;
|
|
86
|
+
let moostHttpInstance = null;
|
|
87
|
+
const origListen = MoostHttp.prototype.listen;
|
|
88
|
+
MoostHttp.prototype.listen = function(...args) {
|
|
89
|
+
moostHandler = this.getServerCb();
|
|
90
|
+
moostHttpInstance = this;
|
|
91
|
+
setTimeout(() => args.filter((a) => typeof a === "function").forEach((a) => a()), 1);
|
|
92
|
+
return Promise.resolve();
|
|
93
|
+
};
|
|
94
|
+
if (typeof opts.entry === "function") await opts.entry();
|
|
95
|
+
else if (typeof opts.entry === "string") await import(
|
|
96
|
+
/* @vite-ignore */
|
|
97
|
+
opts.entry);
|
|
98
|
+
else await import(__MOOST_ENTRY__);
|
|
99
|
+
MoostHttp.prototype.listen = origListen;
|
|
100
|
+
if (enableLocalFetch && moostHttpInstance) enableLocalFetch(moostHttpInstance);
|
|
101
|
+
const sirv = (await import("sirv")).default;
|
|
102
|
+
const serve = sirv(path.resolve(clientDir), { extensions: [] });
|
|
103
|
+
return {
|
|
104
|
+
use(mw) {
|
|
105
|
+
userMiddlewares.push(mw);
|
|
106
|
+
},
|
|
107
|
+
async listen(port) {
|
|
108
|
+
const p = port || defaultPort;
|
|
109
|
+
createHttpServer((req, res) => {
|
|
110
|
+
const url = req.url || "/";
|
|
111
|
+
let i = 0;
|
|
112
|
+
const runNext = () => {
|
|
113
|
+
if (i < userMiddlewares.length) {
|
|
114
|
+
userMiddlewares[i++](req, res, runNext);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (moostHandler && (url.startsWith(prefixSlash) || url === prefix || url.startsWith(prefix + "?"))) {
|
|
118
|
+
moostHandler(req, res);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
serve(req, res, async () => {
|
|
122
|
+
if (req.method !== "GET") {
|
|
123
|
+
res.statusCode = 404;
|
|
124
|
+
res.end();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
if (render) {
|
|
129
|
+
const { html: appHtml, state } = await render(url);
|
|
130
|
+
res.statusCode = 200;
|
|
131
|
+
res.setHeader("Content-Type", "text/html");
|
|
132
|
+
res.end(template.replace(ssrOutlet, appHtml).replace(ssrState, state ? `<script>window.__SSR_STATE__=${state}</script>` : ""));
|
|
133
|
+
} else {
|
|
134
|
+
res.statusCode = 200;
|
|
135
|
+
res.setHeader("Content-Type", "text/html");
|
|
136
|
+
res.end(template);
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
console.error(e);
|
|
140
|
+
res.statusCode = 500;
|
|
141
|
+
res.end(e.message);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
runNext();
|
|
146
|
+
}).listen(p, () => {
|
|
147
|
+
console.log(`Server running at http://localhost:${p}`);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//#endregion
|
|
154
|
+
export { createSSRServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moostjs/vite",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
59
|
+
"sirv": "^3.0.0",
|
|
60
|
+
"vite": "^8.0.0",
|
|
61
|
+
"@moostjs/event-http": "^0.6.3",
|
|
62
|
+
"moost": "^0.6.3"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"sirv": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
51
68
|
},
|
|
52
69
|
"scripts": {
|
|
53
70
|
"pub": "pnpm publish --access public",
|