@mandujs/core 0.9.2 β 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +200 -200
- package/README.md +200 -200
- package/package.json +52 -52
- package/src/bundler/build.ts +6 -0
- package/src/client/Link.tsx +209 -209
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +2 -1
- package/src/client/router.ts +387 -387
- package/src/client/serialize.ts +404 -404
- package/src/contract/client.test.ts +308 -0
- package/src/contract/client.ts +345 -0
- package/src/contract/handler.ts +270 -0
- package/src/contract/index.ts +137 -1
- package/src/contract/infer.test.ts +346 -0
- package/src/contract/types.ts +83 -0
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +438 -438
- package/src/filling/filling.ts +5 -1
- package/src/filling/index.ts +1 -1
- package/src/generator/index.ts +3 -3
- package/src/index.ts +75 -0
- package/src/report/index.ts +1 -1
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/index.ts +3 -3
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/ssr.ts +321 -321
- package/src/runtime/trace.ts +144 -144
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
package/src/runtime/trace.ts
CHANGED
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Trace π§
|
|
3
|
-
* Lifecycle λ¨κ³λ³ μΆμ (μ΅μ
)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ManduContext } from "../filling/context";
|
|
7
|
-
|
|
8
|
-
export type TraceEvent =
|
|
9
|
-
| "request"
|
|
10
|
-
| "parse"
|
|
11
|
-
| "transform"
|
|
12
|
-
| "beforeHandle"
|
|
13
|
-
| "handle"
|
|
14
|
-
| "afterHandle"
|
|
15
|
-
| "mapResponse"
|
|
16
|
-
| "afterResponse"
|
|
17
|
-
| "error";
|
|
18
|
-
|
|
19
|
-
export type TracePhase = "begin" | "end" | "error";
|
|
20
|
-
|
|
21
|
-
export interface TraceEntry {
|
|
22
|
-
event: TraceEvent;
|
|
23
|
-
phase: TracePhase;
|
|
24
|
-
time: number;
|
|
25
|
-
name?: string;
|
|
26
|
-
error?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface TraceCollector {
|
|
30
|
-
records: TraceEntry[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface TraceReportEntry {
|
|
34
|
-
event: TraceEvent;
|
|
35
|
-
name?: string;
|
|
36
|
-
start: number;
|
|
37
|
-
end: number;
|
|
38
|
-
duration: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface TraceReport {
|
|
42
|
-
entries: TraceReportEntry[];
|
|
43
|
-
errors: TraceEntry[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const TRACE_KEY = "__mandu_trace";
|
|
47
|
-
|
|
48
|
-
const now = (): number => {
|
|
49
|
-
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
50
|
-
return performance.now();
|
|
51
|
-
}
|
|
52
|
-
return Date.now();
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export function enableTrace(ctx: ManduContext): TraceCollector {
|
|
56
|
-
const existing = ctx.get<TraceCollector>(TRACE_KEY);
|
|
57
|
-
if (existing) return existing;
|
|
58
|
-
const collector: TraceCollector = { records: [] };
|
|
59
|
-
ctx.set(TRACE_KEY, collector);
|
|
60
|
-
return collector;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function getTrace(ctx: ManduContext): TraceCollector | undefined {
|
|
64
|
-
return ctx.get<TraceCollector>(TRACE_KEY);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Build a normalized trace report with durations
|
|
69
|
-
*/
|
|
70
|
-
export function buildTraceReport(collector: TraceCollector): TraceReport {
|
|
71
|
-
const entries: TraceReportEntry[] = [];
|
|
72
|
-
const errors: TraceEntry[] = [];
|
|
73
|
-
const stacks = new Map<string, TraceEntry[]>();
|
|
74
|
-
|
|
75
|
-
for (const record of collector.records) {
|
|
76
|
-
if (record.phase === "error") {
|
|
77
|
-
errors.push(record);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const key = `${record.event}:${record.name ?? ""}`;
|
|
82
|
-
if (record.phase === "begin") {
|
|
83
|
-
const stack = stacks.get(key) ?? [];
|
|
84
|
-
stack.push(record);
|
|
85
|
-
stacks.set(key, stack);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (record.phase === "end") {
|
|
90
|
-
const stack = stacks.get(key);
|
|
91
|
-
const begin = stack?.pop();
|
|
92
|
-
if (!begin) continue;
|
|
93
|
-
entries.push({
|
|
94
|
-
event: record.event,
|
|
95
|
-
name: record.name,
|
|
96
|
-
start: begin.time,
|
|
97
|
-
end: record.time,
|
|
98
|
-
duration: record.time - begin.time,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return { entries, errors };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Convert trace report to JSON string
|
|
108
|
-
*/
|
|
109
|
-
export function formatTraceReport(report: TraceReport): string {
|
|
110
|
-
return JSON.stringify(report, null, 2);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface Tracer {
|
|
114
|
-
enabled: boolean;
|
|
115
|
-
begin: (event: TraceEvent, name?: string) => () => void;
|
|
116
|
-
error: (event: TraceEvent, err: unknown, name?: string) => void;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const NOOP_TRACER: Tracer = {
|
|
120
|
-
enabled: false,
|
|
121
|
-
begin: () => () => {},
|
|
122
|
-
error: () => {},
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
export function createTracer(ctx: ManduContext, enabled?: boolean): Tracer {
|
|
126
|
-
const shouldEnable = Boolean(enabled) || ctx.has(TRACE_KEY);
|
|
127
|
-
if (!shouldEnable) return NOOP_TRACER;
|
|
128
|
-
|
|
129
|
-
const collector = enableTrace(ctx);
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
enabled: true,
|
|
133
|
-
begin: (event, name) => {
|
|
134
|
-
collector.records.push({ event, phase: "begin", time: now(), name });
|
|
135
|
-
return () => {
|
|
136
|
-
collector.records.push({ event, phase: "end", time: now(), name });
|
|
137
|
-
};
|
|
138
|
-
},
|
|
139
|
-
error: (event, err, name) => {
|
|
140
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
-
collector.records.push({ event, phase: "error", time: now(), name, error: message });
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Trace π§
|
|
3
|
+
* Lifecycle λ¨κ³λ³ μΆμ (μ΅μ
)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ManduContext } from "../filling/context";
|
|
7
|
+
|
|
8
|
+
export type TraceEvent =
|
|
9
|
+
| "request"
|
|
10
|
+
| "parse"
|
|
11
|
+
| "transform"
|
|
12
|
+
| "beforeHandle"
|
|
13
|
+
| "handle"
|
|
14
|
+
| "afterHandle"
|
|
15
|
+
| "mapResponse"
|
|
16
|
+
| "afterResponse"
|
|
17
|
+
| "error";
|
|
18
|
+
|
|
19
|
+
export type TracePhase = "begin" | "end" | "error";
|
|
20
|
+
|
|
21
|
+
export interface TraceEntry {
|
|
22
|
+
event: TraceEvent;
|
|
23
|
+
phase: TracePhase;
|
|
24
|
+
time: number;
|
|
25
|
+
name?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TraceCollector {
|
|
30
|
+
records: TraceEntry[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TraceReportEntry {
|
|
34
|
+
event: TraceEvent;
|
|
35
|
+
name?: string;
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
duration: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TraceReport {
|
|
42
|
+
entries: TraceReportEntry[];
|
|
43
|
+
errors: TraceEntry[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const TRACE_KEY = "__mandu_trace";
|
|
47
|
+
|
|
48
|
+
const now = (): number => {
|
|
49
|
+
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
50
|
+
return performance.now();
|
|
51
|
+
}
|
|
52
|
+
return Date.now();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function enableTrace(ctx: ManduContext): TraceCollector {
|
|
56
|
+
const existing = ctx.get<TraceCollector>(TRACE_KEY);
|
|
57
|
+
if (existing) return existing;
|
|
58
|
+
const collector: TraceCollector = { records: [] };
|
|
59
|
+
ctx.set(TRACE_KEY, collector);
|
|
60
|
+
return collector;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getTrace(ctx: ManduContext): TraceCollector | undefined {
|
|
64
|
+
return ctx.get<TraceCollector>(TRACE_KEY);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a normalized trace report with durations
|
|
69
|
+
*/
|
|
70
|
+
export function buildTraceReport(collector: TraceCollector): TraceReport {
|
|
71
|
+
const entries: TraceReportEntry[] = [];
|
|
72
|
+
const errors: TraceEntry[] = [];
|
|
73
|
+
const stacks = new Map<string, TraceEntry[]>();
|
|
74
|
+
|
|
75
|
+
for (const record of collector.records) {
|
|
76
|
+
if (record.phase === "error") {
|
|
77
|
+
errors.push(record);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const key = `${record.event}:${record.name ?? ""}`;
|
|
82
|
+
if (record.phase === "begin") {
|
|
83
|
+
const stack = stacks.get(key) ?? [];
|
|
84
|
+
stack.push(record);
|
|
85
|
+
stacks.set(key, stack);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (record.phase === "end") {
|
|
90
|
+
const stack = stacks.get(key);
|
|
91
|
+
const begin = stack?.pop();
|
|
92
|
+
if (!begin) continue;
|
|
93
|
+
entries.push({
|
|
94
|
+
event: record.event,
|
|
95
|
+
name: record.name,
|
|
96
|
+
start: begin.time,
|
|
97
|
+
end: record.time,
|
|
98
|
+
duration: record.time - begin.time,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { entries, errors };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert trace report to JSON string
|
|
108
|
+
*/
|
|
109
|
+
export function formatTraceReport(report: TraceReport): string {
|
|
110
|
+
return JSON.stringify(report, null, 2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface Tracer {
|
|
114
|
+
enabled: boolean;
|
|
115
|
+
begin: (event: TraceEvent, name?: string) => () => void;
|
|
116
|
+
error: (event: TraceEvent, err: unknown, name?: string) => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const NOOP_TRACER: Tracer = {
|
|
120
|
+
enabled: false,
|
|
121
|
+
begin: () => () => {},
|
|
122
|
+
error: () => {},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export function createTracer(ctx: ManduContext, enabled?: boolean): Tracer {
|
|
126
|
+
const shouldEnable = Boolean(enabled) || ctx.has(TRACE_KEY);
|
|
127
|
+
if (!shouldEnable) return NOOP_TRACER;
|
|
128
|
+
|
|
129
|
+
const collector = enableTrace(ctx);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
enabled: true,
|
|
133
|
+
begin: (event, name) => {
|
|
134
|
+
collector.records.push({ event, phase: "begin", time: now(), name });
|
|
135
|
+
return () => {
|
|
136
|
+
collector.records.push({ event, phase: "end", time: now(), name });
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
error: (event, err, name) => {
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
collector.records.push({ event, phase: "error", time: now(), name, error: message });
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
package/src/spec/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./schema";
|
|
2
|
-
export * from "./load";
|
|
3
|
-
export * from "./lock";
|
|
1
|
+
export * from "./schema";
|
|
2
|
+
export * from "./load";
|
|
3
|
+
export * from "./lock";
|
package/src/spec/load.ts
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import { RoutesManifest, type RoutesManifest as RoutesManifestType } from "./schema";
|
|
2
|
-
import { ZodError } from "zod";
|
|
3
|
-
|
|
4
|
-
export interface LoadResult {
|
|
5
|
-
success: boolean;
|
|
6
|
-
data?: RoutesManifestType;
|
|
7
|
-
errors?: string[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function formatZodError(error: ZodError): string[] {
|
|
11
|
-
return error.errors.map((e) => {
|
|
12
|
-
const path = e.path.length > 0 ? `[${e.path.join(".")}] ` : "";
|
|
13
|
-
return `${path}${e.message}`;
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function loadManifest(filePath: string): Promise<LoadResult> {
|
|
18
|
-
try {
|
|
19
|
-
const file = Bun.file(filePath);
|
|
20
|
-
const exists = await file.exists();
|
|
21
|
-
|
|
22
|
-
if (!exists) {
|
|
23
|
-
return {
|
|
24
|
-
success: false,
|
|
25
|
-
errors: [`νμΌμ μ°Ύμ μ μμ΅λλ€: ${filePath}`],
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const content = await file.text();
|
|
30
|
-
let json: unknown;
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
json = JSON.parse(content);
|
|
34
|
-
} catch {
|
|
35
|
-
return {
|
|
36
|
-
success: false,
|
|
37
|
-
errors: ["JSON νμ± μ€ν¨: μ¬λ°λ₯Έ JSON νμμ΄ μλλλ€"],
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const result = RoutesManifest.safeParse(json);
|
|
42
|
-
|
|
43
|
-
if (!result.success) {
|
|
44
|
-
return {
|
|
45
|
-
success: false,
|
|
46
|
-
errors: formatZodError(result.error),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
success: true,
|
|
52
|
-
data: result.data,
|
|
53
|
-
};
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return {
|
|
56
|
-
success: false,
|
|
57
|
-
errors: [`μμμΉ λͺ»ν μ€λ₯: ${error instanceof Error ? error.message : String(error)}`],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function validateManifest(data: unknown): LoadResult {
|
|
63
|
-
const result = RoutesManifest.safeParse(data);
|
|
64
|
-
|
|
65
|
-
if (!result.success) {
|
|
66
|
-
return {
|
|
67
|
-
success: false,
|
|
68
|
-
errors: formatZodError(result.error),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
success: true,
|
|
74
|
-
data: result.data,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
1
|
+
import { RoutesManifest, type RoutesManifest as RoutesManifestType } from "./schema";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
|
|
4
|
+
export interface LoadResult {
|
|
5
|
+
success: boolean;
|
|
6
|
+
data?: RoutesManifestType;
|
|
7
|
+
errors?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatZodError(error: ZodError): string[] {
|
|
11
|
+
return error.errors.map((e) => {
|
|
12
|
+
const path = e.path.length > 0 ? `[${e.path.join(".")}] ` : "";
|
|
13
|
+
return `${path}${e.message}`;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadManifest(filePath: string): Promise<LoadResult> {
|
|
18
|
+
try {
|
|
19
|
+
const file = Bun.file(filePath);
|
|
20
|
+
const exists = await file.exists();
|
|
21
|
+
|
|
22
|
+
if (!exists) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
errors: [`νμΌμ μ°Ύμ μ μμ΅λλ€: ${filePath}`],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = await file.text();
|
|
30
|
+
let json: unknown;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
json = JSON.parse(content);
|
|
34
|
+
} catch {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
errors: ["JSON νμ± μ€ν¨: μ¬λ°λ₯Έ JSON νμμ΄ μλλλ€"],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = RoutesManifest.safeParse(json);
|
|
42
|
+
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
errors: formatZodError(result.error),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
data: result.data,
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
errors: [`μμμΉ λͺ»ν μ€λ₯: ${error instanceof Error ? error.message : String(error)}`],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function validateManifest(data: unknown): LoadResult {
|
|
63
|
+
const result = RoutesManifest.safeParse(data);
|
|
64
|
+
|
|
65
|
+
if (!result.success) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
errors: formatZodError(result.error),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
data: result.data,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/spec/lock.ts
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import { createHash } from "crypto";
|
|
2
|
-
import type { RoutesManifest } from "./schema";
|
|
3
|
-
|
|
4
|
-
export interface SpecLock {
|
|
5
|
-
routesHash: string;
|
|
6
|
-
updatedAt: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function computeHash(manifest: RoutesManifest): string {
|
|
10
|
-
const content = JSON.stringify(manifest, null, 2);
|
|
11
|
-
return createHash("sha256").update(content).digest("hex");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function readLock(lockPath: string): Promise<SpecLock | null> {
|
|
15
|
-
try {
|
|
16
|
-
const file = Bun.file(lockPath);
|
|
17
|
-
const exists = await file.exists();
|
|
18
|
-
|
|
19
|
-
if (!exists) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const content = await file.text();
|
|
24
|
-
return JSON.parse(content) as SpecLock;
|
|
25
|
-
} catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function writeLock(lockPath: string, manifest: RoutesManifest): Promise<SpecLock> {
|
|
31
|
-
const lock: SpecLock = {
|
|
32
|
-
routesHash: computeHash(manifest),
|
|
33
|
-
updatedAt: new Date().toISOString(),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
await Bun.write(lockPath, JSON.stringify(lock, null, 2));
|
|
37
|
-
return lock;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function verifyLock(
|
|
41
|
-
lockPath: string,
|
|
42
|
-
manifest: RoutesManifest
|
|
43
|
-
): Promise<{ valid: boolean; currentHash: string; lockHash: string | null }> {
|
|
44
|
-
const currentHash = computeHash(manifest);
|
|
45
|
-
const lock = await readLock(lockPath);
|
|
46
|
-
|
|
47
|
-
if (!lock) {
|
|
48
|
-
return { valid: false, currentHash, lockHash: null };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
valid: lock.routesHash === currentHash,
|
|
53
|
-
currentHash,
|
|
54
|
-
lockHash: lock.routesHash,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import type { RoutesManifest } from "./schema";
|
|
3
|
+
|
|
4
|
+
export interface SpecLock {
|
|
5
|
+
routesHash: string;
|
|
6
|
+
updatedAt: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function computeHash(manifest: RoutesManifest): string {
|
|
10
|
+
const content = JSON.stringify(manifest, null, 2);
|
|
11
|
+
return createHash("sha256").update(content).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function readLock(lockPath: string): Promise<SpecLock | null> {
|
|
15
|
+
try {
|
|
16
|
+
const file = Bun.file(lockPath);
|
|
17
|
+
const exists = await file.exists();
|
|
18
|
+
|
|
19
|
+
if (!exists) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const content = await file.text();
|
|
24
|
+
return JSON.parse(content) as SpecLock;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeLock(lockPath: string, manifest: RoutesManifest): Promise<SpecLock> {
|
|
31
|
+
const lock: SpecLock = {
|
|
32
|
+
routesHash: computeHash(manifest),
|
|
33
|
+
updatedAt: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
await Bun.write(lockPath, JSON.stringify(lock, null, 2));
|
|
37
|
+
return lock;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function verifyLock(
|
|
41
|
+
lockPath: string,
|
|
42
|
+
manifest: RoutesManifest
|
|
43
|
+
): Promise<{ valid: boolean; currentHash: string; lockHash: string | null }> {
|
|
44
|
+
const currentHash = computeHash(manifest);
|
|
45
|
+
const lock = await readLock(lockPath);
|
|
46
|
+
|
|
47
|
+
if (!lock) {
|
|
48
|
+
return { valid: false, currentHash, lockHash: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
valid: lock.routesHash === currentHash,
|
|
53
|
+
currentHash,
|
|
54
|
+
lockHash: lock.routesHash,
|
|
55
|
+
};
|
|
56
|
+
}
|