@mandujs/core 0.3.2 → 0.3.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.ko.md +200 -200
- package/README.md +200 -200
- package/package.json +4 -2
- package/src/change/history.ts +145 -0
- package/src/change/index.ts +40 -0
- package/src/change/integrity.ts +81 -0
- package/src/change/snapshot.ts +233 -0
- package/src/change/transaction.ts +293 -0
- package/src/change/types.ts +102 -0
- package/src/error/classifier.ts +314 -0
- package/src/error/formatter.ts +237 -0
- package/src/error/index.ts +25 -0
- package/src/error/stack-analyzer.ts +295 -0
- package/src/error/types.ts +140 -0
- package/src/filling/context.ts +228 -219
- package/src/filling/filling.ts +256 -234
- package/src/filling/index.ts +7 -7
- package/src/generator/generate.ts +85 -3
- package/src/generator/index.ts +2 -2
- package/src/guard/auto-correct.ts +257 -203
- package/src/index.ts +2 -0
- package/src/report/index.ts +1 -1
- package/src/runtime/index.ts +3 -3
- package/src/runtime/router.ts +65 -65
- package/src/runtime/server.ts +189 -139
- package/src/runtime/ssr.ts +38 -38
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
|
@@ -1,203 +1,257 @@
|
|
|
1
|
-
import type { RoutesManifest } from "../spec/schema";
|
|
2
|
-
import type { GuardViolation } from "./rules";
|
|
3
|
-
import { GUARD_RULES } from "./rules";
|
|
4
|
-
import { runGuardCheck } from "./check";
|
|
5
|
-
import { writeLock } from "../spec/lock";
|
|
6
|
-
import { generateRoutes } from "../generator/generate";
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
1
|
+
import type { RoutesManifest } from "../spec/schema";
|
|
2
|
+
import type { GuardViolation } from "./rules";
|
|
3
|
+
import { GUARD_RULES } from "./rules";
|
|
4
|
+
import { runGuardCheck } from "./check";
|
|
5
|
+
import { writeLock } from "../spec/lock";
|
|
6
|
+
import { generateRoutes } from "../generator/generate";
|
|
7
|
+
import { beginChange, commitChange, rollbackChange } from "../change";
|
|
8
|
+
import path from "path";
|
|
9
|
+
|
|
10
|
+
export interface AutoCorrectStep {
|
|
11
|
+
ruleId: string;
|
|
12
|
+
action: string;
|
|
13
|
+
success: boolean;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AutoCorrectResult {
|
|
18
|
+
fixed: boolean;
|
|
19
|
+
steps: AutoCorrectStep[];
|
|
20
|
+
remainingViolations: GuardViolation[];
|
|
21
|
+
retriedCount: number;
|
|
22
|
+
/** 롤백이 발생했는지 여부 */
|
|
23
|
+
rolledBack?: boolean;
|
|
24
|
+
/** 관련 트랜잭션 ID */
|
|
25
|
+
changeId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 자동 수정 가능한 규칙들
|
|
29
|
+
const AUTO_CORRECTABLE_RULES = new Set([
|
|
30
|
+
GUARD_RULES.SPEC_HASH_MISMATCH.id,
|
|
31
|
+
GUARD_RULES.GENERATED_MANUAL_EDIT.id,
|
|
32
|
+
GUARD_RULES.SLOT_NOT_FOUND.id,
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export function isAutoCorrectableViolation(violation: GuardViolation): boolean {
|
|
36
|
+
return AUTO_CORRECTABLE_RULES.has(violation.ruleId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runAutoCorrect(
|
|
40
|
+
violations: GuardViolation[],
|
|
41
|
+
manifest: RoutesManifest,
|
|
42
|
+
rootDir: string,
|
|
43
|
+
maxRetries: number = 3
|
|
44
|
+
): Promise<AutoCorrectResult> {
|
|
45
|
+
const steps: AutoCorrectStep[] = [];
|
|
46
|
+
let currentViolations = violations;
|
|
47
|
+
let retriedCount = 0;
|
|
48
|
+
|
|
49
|
+
// 1. 수정 전 스냅샷 생성 (트랜잭션 시작)
|
|
50
|
+
let change;
|
|
51
|
+
try {
|
|
52
|
+
change = await beginChange(rootDir, {
|
|
53
|
+
message: `Auto-correct: ${violations.length} violations`,
|
|
54
|
+
autoGenerated: true,
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// 이미 활성 트랜잭션이 있는 경우 그대로 진행 (사용자가 수동으로 시작한 경우)
|
|
58
|
+
// 다른 오류는 트랜잭션 없이 진행
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
while (retriedCount < maxRetries) {
|
|
63
|
+
const autoCorrectableViolations = currentViolations.filter(isAutoCorrectableViolation);
|
|
64
|
+
|
|
65
|
+
if (autoCorrectableViolations.length === 0) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 각 위반에 대해 수정 시도
|
|
70
|
+
let anyFixed = false;
|
|
71
|
+
|
|
72
|
+
for (const violation of autoCorrectableViolations) {
|
|
73
|
+
const step = await correctViolation(violation, manifest, rootDir);
|
|
74
|
+
steps.push(step);
|
|
75
|
+
|
|
76
|
+
if (step.success) {
|
|
77
|
+
anyFixed = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!anyFixed) {
|
|
82
|
+
// 아무것도 수정하지 못했으면 루프 종료
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Guard 재검사
|
|
87
|
+
retriedCount++;
|
|
88
|
+
const recheckResult = await runGuardCheck(manifest, rootDir);
|
|
89
|
+
currentViolations = recheckResult.violations;
|
|
90
|
+
|
|
91
|
+
if (recheckResult.passed) {
|
|
92
|
+
// 2. 성공 시 커밋
|
|
93
|
+
if (change) {
|
|
94
|
+
await commitChange(rootDir, change.id);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
fixed: true,
|
|
98
|
+
steps,
|
|
99
|
+
remainingViolations: [],
|
|
100
|
+
retriedCount,
|
|
101
|
+
changeId: change?.id,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. 실패 시 (maxRetries 도달 또는 수정 불가)
|
|
107
|
+
const fixed = currentViolations.length === 0;
|
|
108
|
+
|
|
109
|
+
if (fixed && change) {
|
|
110
|
+
// 모든 위반이 해결된 경우 커밋
|
|
111
|
+
await commitChange(rootDir, change.id);
|
|
112
|
+
} else if (!fixed && change) {
|
|
113
|
+
// 실패 시 롤백
|
|
114
|
+
await rollbackChange(rootDir, change.id);
|
|
115
|
+
return {
|
|
116
|
+
fixed: false,
|
|
117
|
+
steps,
|
|
118
|
+
remainingViolations: currentViolations,
|
|
119
|
+
retriedCount,
|
|
120
|
+
rolledBack: true,
|
|
121
|
+
changeId: change.id,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
fixed,
|
|
127
|
+
steps,
|
|
128
|
+
remainingViolations: currentViolations,
|
|
129
|
+
retriedCount,
|
|
130
|
+
changeId: change?.id,
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// 4. 오류 발생 시 롤백
|
|
134
|
+
if (change) {
|
|
135
|
+
try {
|
|
136
|
+
await rollbackChange(rootDir, change.id);
|
|
137
|
+
} catch {
|
|
138
|
+
// 롤백 실패는 무시 (원본 오류가 더 중요)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function correctViolation(
|
|
146
|
+
violation: GuardViolation,
|
|
147
|
+
manifest: RoutesManifest,
|
|
148
|
+
rootDir: string
|
|
149
|
+
): Promise<AutoCorrectStep> {
|
|
150
|
+
switch (violation.ruleId) {
|
|
151
|
+
case GUARD_RULES.SPEC_HASH_MISMATCH.id:
|
|
152
|
+
return await correctSpecHashMismatch(manifest, rootDir);
|
|
153
|
+
|
|
154
|
+
case GUARD_RULES.GENERATED_MANUAL_EDIT.id:
|
|
155
|
+
return await correctGeneratedManualEdit(manifest, rootDir);
|
|
156
|
+
|
|
157
|
+
case GUARD_RULES.SLOT_NOT_FOUND.id:
|
|
158
|
+
return await correctSlotNotFound(manifest, rootDir);
|
|
159
|
+
|
|
160
|
+
default:
|
|
161
|
+
return {
|
|
162
|
+
ruleId: violation.ruleId,
|
|
163
|
+
action: "skip",
|
|
164
|
+
success: false,
|
|
165
|
+
message: `자동 수정 불가능한 규칙: ${violation.ruleId}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function correctSpecHashMismatch(
|
|
171
|
+
manifest: RoutesManifest,
|
|
172
|
+
rootDir: string
|
|
173
|
+
): Promise<AutoCorrectStep> {
|
|
174
|
+
try {
|
|
175
|
+
const lockPath = path.join(rootDir, "spec/spec.lock.json");
|
|
176
|
+
await writeLock(lockPath, manifest);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
ruleId: GUARD_RULES.SPEC_HASH_MISMATCH.id,
|
|
180
|
+
action: "spec-upsert",
|
|
181
|
+
success: true,
|
|
182
|
+
message: "spec.lock.json 업데이트 완료",
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
ruleId: GUARD_RULES.SPEC_HASH_MISMATCH.id,
|
|
187
|
+
action: "spec-upsert",
|
|
188
|
+
success: false,
|
|
189
|
+
message: `spec.lock.json 업데이트 실패: ${error instanceof Error ? error.message : String(error)}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function correctGeneratedManualEdit(
|
|
195
|
+
manifest: RoutesManifest,
|
|
196
|
+
rootDir: string
|
|
197
|
+
): Promise<AutoCorrectStep> {
|
|
198
|
+
try {
|
|
199
|
+
const result = await generateRoutes(manifest, rootDir);
|
|
200
|
+
|
|
201
|
+
if (result.success) {
|
|
202
|
+
return {
|
|
203
|
+
ruleId: GUARD_RULES.GENERATED_MANUAL_EDIT.id,
|
|
204
|
+
action: "generate",
|
|
205
|
+
success: true,
|
|
206
|
+
message: `코드 재생성 완료 (${result.created.length}개 파일)`,
|
|
207
|
+
};
|
|
208
|
+
} else {
|
|
209
|
+
return {
|
|
210
|
+
ruleId: GUARD_RULES.GENERATED_MANUAL_EDIT.id,
|
|
211
|
+
action: "generate",
|
|
212
|
+
success: false,
|
|
213
|
+
message: `코드 재생성 실패: ${result.errors.join(", ")}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return {
|
|
218
|
+
ruleId: GUARD_RULES.GENERATED_MANUAL_EDIT.id,
|
|
219
|
+
action: "generate",
|
|
220
|
+
success: false,
|
|
221
|
+
message: `코드 재생성 실패: ${error instanceof Error ? error.message : String(error)}`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function correctSlotNotFound(
|
|
227
|
+
manifest: RoutesManifest,
|
|
228
|
+
rootDir: string
|
|
229
|
+
): Promise<AutoCorrectStep> {
|
|
230
|
+
try {
|
|
231
|
+
const result = await generateRoutes(manifest, rootDir);
|
|
232
|
+
|
|
233
|
+
if (result.success) {
|
|
234
|
+
const slotCount = result.created.filter((f) => f.includes("slots")).length;
|
|
235
|
+
return {
|
|
236
|
+
ruleId: GUARD_RULES.SLOT_NOT_FOUND.id,
|
|
237
|
+
action: "generate-slot",
|
|
238
|
+
success: true,
|
|
239
|
+
message: `Slot 파일 생성 완료 (${slotCount}개 파일)`,
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
return {
|
|
243
|
+
ruleId: GUARD_RULES.SLOT_NOT_FOUND.id,
|
|
244
|
+
action: "generate-slot",
|
|
245
|
+
success: false,
|
|
246
|
+
message: `Slot 파일 생성 실패: ${result.errors.join(", ")}`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return {
|
|
251
|
+
ruleId: GUARD_RULES.SLOT_NOT_FOUND.id,
|
|
252
|
+
action: "generate-slot",
|
|
253
|
+
success: false,
|
|
254
|
+
message: `Slot 파일 생성 실패: ${error instanceof Error ? error.message : String(error)}`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
package/src/index.ts
CHANGED
package/src/report/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./build";
|
|
1
|
+
export * from "./build";
|
package/src/runtime/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./ssr";
|
|
2
|
-
export * from "./router";
|
|
3
|
-
export * from "./server";
|
|
1
|
+
export * from "./ssr";
|
|
2
|
+
export * from "./router";
|
|
3
|
+
export * from "./server";
|
package/src/runtime/router.ts
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import type { RouteSpec } from "../spec/schema";
|
|
2
|
-
|
|
3
|
-
export interface MatchResult {
|
|
4
|
-
route: RouteSpec;
|
|
5
|
-
params: Record<string, string>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class Router {
|
|
9
|
-
private routes: RouteSpec[] = [];
|
|
10
|
-
private compiledPatterns: Map<string, { regex: RegExp; paramNames: string[] }> = new Map();
|
|
11
|
-
|
|
12
|
-
constructor(routes: RouteSpec[] = []) {
|
|
13
|
-
this.setRoutes(routes);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
setRoutes(routes: RouteSpec[]): void {
|
|
17
|
-
this.routes = routes;
|
|
18
|
-
this.compiledPatterns.clear();
|
|
19
|
-
|
|
20
|
-
for (const route of routes) {
|
|
21
|
-
this.compiledPatterns.set(route.id, this.compilePattern(route.pattern));
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private compilePattern(pattern: string): { regex: RegExp; paramNames: string[] } {
|
|
26
|
-
const paramNames: string[] = [];
|
|
27
|
-
|
|
28
|
-
const regexStr = pattern
|
|
29
|
-
.replace(/\//g, "\\/")
|
|
30
|
-
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
|
|
31
|
-
paramNames.push(paramName);
|
|
32
|
-
return "([^/]+)";
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const regex = new RegExp(`^${regexStr}$`);
|
|
36
|
-
return { regex, paramNames };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
match(pathname: string): MatchResult | null {
|
|
40
|
-
for (const route of this.routes) {
|
|
41
|
-
const compiled = this.compiledPatterns.get(route.id);
|
|
42
|
-
if (!compiled) continue;
|
|
43
|
-
|
|
44
|
-
const match = pathname.match(compiled.regex);
|
|
45
|
-
if (match) {
|
|
46
|
-
const params: Record<string, string> = {};
|
|
47
|
-
compiled.paramNames.forEach((name, index) => {
|
|
48
|
-
params[name] = match[index + 1];
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return { route, params };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
getRoutes(): RouteSpec[] {
|
|
59
|
-
return [...this.routes];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function createRouter(routes: RouteSpec[] = []): Router {
|
|
64
|
-
return new Router(routes);
|
|
65
|
-
}
|
|
1
|
+
import type { RouteSpec } from "../spec/schema";
|
|
2
|
+
|
|
3
|
+
export interface MatchResult {
|
|
4
|
+
route: RouteSpec;
|
|
5
|
+
params: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class Router {
|
|
9
|
+
private routes: RouteSpec[] = [];
|
|
10
|
+
private compiledPatterns: Map<string, { regex: RegExp; paramNames: string[] }> = new Map();
|
|
11
|
+
|
|
12
|
+
constructor(routes: RouteSpec[] = []) {
|
|
13
|
+
this.setRoutes(routes);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setRoutes(routes: RouteSpec[]): void {
|
|
17
|
+
this.routes = routes;
|
|
18
|
+
this.compiledPatterns.clear();
|
|
19
|
+
|
|
20
|
+
for (const route of routes) {
|
|
21
|
+
this.compiledPatterns.set(route.id, this.compilePattern(route.pattern));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private compilePattern(pattern: string): { regex: RegExp; paramNames: string[] } {
|
|
26
|
+
const paramNames: string[] = [];
|
|
27
|
+
|
|
28
|
+
const regexStr = pattern
|
|
29
|
+
.replace(/\//g, "\\/")
|
|
30
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
|
|
31
|
+
paramNames.push(paramName);
|
|
32
|
+
return "([^/]+)";
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
36
|
+
return { regex, paramNames };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
match(pathname: string): MatchResult | null {
|
|
40
|
+
for (const route of this.routes) {
|
|
41
|
+
const compiled = this.compiledPatterns.get(route.id);
|
|
42
|
+
if (!compiled) continue;
|
|
43
|
+
|
|
44
|
+
const match = pathname.match(compiled.regex);
|
|
45
|
+
if (match) {
|
|
46
|
+
const params: Record<string, string> = {};
|
|
47
|
+
compiled.paramNames.forEach((name, index) => {
|
|
48
|
+
params[name] = match[index + 1];
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return { route, params };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getRoutes(): RouteSpec[] {
|
|
59
|
+
return [...this.routes];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createRouter(routes: RouteSpec[] = []): Router {
|
|
64
|
+
return new Router(routes);
|
|
65
|
+
}
|