@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.
@@ -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 { DEFAULT_BODY_LIMIT_BYTES, MoonsError, type PrerenderStorage } from "@moonsjs/core";
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