@moonsjs/vite 0.1.3 → 0.1.5
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/dist/api.d.ts +8 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/{index-1sm9pvvd.js → index-1fnt137k.js} +106 -18
- package/dist/index-1fnt137k.js.map +14 -0
- package/dist/{index-3ec9vpek.js → index-e7qz0epf.js} +232 -24
- package/dist/index-e7qz0epf.js.map +11 -0
- package/dist/index.js +220 -19
- package/dist/index.js.map +5 -4
- package/dist/manifest.d.ts +6 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/payload-scan.d.ts +18 -0
- package/dist/payload-scan.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/server/dev.d.ts +4 -0
- package/dist/server/dev.d.ts.map +1 -1
- package/dist/server/handler.d.ts +21 -1
- package/dist/server/handler.d.ts.map +1 -1
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +584 -28
- package/dist/server/index.js.map +10 -7
- package/dist/server/node-server.d.ts +4 -1
- package/dist/server/node-server.d.ts.map +1 -1
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/plugins.d.ts +26 -0
- package/dist/server/plugins.d.ts.map +1 -0
- package/dist/server/prerender.d.ts +28 -0
- package/dist/server/prerender.d.ts.map +1 -0
- package/dist/server/shell.d.ts +3 -3
- package/dist/server/shell.d.ts.map +1 -1
- package/dist/server/shutdown.d.ts +6 -0
- package/dist/server/shutdown.d.ts.map +1 -1
- package/dist/server/test-server.d.ts.map +1 -1
- package/dist/server-only.d.ts +57 -0
- package/dist/server-only.d.ts.map +1 -0
- package/dist/transform.d.ts +9 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +1 -1
- package/dist/virtual-modules.d.ts +1 -0
- package/dist/virtual-modules.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +124 -0
- package/src/manifest.ts +42 -1
- package/src/payload-scan.ts +53 -0
- package/src/plugin.ts +137 -10
- package/src/server/dev.ts +48 -12
- package/src/server/handler.ts +373 -14
- package/src/server/index.ts +6 -0
- package/src/server/node-server.ts +70 -16
- package/src/server/node.ts +17 -0
- package/src/server/plugins.ts +85 -0
- package/src/server/prerender.ts +230 -0
- package/src/server/shell.ts +62 -8
- package/src/server/shutdown.ts +19 -2
- package/src/server/test-server.ts +2 -1
- package/src/server-only.ts +220 -0
- package/src/transform.ts +138 -8
- package/src/virtual-modules.ts +1 -0
- package/dist/index-1sm9pvvd.js.map +0 -14
- package/dist/index-3ec9vpek.js.map +0 -10
package/src/api.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
import { API_METHODS, MoonsError } from "@moonsjs/core";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validasi statis export modul `src/api` (PRD Addendum D.1):
|
|
6
|
+
* hanya handler method `GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS` sebagai fungsi;
|
|
7
|
+
* `default`/export lain ditolak → `E_API_HANDLER_INVALID`.
|
|
8
|
+
*/
|
|
9
|
+
export function assertApiModuleExports(code: string, file: string): string[] {
|
|
10
|
+
let ast: unknown;
|
|
11
|
+
try {
|
|
12
|
+
ast = parse(code, {
|
|
13
|
+
sourceType: "module",
|
|
14
|
+
plugins: file.endsWith(".tsx") ? ["typescript", "jsx"] : ["typescript"],
|
|
15
|
+
});
|
|
16
|
+
} catch {
|
|
17
|
+
return []; // biarkan bundler melaporkan syntax error
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const program = (ast as { program?: { body?: Array<Record<string, unknown>> } }).program;
|
|
21
|
+
const body = program?.body ?? [];
|
|
22
|
+
const methods: string[] = [];
|
|
23
|
+
|
|
24
|
+
for (const statement of body) {
|
|
25
|
+
if (statement["type"] === "ExportDefaultDeclaration") {
|
|
26
|
+
throw apiError(
|
|
27
|
+
file,
|
|
28
|
+
"export `default` tidak didukung di modul API",
|
|
29
|
+
"Modul `src/api` hanya mengekspor handler method bernama (PRD Addendum D.1).",
|
|
30
|
+
["Ganti menjadi `export const GET = (args) => …`", "Hapus export default"],
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (statement["type"] !== "ExportNamedDeclaration") {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (statement["source"] !== undefined) {
|
|
37
|
+
// re-export `export { X } from "..."` — nama tetap dicek di bawah.
|
|
38
|
+
}
|
|
39
|
+
const declaration = statement["declaration"] as Record<string, unknown> | undefined;
|
|
40
|
+
if (declaration !== undefined) {
|
|
41
|
+
if (declaration["type"] === "VariableDeclaration") {
|
|
42
|
+
for (const declarator of (declaration["declarations"] ?? []) as Array<Record<string, unknown>>) {
|
|
43
|
+
const id = declarator["id"] as Record<string, unknown> | undefined;
|
|
44
|
+
if (id?.["type"] === "Identifier") {
|
|
45
|
+
methods.push(id["name"] as string);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else if (declaration["type"] === "FunctionDeclaration") {
|
|
49
|
+
const id = declaration["id"] as Record<string, unknown> | undefined;
|
|
50
|
+
if (id?.["type"] === "Identifier") {
|
|
51
|
+
methods.push(id["name"] as string);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const specifier of (statement["specifiers"] ?? []) as Array<Record<string, unknown>>) {
|
|
57
|
+
const exported = specifier["exported"] as Record<string, unknown> | undefined;
|
|
58
|
+
if (exported?.["type"] === "Identifier") {
|
|
59
|
+
methods.push(exported["name"] as string);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const illegal = methods.filter((name) => !API_METHODS.includes(name as never));
|
|
65
|
+
if (illegal.length > 0) {
|
|
66
|
+
throw apiError(
|
|
67
|
+
file,
|
|
68
|
+
`export \`${illegal[0]}\` bukan handler method yang dikenal`,
|
|
69
|
+
`Handler yang legal: ${API_METHODS.join(", ")} (PRD Addendum D.1).`,
|
|
70
|
+
[
|
|
71
|
+
"Ganti nama export menjadi salah satu method HTTP",
|
|
72
|
+
"Pindahkan helper ke modul lain (mis. `src/server/…`)",
|
|
73
|
+
],
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (methods.length === 0) {
|
|
77
|
+
throw apiError(
|
|
78
|
+
file,
|
|
79
|
+
"modul API tanpa handler method",
|
|
80
|
+
`Wajib ada minimal satu dari ${API_METHODS.join(", ")} (PRD Addendum D.1).`,
|
|
81
|
+
[`Tambahkan \`export const GET = (args) => new Response(…)\``],
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return methods;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function assertMiddlewareExports(code: string, file: string): void {
|
|
89
|
+
let ast: unknown;
|
|
90
|
+
try {
|
|
91
|
+
ast = parse(code, {
|
|
92
|
+
sourceType: "module",
|
|
93
|
+
plugins: file.endsWith(".tsx") ? ["typescript", "jsx"] : ["typescript"],
|
|
94
|
+
});
|
|
95
|
+
} catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const program = (ast as { program?: { body?: Array<Record<string, unknown>> } }).program;
|
|
99
|
+
const hasDefault = (program?.body ?? []).some(
|
|
100
|
+
(statement) => statement["type"] === "ExportDefaultDeclaration",
|
|
101
|
+
);
|
|
102
|
+
if (!hasDefault) {
|
|
103
|
+
throw new MoonsError({
|
|
104
|
+
code: "E_MIDDLEWARE_INVALID",
|
|
105
|
+
file,
|
|
106
|
+
message: "`_middleware.ts` tanpa `export default`",
|
|
107
|
+
details: "Middleware direktori mengikuti bentuk `Middleware`: `(args) => Promise<Response>` (PRD Addendum D.3).",
|
|
108
|
+
suggestions: [
|
|
109
|
+
"Tambahkan `export default async ({ req, ctx, next }) => await next()`",
|
|
110
|
+
"Hapus file bila tidak dipakai",
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function apiError(file: string, message: string, details: string, suggestions: string[]): MoonsError {
|
|
117
|
+
return new MoonsError({
|
|
118
|
+
code: "E_API_HANDLER_INVALID",
|
|
119
|
+
file,
|
|
120
|
+
message,
|
|
121
|
+
details,
|
|
122
|
+
suggestions,
|
|
123
|
+
});
|
|
124
|
+
}
|
package/src/manifest.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findLayouts, routeKeyFromFile, type RouteDefinition } from "@moonsjs/core";
|
|
1
|
+
import { findLayouts, routeKeyFromFile, type ApiRouteDefinition, type RouteDefinition } from "@moonsjs/core";
|
|
2
2
|
import { relative, sep } from "node:path";
|
|
3
3
|
|
|
4
4
|
export interface ClientManifestEntry {
|
|
@@ -38,10 +38,51 @@ export function buildClientManifest(
|
|
|
38
38
|
if (errorRoute !== undefined) {
|
|
39
39
|
manifest[ERROR_ROUTE_KEY] = { page: routeKeyFromFile(routesDir, errorRoute.file), layouts: [] };
|
|
40
40
|
}
|
|
41
|
+
// D.3: semua boundary `_error` (termasuk bersarang) ikut manifest — payload
|
|
42
|
+
// memakai key file boundary yang dipilih runtime.
|
|
43
|
+
for (const route of routes) {
|
|
44
|
+
if (route.kind !== "error") {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const key = routeKeyFromFile(routesDir, route.file);
|
|
48
|
+
manifest[key] = { page: key, layouts: [] };
|
|
49
|
+
}
|
|
41
50
|
|
|
42
51
|
return manifest;
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
/** `virtual:moons/api` (Addendum D.1 + D.3): tabel API + middleware + peta import(). */
|
|
55
|
+
export function renderApiModule(
|
|
56
|
+
apiRoutes: ApiRouteDefinition[],
|
|
57
|
+
apiMiddlewares: ReadonlyArray<{ file: string; dir: string }>,
|
|
58
|
+
projectRoot: string,
|
|
59
|
+
): string {
|
|
60
|
+
const loadEntries = apiRoutes
|
|
61
|
+
.map(
|
|
62
|
+
(route) =>
|
|
63
|
+
` ${JSON.stringify(route.path)}: () => import(${JSON.stringify(rootSpecifier(projectRoot, route.file))}),`,
|
|
64
|
+
)
|
|
65
|
+
.join("\n");
|
|
66
|
+
const middlewareEntries = apiMiddlewares
|
|
67
|
+
.map(
|
|
68
|
+
(middleware) =>
|
|
69
|
+
` ${JSON.stringify(middleware.file)}: () => import(${JSON.stringify(rootSpecifier(projectRoot, middleware.file))}),`,
|
|
70
|
+
)
|
|
71
|
+
.join("\n");
|
|
72
|
+
return [
|
|
73
|
+
"// Generated by @moons. Jangan diubah manual.",
|
|
74
|
+
`export const apiRoutes = ${JSON.stringify(apiRoutes, null, 2)};`,
|
|
75
|
+
`export const apiMiddlewares = ${JSON.stringify(apiMiddlewares, null, 2)};`,
|
|
76
|
+
"export const loadApi = {",
|
|
77
|
+
loadEntries,
|
|
78
|
+
"};",
|
|
79
|
+
"export const loadApiMiddleware = {",
|
|
80
|
+
middlewareEntries,
|
|
81
|
+
"};",
|
|
82
|
+
"",
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
45
86
|
/**
|
|
46
87
|
* Isi `virtual:moons/routes`: daftar RouteDefinition, manifest client, dan
|
|
47
88
|
* peta import() dinamis per file route (specifier root-relative).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pemindaian payload SSR (nilai A1 / PRD §2): HANYA dipakai di dev sebagai
|
|
3
|
+
* peringatan — tidak ada biaya runtime di production (production tidak pernah
|
|
4
|
+
* memanggil fungsi ini; lihat `RequestHandlerOptions.payloadScan`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface PayloadScanOptions {
|
|
8
|
+
/** Canary `leakTest.canary` (bila dikonfigurasi). */
|
|
9
|
+
canary?: string;
|
|
10
|
+
/** Lingkungan server (biasanya `process.env`). */
|
|
11
|
+
env?: Record<string, string | undefined>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PayloadScanFinding {
|
|
15
|
+
kind: "canary" | "env";
|
|
16
|
+
/** Label untuk pesan: `canary` atau nama env var. */
|
|
17
|
+
label: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Nama env yang "mirip rahasia" — dideteksi lewat pola nama + panjang nilai
|
|
22
|
+
* minimum 12 karakter untuk menghindari false positive (keputusan pemilik).
|
|
23
|
+
*/
|
|
24
|
+
const SECRET_KEY_PATTERN =
|
|
25
|
+
/(SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|AUTH|API_KEY|ACCESS_KEY|SIGNING)/i;
|
|
26
|
+
|
|
27
|
+
const MIN_SECRET_LENGTH = 12;
|
|
28
|
+
|
|
29
|
+
export function scanPayloadForSecrets(
|
|
30
|
+
payload: string,
|
|
31
|
+
options: PayloadScanOptions = {},
|
|
32
|
+
): PayloadScanFinding[] {
|
|
33
|
+
const findings: PayloadScanFinding[] = [];
|
|
34
|
+
|
|
35
|
+
const canary = options.canary;
|
|
36
|
+
if (canary !== undefined && canary.length > 0 && payload.includes(canary)) {
|
|
37
|
+
findings.push({ kind: "canary", label: "canary" });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(options.env ?? {})) {
|
|
41
|
+
if (typeof value !== "string" || value.length < MIN_SECRET_LENGTH) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!SECRET_KEY_PATTERN.test(key) || /PUBLIC/i.test(key)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (payload.includes(value)) {
|
|
48
|
+
findings.push({ kind: "env", label: key });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return findings;
|
|
53
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -5,13 +5,19 @@ import {
|
|
|
5
5
|
MoonsError,
|
|
6
6
|
fnRegistryKey,
|
|
7
7
|
generateTypes,
|
|
8
|
+
scanApiMiddlewares,
|
|
9
|
+
scanApiRoutes,
|
|
8
10
|
scanRoutes,
|
|
9
11
|
validateConfig,
|
|
12
|
+
type ApiMiddlewareDefinition,
|
|
13
|
+
type ApiRouteDefinition,
|
|
10
14
|
type RouteDefinition,
|
|
11
15
|
} from "@moonsjs/core";
|
|
12
16
|
import { loadConfigFromFile, type Plugin, type ViteDevServer } from "vite";
|
|
17
|
+
import { assertApiModuleExports, assertMiddlewareExports } from "./api";
|
|
13
18
|
import { findLeak } from "./leak-check";
|
|
14
|
-
import {
|
|
19
|
+
import type { PackageJsonInfo } from "./server-only";
|
|
20
|
+
import { renderApiModule, renderRoutesModule, rootSpecifier } from "./manifest";
|
|
15
21
|
import { installDevMiddleware } from "./server/dev";
|
|
16
22
|
import {
|
|
17
23
|
analyzeServerFile,
|
|
@@ -49,7 +55,12 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
49
55
|
let isSsrBuild = false;
|
|
50
56
|
let configFilePath = "";
|
|
51
57
|
let routesDir = "";
|
|
52
|
-
let
|
|
58
|
+
let serverOnlyPackages: ReadonlyArray<string> = [];
|
|
59
|
+
let apiDir = "";
|
|
60
|
+
let apiRoutes: ApiRouteDefinition[] = [];
|
|
61
|
+
let apiMiddlewares: ApiMiddlewareDefinition[] = [];
|
|
62
|
+
/** Cache package.json untuk gate Addendum C (dibagi lintas modul). */
|
|
63
|
+
const packageCache = new Map<string, PackageJsonInfo | null>(); let routes: RouteDefinition[] = [];
|
|
53
64
|
let analysisCache = new Map<string, ServerFileAnalysis>();
|
|
54
65
|
const stubTransformed = new Set<string>();
|
|
55
66
|
|
|
@@ -163,8 +174,18 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
163
174
|
emptyOutDir: true,
|
|
164
175
|
// Entry nyata yang di-generate saat buildStart (Rolldown butuh file,
|
|
165
176
|
// entry virtual tidak di-resolve lewat plugin container).
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
// D.5: dua entry — server (index) + prerender runner.
|
|
178
|
+
ssr: true,
|
|
179
|
+
rollupOptions: {
|
|
180
|
+
input: {
|
|
181
|
+
index: ".moons/server-entry.ts",
|
|
182
|
+
prerender: ".moons/prerender-entry.ts",
|
|
183
|
+
},
|
|
184
|
+
output: {
|
|
185
|
+
entryFileNames: "[name].js",
|
|
186
|
+
chunkFileNames: "chunks/[name]-[hash].js",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
168
189
|
},
|
|
169
190
|
ssr: { noExternal: true, target: "node" as const },
|
|
170
191
|
};
|
|
@@ -223,10 +244,31 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
223
244
|
|
|
224
245
|
const config = validateConfig(raw, { file: configFilePath, projectRoot: root });
|
|
225
246
|
routesDir = resolvePath(root, config.routesDir);
|
|
247
|
+
serverOnlyPackages = config.security?.serverOnlyPackages ?? [];
|
|
248
|
+
|
|
249
|
+
// Addendum D.1: scan + validasi statis modul `src/api`.
|
|
250
|
+
apiDir = resolvePath(root, config.apiDir ?? "src/api");
|
|
251
|
+
if (existsSync(apiDir)) {
|
|
252
|
+
apiRoutes = scanApiRoutes(apiDir);
|
|
253
|
+
for (const route of apiRoutes) {
|
|
254
|
+
assertApiModuleExports(readFileSync(route.file, "utf8"), route.file);
|
|
255
|
+
}
|
|
256
|
+
apiMiddlewares = scanApiMiddlewares(apiDir);
|
|
257
|
+
for (const middleware of apiMiddlewares) {
|
|
258
|
+
assertMiddlewareExports(readFileSync(middleware.file, "utf8"), middleware.file);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
apiRoutes = [];
|
|
262
|
+
apiMiddlewares = [];
|
|
263
|
+
}
|
|
226
264
|
},
|
|
227
265
|
|
|
228
266
|
async buildStart() {
|
|
229
267
|
routes = scanRoutes(routesDir);
|
|
268
|
+
// D.3: validasi statis `_middleware.ts` pohon routes.
|
|
269
|
+
for (const middleware of routes.filter((route) => route.kind === "middleware")) {
|
|
270
|
+
assertMiddlewareExports(readFileSync(middleware.file, "utf8"), middleware.file);
|
|
271
|
+
}
|
|
230
272
|
await generateTypes({
|
|
231
273
|
routesDir,
|
|
232
274
|
configPath: configFilePath,
|
|
@@ -239,6 +281,11 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
239
281
|
renderServerEntryBuild(routesDir, serverFnFileSpecifiers()),
|
|
240
282
|
"utf8",
|
|
241
283
|
);
|
|
284
|
+
writeFileSync(
|
|
285
|
+
resolvePath(root, ".moons/prerender-entry.ts"),
|
|
286
|
+
renderPrerenderEntry(routesDir, root, options.leakTest?.canary),
|
|
287
|
+
"utf8",
|
|
288
|
+
);
|
|
242
289
|
}
|
|
243
290
|
},
|
|
244
291
|
|
|
@@ -258,6 +305,8 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
258
305
|
switch (name) {
|
|
259
306
|
case VIRTUAL_MODULES.routes:
|
|
260
307
|
return renderRoutesModule(routes, routesDir, root);
|
|
308
|
+
case VIRTUAL_MODULES.api:
|
|
309
|
+
return renderApiModule(apiRoutes, apiMiddlewares, root);
|
|
261
310
|
case VIRTUAL_MODULES.clientEntry:
|
|
262
311
|
return '// Generated by @moons. Jangan diubah manual.\nexport { clientManifest, loadRoute } from "virtual:moons/routes";\n';
|
|
263
312
|
case VIRTUAL_MODULES.config:
|
|
@@ -316,6 +365,9 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
316
365
|
}
|
|
317
366
|
},
|
|
318
367
|
analyze: (absoluteFile) => analysisCache.get(absoluteFile) ?? analyzeFile(absoluteFile),
|
|
368
|
+
apiDir,
|
|
369
|
+
serverOnlyPackages,
|
|
370
|
+
packageCache,
|
|
319
371
|
});
|
|
320
372
|
}
|
|
321
373
|
|
|
@@ -369,7 +421,18 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
369
421
|
},
|
|
370
422
|
|
|
371
423
|
configureServer(server) {
|
|
372
|
-
installDevMiddleware(server, {
|
|
424
|
+
installDevMiddleware(server, {
|
|
425
|
+
templatePath: resolvePath(root, "index.html"),
|
|
426
|
+
// A1: pemindaian payload SSR hanya di dev (warning), tanpa biaya di production.
|
|
427
|
+
payloadScan: {
|
|
428
|
+
...(options.leakTest?.canary === undefined ? {} : { canary: options.leakTest.canary }),
|
|
429
|
+
env: process.env,
|
|
430
|
+
},
|
|
431
|
+
// D.1: request ke prefix API (semua method) dirutekan ke runtime.
|
|
432
|
+
...(apiRoutes.length === 0
|
|
433
|
+
? {}
|
|
434
|
+
: { apiPrefix: `/${apiDir.split(/[\\/]/).filter((part) => part.length > 0).pop() ?? "api"}` }),
|
|
435
|
+
});
|
|
373
436
|
},
|
|
374
437
|
|
|
375
438
|
async handleHotUpdate(context) {
|
|
@@ -379,12 +442,34 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
379
442
|
return;
|
|
380
443
|
}
|
|
381
444
|
|
|
445
|
+
if (context.file.startsWith(apiDir) && existsSync(apiDir)) {
|
|
446
|
+
try {
|
|
447
|
+
apiRoutes = scanApiRoutes(apiDir);
|
|
448
|
+
for (const route of apiRoutes) {
|
|
449
|
+
assertApiModuleExports(readFileSync(route.file, "utf8"), route.file);
|
|
450
|
+
}
|
|
451
|
+
apiMiddlewares = scanApiMiddlewares(apiDir);
|
|
452
|
+
for (const middleware of apiMiddlewares) {
|
|
453
|
+
assertMiddlewareExports(readFileSync(middleware.file, "utf8"), middleware.file);
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
context.server.config.logger.error(String(error));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
invalidateVirtualModules(context.server);
|
|
460
|
+
context.server.ws.send({ type: "full-reload" });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
382
464
|
if (!context.file.startsWith(routesDir)) {
|
|
383
465
|
return;
|
|
384
466
|
}
|
|
385
467
|
|
|
386
468
|
try {
|
|
387
469
|
routes = scanRoutes(routesDir);
|
|
470
|
+
for (const middleware of routes.filter((route) => route.kind === "middleware")) {
|
|
471
|
+
assertMiddlewareExports(readFileSync(middleware.file, "utf8"), middleware.file);
|
|
472
|
+
}
|
|
388
473
|
await generateTypes({
|
|
389
474
|
routesDir,
|
|
390
475
|
configPath: configFilePath,
|
|
@@ -414,13 +499,16 @@ function renderConfigModule(root: string, configFilePath: string): string {
|
|
|
414
499
|
function renderServerEntryDev(routesDir: string, eagerImports: string[]): string {
|
|
415
500
|
return [
|
|
416
501
|
"// Generated by @moons. Jangan diubah manual.",
|
|
417
|
-
'import { createRequestHandler } from "@moonsjs/vite/server";',
|
|
502
|
+
'import { createPluginRuntime, createRequestHandler } from "@moonsjs/vite/server";',
|
|
418
503
|
'import config from "virtual:moons/config";',
|
|
419
504
|
'import { routes, loadRoute } from "virtual:moons/routes";',
|
|
505
|
+
'import { apiRoutes, apiMiddlewares, loadApi, loadApiMiddleware } from "virtual:moons/api";',
|
|
420
506
|
...eagerImports.map((specifier) => `import ${JSON.stringify(specifier)};`),
|
|
421
507
|
"",
|
|
422
|
-
"export function
|
|
423
|
-
|
|
508
|
+
"export function createDevApp(shell, payloadScan) {",
|
|
509
|
+
" const plugins = createPluginRuntime({ config, routes, middlewares: config.middleware ?? [] });",
|
|
510
|
+
` const handler = createRequestHandler({ routes, apiRoutes, apiMiddlewares, loadApi, loadApiMiddleware, config, routesDir: ${JSON.stringify(routesDir)}, loadRoute, shell, onContext: (ctx) => plugins.applyExtensions(ctx), ...(payloadScan === undefined ? {} : { payloadScan }) });`,
|
|
511
|
+
" return { handler, plugins };",
|
|
424
512
|
"}",
|
|
425
513
|
"",
|
|
426
514
|
].join("\n");
|
|
@@ -429,20 +517,27 @@ function renderServerEntryDev(routesDir: string, eagerImports: string[]): string
|
|
|
429
517
|
function renderServerEntryBuild(routesDir: string, eagerImports: string[]): string {
|
|
430
518
|
return [
|
|
431
519
|
"// Generated by @moons. Jangan diubah manual.",
|
|
432
|
-
'import { createFileShell, createRequestHandler, startNodeServer } from "@moonsjs/vite/server";',
|
|
520
|
+
'import { createFileShell, createPluginRuntime, createRequestHandler, startNodeServer } from "@moonsjs/vite/server";',
|
|
433
521
|
'import config from "virtual:moons/config";',
|
|
434
522
|
'import { routes, loadRoute } from "virtual:moons/routes";',
|
|
523
|
+
'import { apiRoutes, apiMiddlewares, loadApi, loadApiMiddleware } from "virtual:moons/api";',
|
|
435
524
|
...eagerImports.map((specifier) => `import ${JSON.stringify(specifier)};`),
|
|
436
525
|
"",
|
|
526
|
+
"const plugins = createPluginRuntime({ config, routes, middlewares: config.middleware ?? [] });",
|
|
437
527
|
"const handler = createRequestHandler({",
|
|
438
528
|
" routes,",
|
|
529
|
+
" apiRoutes,",
|
|
530
|
+
" apiMiddlewares,",
|
|
531
|
+
" loadApi,",
|
|
532
|
+
" loadApiMiddleware,",
|
|
439
533
|
" config,",
|
|
440
534
|
` routesDir: ${JSON.stringify(routesDir)},`,
|
|
441
535
|
" loadRoute,",
|
|
442
536
|
" shell: createFileShell(),",
|
|
537
|
+
" onContext: (ctx) => plugins.applyExtensions(ctx),",
|
|
443
538
|
"});",
|
|
444
539
|
"",
|
|
445
|
-
"startNodeServer(handler);",
|
|
540
|
+
"startNodeServer(handler, { plugins });",
|
|
446
541
|
"",
|
|
447
542
|
].join("\n");
|
|
448
543
|
}
|
|
@@ -452,6 +547,38 @@ interface ModuleGraphLike {
|
|
|
452
547
|
invalidateModule(module: unknown): void;
|
|
453
548
|
}
|
|
454
549
|
|
|
550
|
+
/** Entry D.5: runner prerender (dijalankan CLI setelah build server). */
|
|
551
|
+
function renderPrerenderEntry(
|
|
552
|
+
routesDir: string,
|
|
553
|
+
projectRoot: string,
|
|
554
|
+
canary: string | undefined,
|
|
555
|
+
): string {
|
|
556
|
+
return [
|
|
557
|
+
"// Generated by @moons. Jangan diubah manual.",
|
|
558
|
+
'import { createFileShell, createRequestHandler, runPrerender } from "@moonsjs/vite/server";',
|
|
559
|
+
'import config from "virtual:moons/config";',
|
|
560
|
+
'import { routes, loadRoute } from "virtual:moons/routes";',
|
|
561
|
+
"",
|
|
562
|
+
"const handler = createRequestHandler({",
|
|
563
|
+
" routes,",
|
|
564
|
+
" config,",
|
|
565
|
+
` routesDir: ${JSON.stringify(routesDir)},`,
|
|
566
|
+
" loadRoute,",
|
|
567
|
+
" shell: createFileShell(),",
|
|
568
|
+
"});",
|
|
569
|
+
"",
|
|
570
|
+
"await runPrerender({",
|
|
571
|
+
" routes,",
|
|
572
|
+
` routesDir: ${JSON.stringify(routesDir)},`,
|
|
573
|
+
" loadRoute,",
|
|
574
|
+
" handler,",
|
|
575
|
+
` clientDir: ${JSON.stringify(resolvePath(projectRoot, "dist/client"))},`,
|
|
576
|
+
...(canary === undefined ? [] : [` canary: ${JSON.stringify(canary)},`]),
|
|
577
|
+
"});",
|
|
578
|
+
"",
|
|
579
|
+
].join("\n");
|
|
580
|
+
}
|
|
581
|
+
|
|
455
582
|
function environmentName(context: unknown): string {
|
|
456
583
|
const environment = (context as { environment?: { name?: string } } | undefined)?.environment;
|
|
457
584
|
return environment?.name ?? "client";
|
package/src/server/dev.ts
CHANGED
|
@@ -1,43 +1,74 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { ViteDevServer } from "vite";
|
|
3
3
|
import { VIRTUAL_MODULES } from "../virtual-modules";
|
|
4
|
+
import type { PayloadScanOptions } from "../payload-scan";
|
|
5
|
+
import type { PluginRuntime } from "./plugins";
|
|
4
6
|
import type { Shell } from "./shell";
|
|
5
7
|
import { createDevShell } from "./shell";
|
|
6
8
|
import { sendNodeResponse, toWebRequest } from "./node";
|
|
7
9
|
|
|
10
|
+
interface DevApp {
|
|
11
|
+
handler: (request: Request) => Promise<Response>;
|
|
12
|
+
plugins: PluginRuntime;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
type DevServerEntry = {
|
|
9
|
-
|
|
16
|
+
createDevApp: (
|
|
17
|
+
shell: Shell,
|
|
18
|
+
payloadScan?: import("../payload-scan").PayloadScanOptions,
|
|
19
|
+
) => DevApp;
|
|
10
20
|
};
|
|
11
21
|
|
|
12
22
|
/**
|
|
13
23
|
* Middleware SSR dev: dipasang sebelum middleware internal Vite, menangani
|
|
14
24
|
* request HTML dan RPC serverFn (`/_moons/fn/*`) — modul/aset tetap Vite.
|
|
25
|
+
* Handler + lifecycle plugin (Addendum B.4) diinisialisasi sekali.
|
|
15
26
|
*/
|
|
16
27
|
export function installDevMiddleware(
|
|
17
28
|
server: ViteDevServer,
|
|
18
|
-
options: { templatePath: string },
|
|
29
|
+
options: { templatePath: string; payloadScan?: PayloadScanOptions; apiPrefix?: string },
|
|
19
30
|
): void {
|
|
31
|
+
let appPromise: Promise<DevApp> | null = null;
|
|
32
|
+
|
|
33
|
+
const getApp = (): Promise<DevApp> => {
|
|
34
|
+
if (appPromise === null) {
|
|
35
|
+
appPromise = (async () => {
|
|
36
|
+
const entry = (await server.ssrLoadModule(VIRTUAL_MODULES.serverEntry)) as DevServerEntry;
|
|
37
|
+
const app = entry.createDevApp(createDevShell(server, options), options.payloadScan);
|
|
38
|
+
await app.plugins.start();
|
|
39
|
+
await app.plugins.ready();
|
|
40
|
+
return app;
|
|
41
|
+
})();
|
|
42
|
+
}
|
|
43
|
+
return appPromise;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
server.httpServer?.once("close", () => {
|
|
47
|
+
void appPromise?.then((app) => app.plugins.shutdown());
|
|
48
|
+
});
|
|
49
|
+
|
|
20
50
|
server.middlewares.use((req, res, next) => {
|
|
21
|
-
if (!shouldHandle(req)) {
|
|
51
|
+
if (!shouldHandle(req, options.apiPrefix)) {
|
|
22
52
|
next();
|
|
23
53
|
return;
|
|
24
54
|
}
|
|
25
|
-
void handleRequest(
|
|
55
|
+
void handleRequest(getApp, req, res).catch((error: unknown) => {
|
|
26
56
|
next(error as Error);
|
|
27
57
|
});
|
|
28
58
|
});
|
|
29
59
|
}
|
|
30
60
|
|
|
31
|
-
function handleRequest(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
async function handleRequest(
|
|
62
|
+
getApp: () => Promise<DevApp>,
|
|
63
|
+
req: IncomingMessage,
|
|
64
|
+
res: ServerResponse,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const app = await getApp();
|
|
67
|
+
const response = await app.handler(await toWebRequest(req));
|
|
68
|
+
await sendNodeResponse(res, response);
|
|
38
69
|
}
|
|
39
70
|
|
|
40
|
-
function shouldHandle(req: IncomingMessage): boolean {
|
|
71
|
+
function shouldHandle(req: IncomingMessage, apiPrefix: string | undefined): boolean {
|
|
41
72
|
const path = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
42
73
|
|
|
43
74
|
// RPC serverFn (semua method): dulu hanya GET/HEAD HTML yang dirutekan,
|
|
@@ -47,6 +78,11 @@ function shouldHandle(req: IncomingMessage): boolean {
|
|
|
47
78
|
return true;
|
|
48
79
|
}
|
|
49
80
|
|
|
81
|
+
// Addendum D.1: endpoint `src/api` (semua method) dirutekan ke runtime.
|
|
82
|
+
if (apiPrefix !== undefined && (path === apiPrefix || path.startsWith(`${apiPrefix}/`))) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
51
87
|
return false;
|
|
52
88
|
}
|