@moonsjs/vite 0.1.15 → 0.1.17
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/{index-vafk4b9b.js → index-v5sbd3az.js} +2 -2
- package/dist/{index-vafk4b9b.js.map → index-v5sbd3az.js.map} +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +3 -3
- package/dist/server/handler.d.ts.map +1 -1
- package/dist/server/index.js +109 -6
- package/dist/server/index.js.map +6 -5
- package/dist/server/logger.d.ts +25 -0
- package/dist/server/logger.d.ts.map +1 -0
- package/dist/server/node-server.d.ts +8 -0
- package/dist/server/node-server.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/plugin.ts +1 -1
- package/src/server/handler.ts +14 -0
- package/src/server/logger.ts +69 -0
- package/src/server/node-server.ts +68 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Addendum L.3: logger akses terstruktur — JSON default, redaksi nilai
|
|
3
|
+
* query-param (mis. `?token=…`), dan target tulis injectable (untuk test).
|
|
4
|
+
* Semua field tambahan diteruskan apa adanya; `path` selalu di-redaksi.
|
|
5
|
+
*/
|
|
6
|
+
export interface LogEvent {
|
|
7
|
+
/** Default `info`. */
|
|
8
|
+
level?: "info" | "warn" | "error";
|
|
9
|
+
/** Nama event, mis. `request` atau `isr:regenerated`. */
|
|
10
|
+
msg: string;
|
|
11
|
+
[field: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LoggerOptions {
|
|
15
|
+
/** Default `true` (JSON). `false` = teks ringkas. */
|
|
16
|
+
json?: boolean;
|
|
17
|
+
/** Nama query-param yang nilainya diganti `[redacted]`. */
|
|
18
|
+
redact?: readonly string[];
|
|
19
|
+
/** Tujuan tulis; default `console.log`. */
|
|
20
|
+
write?: (line: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type Logger = (event: LogEvent) => void;
|
|
24
|
+
|
|
25
|
+
/** Ganti nilai query-param bernama `redact` menjadi `[redacted]`. */
|
|
26
|
+
export function redactPath(path: string, redact: readonly string[]): string {
|
|
27
|
+
if (redact.length === 0 || !path.includes("?")) {
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
const index = path.indexOf("?");
|
|
31
|
+
const pathname = path.slice(0, index);
|
|
32
|
+
const params = new URLSearchParams(path.slice(index + 1));
|
|
33
|
+
let changed = false;
|
|
34
|
+
for (const name of redact) {
|
|
35
|
+
if (params.has(name)) {
|
|
36
|
+
params.set(name, "[redacted]");
|
|
37
|
+
changed = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return changed ? `${pathname}?${params.toString()}` : path;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createLogger(options: LoggerOptions = {}): Logger {
|
|
44
|
+
const json = options.json !== false;
|
|
45
|
+
const redact = options.redact ?? [];
|
|
46
|
+
const write = options.write ?? ((line: string): void => console.log(line));
|
|
47
|
+
|
|
48
|
+
return (event) => {
|
|
49
|
+
const fields: Record<string, unknown> = { level: "info", ...event };
|
|
50
|
+
if (typeof fields["path"] === "string") {
|
|
51
|
+
fields["path"] = redactPath(fields["path"], redact);
|
|
52
|
+
}
|
|
53
|
+
if (json) {
|
|
54
|
+
write(JSON.stringify({ time: new Date().toISOString(), ...fields }));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const parts = ["[moons]", String(fields["level"]).toUpperCase(), String(fields["msg"])];
|
|
58
|
+
if (fields["path"] !== undefined) {
|
|
59
|
+
parts.push(String(fields["path"]));
|
|
60
|
+
}
|
|
61
|
+
if (fields["status"] !== undefined) {
|
|
62
|
+
parts.push(String(fields["status"]));
|
|
63
|
+
}
|
|
64
|
+
if (fields["durationMs"] !== undefined) {
|
|
65
|
+
parts.push(`${String(fields["durationMs"])}ms`);
|
|
66
|
+
}
|
|
67
|
+
write(parts.join(" "));
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -2,17 +2,24 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { gzipSync } from "node:zlib";
|
|
3
3
|
import { createServer, type Server } from "node:http";
|
|
4
4
|
import { extname, join, resolve, sep } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_BODY_LIMIT_BYTES,
|
|
7
|
+
MoonsError,
|
|
8
|
+
trailingSlashRedirect,
|
|
9
|
+
type PrerenderStorage,
|
|
10
|
+
} from "@moonsjs/core";
|
|
6
11
|
import { revalidatePrerendered, shouldRevalidate } from "./isr";
|
|
7
12
|
import { contentTypeForPathname } from "./storage";
|
|
8
13
|
import {
|
|
9
14
|
BodyLimitError,
|
|
10
15
|
DEFAULT_SECURITY_HEADERS,
|
|
11
16
|
InvalidRequestError,
|
|
17
|
+
resolveClientIp,
|
|
12
18
|
resolveRequestUrl,
|
|
13
19
|
sendNodeResponse,
|
|
14
20
|
toWebRequest,
|
|
15
21
|
} from "./node";
|
|
22
|
+
import { createLogger, type Logger } from "./logger";
|
|
16
23
|
import type { PluginRuntime } from "./plugins";
|
|
17
24
|
import { installGracefulShutdown } from "./shutdown";
|
|
18
25
|
|
|
@@ -64,6 +71,10 @@ export interface NodeServerOptions {
|
|
|
64
71
|
trustProxy?: boolean;
|
|
65
72
|
/** Addendum J.2: batas body RPC di transport (default `DEFAULT_BODY_LIMIT_BYTES`). */
|
|
66
73
|
bodyLimitBytes?: number;
|
|
74
|
+
/** Addendum L.3: log akses terstruktur (default senyap). */
|
|
75
|
+
logging?: { accessLog?: boolean; json?: boolean; redact?: string[] };
|
|
76
|
+
/** Addendum M: kebijakan trailing slash (juga untuk path prerender/static). */
|
|
77
|
+
trailingSlash?: "never" | "always";
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
/**
|
|
@@ -77,7 +88,29 @@ export function startNodeServer(
|
|
|
77
88
|
const clientDir = resolve(options.clientDir ?? join(process.cwd(), "dist/client"));
|
|
78
89
|
const port = options.port ?? Number(process.env["PORT"] ?? 3000);
|
|
79
90
|
|
|
91
|
+
const accessLogger: Logger | null =
|
|
92
|
+
options.logging?.accessLog === true
|
|
93
|
+
? createLogger({
|
|
94
|
+
json: options.logging.json !== false,
|
|
95
|
+
redact: options.logging.redact ?? [],
|
|
96
|
+
})
|
|
97
|
+
: null;
|
|
98
|
+
|
|
80
99
|
const server = createServer((req, res) => {
|
|
100
|
+
const requestStartedAt = performance.now();
|
|
101
|
+
if (accessLogger !== null) {
|
|
102
|
+
res.once("finish", () => {
|
|
103
|
+
const ip = resolveClientIp(req, options.trustProxy === true);
|
|
104
|
+
accessLogger({
|
|
105
|
+
msg: "request",
|
|
106
|
+
method: req.method ?? "GET",
|
|
107
|
+
path: req.url ?? "/",
|
|
108
|
+
status: res.statusCode,
|
|
109
|
+
durationMs: Math.round(performance.now() - requestStartedAt),
|
|
110
|
+
...(ip === null ? {} : { ip }),
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
81
114
|
void (async () => {
|
|
82
115
|
let url: URL;
|
|
83
116
|
try {
|
|
@@ -94,6 +127,19 @@ export function startNodeServer(
|
|
|
94
127
|
}
|
|
95
128
|
|
|
96
129
|
try {
|
|
130
|
+
// Addendum M: kanonikalisasi trailing slash di transport — mencakup
|
|
131
|
+
// path prerender yang dilayani langsung (tidak lewat handler).
|
|
132
|
+
if (options.trailingSlash !== undefined) {
|
|
133
|
+
const canonical = trailingSlashRedirect(url.pathname, options.trailingSlash);
|
|
134
|
+
if (canonical !== null) {
|
|
135
|
+
url.pathname = canonical;
|
|
136
|
+
res.statusCode = 308;
|
|
137
|
+
res.setHeader("location", url.toString());
|
|
138
|
+
res.end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
97
143
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
98
144
|
// Addendum I: storage bersama (bila dikonfigurasi) diutamakan.
|
|
99
145
|
if (options.storage !== undefined) {
|
|
@@ -106,6 +152,7 @@ export function startNodeServer(
|
|
|
106
152
|
handler,
|
|
107
153
|
options,
|
|
108
154
|
clientDir,
|
|
155
|
+
logger: accessLogger,
|
|
109
156
|
});
|
|
110
157
|
res.statusCode = 200;
|
|
111
158
|
res.setHeader("content-type", contentTypeForPathname(url.pathname));
|
|
@@ -129,6 +176,7 @@ export function startNodeServer(
|
|
|
129
176
|
handler,
|
|
130
177
|
options,
|
|
131
178
|
clientDir,
|
|
179
|
+
logger: accessLogger,
|
|
132
180
|
});
|
|
133
181
|
const body = readFileSync(staticFile);
|
|
134
182
|
const stat = statSync(staticFile);
|
|
@@ -444,6 +492,7 @@ function scheduleRevalidation(input: {
|
|
|
444
492
|
handler: (request: Request) => Promise<Response>;
|
|
445
493
|
options: NodeServerOptions;
|
|
446
494
|
clientDir: string;
|
|
495
|
+
logger?: Logger | null;
|
|
447
496
|
}): void {
|
|
448
497
|
const normalized = input.pathname.length > 1 ? input.pathname.replace(/\/+$/, "") : input.pathname;
|
|
449
498
|
const seconds = readPrerenderManifest(input.clientDir).revalidate.get(normalized);
|
|
@@ -453,6 +502,8 @@ function scheduleRevalidation(input: {
|
|
|
453
502
|
if (!shouldRevalidate({ mtimeMs: input.mtimeMs, revalidateSeconds: seconds })) {
|
|
454
503
|
return;
|
|
455
504
|
}
|
|
505
|
+
const regeneratedAt = performance.now();
|
|
506
|
+
const logger = input.logger ?? null;
|
|
456
507
|
void revalidatePrerendered({
|
|
457
508
|
pathname: normalized,
|
|
458
509
|
...(input.file === undefined ? {} : { file: input.file }),
|
|
@@ -460,6 +511,21 @@ function scheduleRevalidation(input: {
|
|
|
460
511
|
handler: input.handler,
|
|
461
512
|
...(input.options.canary === undefined ? {} : { canary: input.options.canary }),
|
|
462
513
|
env: process.env,
|
|
463
|
-
})
|
|
514
|
+
})
|
|
515
|
+
.then(() => {
|
|
516
|
+
logger?.({
|
|
517
|
+
msg: "isr:regenerated",
|
|
518
|
+
path: normalized,
|
|
519
|
+
durationMs: Math.round(performance.now() - regeneratedAt),
|
|
520
|
+
});
|
|
521
|
+
})
|
|
522
|
+
.catch((error: unknown) => {
|
|
523
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
524
|
+
if (logger !== null) {
|
|
525
|
+
logger({ msg: "isr:revalidate-failed", level: "warn", path: normalized, error: message });
|
|
526
|
+
} else {
|
|
527
|
+
console.error(`[moons] isr regenerasi gagal (${normalized}): ${message}`);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
464
530
|
}
|
|
465
531
|
|