@mandujs/core 0.41.2 → 0.43.0

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.
Files changed (88) hide show
  1. package/package.json +21 -4
  2. package/src/auth/__tests__/login.test.ts +420 -419
  3. package/src/auth/__tests__/reset.test.ts +296 -296
  4. package/src/brain/adapters/anthropic-oauth.ts +421 -420
  5. package/src/brain/adapters/index.ts +2 -1
  6. package/src/brain/adapters/ollama.ts +1 -1
  7. package/src/brain/adapters/openai-oauth.ts +534 -533
  8. package/src/brain/brain.ts +2 -1
  9. package/src/brain/redactor.ts +196 -196
  10. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
  11. package/src/bundler/__tests__/cold-start.test.ts +504 -504
  12. package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
  13. package/src/bundler/__tests__/hdr.test.ts +1 -1
  14. package/src/bundler/analyzer.ts +958 -958
  15. package/src/bundler/build.ts +104 -14
  16. package/src/bundler/dev.ts +125 -0
  17. package/src/bundler/hmr-types.ts +1 -0
  18. package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
  19. package/src/bundler/plugins/index.ts +14 -0
  20. package/src/bundler/plugins/react-compiler-lint.ts +253 -0
  21. package/src/bundler/plugins/react-compiler.ts +162 -0
  22. package/src/bundler/types.ts +12 -0
  23. package/src/change/integrity.ts +2 -1
  24. package/src/client/index.ts +10 -0
  25. package/src/client/island.ts +38 -11
  26. package/src/client/router.ts +6 -1
  27. package/src/config/mandu.ts +57 -0
  28. package/src/config/validate.ts +42 -0
  29. package/src/content/collection.ts +844 -809
  30. package/src/content/content-layer.ts +316 -314
  31. package/src/content/content.test.ts +433 -433
  32. package/src/content/digest.ts +133 -133
  33. package/src/content/generate-types.ts +168 -168
  34. package/src/content/index.ts +6 -1
  35. package/src/content/llms-txt.ts +277 -277
  36. package/src/contract/define.ts +474 -474
  37. package/src/contract/route-helpers.ts +2 -1
  38. package/src/contract/zod-utils.ts +158 -155
  39. package/src/db/index.ts +513 -513
  40. package/src/desktop/__tests__/smoke.test.ts +100 -100
  41. package/src/desktop/webview-fallback.ts +583 -583
  42. package/src/desktop/window.ts +3 -1
  43. package/src/dev-error-overlay/overlay-client.ts +300 -300
  44. package/src/devtools/ai/mcp-connector.ts +499 -498
  45. package/src/devtools/client/components/kitchen-root.tsx +7 -2
  46. package/src/email/resend.ts +163 -163
  47. package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
  48. package/src/guard/ast-analyzer.ts +806 -806
  49. package/src/guard/graph.ts +898 -898
  50. package/src/guard/index.ts +16 -0
  51. package/src/guard/statistics.ts +578 -578
  52. package/src/guard/tsgolint-bridge.ts +512 -0
  53. package/src/i18n/locale-resolver.ts +214 -214
  54. package/src/id/__tests__/id.test.ts +120 -120
  55. package/src/intent/index.ts +321 -321
  56. package/src/island/index.ts +39 -23
  57. package/src/kitchen/api/contract-api.ts +15 -8
  58. package/src/kitchen/kitchen-ui.ts +2137 -2137
  59. package/src/lockfile/index.ts +3 -2
  60. package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
  61. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
  62. package/src/middleware/secure/index.ts +417 -417
  63. package/src/observability/event-bus.ts +2 -2
  64. package/src/observability/metrics.ts +334 -334
  65. package/src/observability/tracing.ts +694 -694
  66. package/src/openapi/generator.ts +1 -1
  67. package/src/perf/user-marks.ts +553 -553
  68. package/src/plugins/registry.ts +387 -387
  69. package/src/resource/ddl/diff.ts +392 -392
  70. package/src/resource/ddl/snapshot.ts +448 -447
  71. package/src/resource/generator-schema.ts +477 -476
  72. package/src/resource/parser.ts +4 -2
  73. package/src/resource/schema.ts +1 -1
  74. package/src/router/fs-patterns.ts +422 -422
  75. package/src/runtime/fast-refresh-types.ts +126 -128
  76. package/src/runtime/image-handler.ts +206 -195
  77. package/src/runtime/router.test.ts +476 -476
  78. package/src/runtime/security.ts +155 -155
  79. package/src/runtime/server.ts +36 -19
  80. package/src/runtime/session-key.ts +328 -328
  81. package/src/scheduler/__tests__/scheduler.test.ts +514 -514
  82. package/src/seo/resolve/index.ts +353 -353
  83. package/src/spec/load.ts +1 -1
  84. package/src/testing/reporter.ts +676 -676
  85. package/src/testing/server.ts +196 -196
  86. package/src/testing/snapshot.ts +444 -444
  87. package/src/utils/__tests__/lru-cache.test.ts +186 -186
  88. package/src/utils/bun.ts +8 -8
@@ -1,806 +1,806 @@
1
- /**
2
- * Mandu Guard AST Analyzer
3
- *
4
- * TypeScript AST 기반 정밀 분석
5
- *
6
- * 정규식보다 정확한 import 추출
7
- * - 주석 내 import 무시
8
- * - 문자열 내 import 무시
9
- * - 복잡한 멀티라인 import 처리
10
- * - Type-only import 구분
11
- */
12
-
13
- import type { ImportInfo } from "./types";
14
-
15
- // ═══════════════════════════════════════════════════════════════════════════
16
- // Token Types
17
- // ═══════════════════════════════════════════════════════════════════════════
18
-
19
- type TokenType =
20
- | "import"
21
- | "export"
22
- | "from"
23
- | "require"
24
- | "string"
25
- | "identifier"
26
- | "punctuation"
27
- | "keyword"
28
- | "comment"
29
- | "whitespace"
30
- | "newline"
31
- | "eof";
32
-
33
- interface Token {
34
- type: TokenType;
35
- value: string;
36
- start: number;
37
- end: number;
38
- line: number;
39
- column: number;
40
- }
41
-
42
- // ═══════════════════════════════════════════════════════════════════════════
43
- // Lexer (Tokenizer)
44
- // ═══════════════════════════════════════════════════════════════════════════
45
-
46
- /**
47
- * 간단한 TypeScript/JavaScript 토크나이저
48
- */
49
- function tokenize(content: string): Token[] {
50
- const tokens: Token[] = [];
51
- let pos = 0;
52
- let line = 1;
53
- let column = 1;
54
-
55
- const keywords = new Set([
56
- "import",
57
- "export",
58
- "from",
59
- "as",
60
- "type",
61
- "typeof",
62
- "const",
63
- "let",
64
- "var",
65
- "function",
66
- "class",
67
- "interface",
68
- "enum",
69
- "await",
70
- "async",
71
- "default",
72
- "require",
73
- ]);
74
-
75
- while (pos < content.length) {
76
- const start = pos;
77
- const startLine = line;
78
- const startColumn = column;
79
- let char = content[pos];
80
-
81
- // Newline
82
- if (char === "\n") {
83
- pos++;
84
- line++;
85
- column = 1;
86
- tokens.push({
87
- type: "newline",
88
- value: "\n",
89
- start,
90
- end: pos,
91
- line: startLine,
92
- column: startColumn,
93
- });
94
- continue;
95
- }
96
-
97
- // Whitespace
98
- if (/\s/.test(char)) {
99
- while (pos < content.length && /\s/.test(content[pos]) && content[pos] !== "\n") {
100
- pos++;
101
- column++;
102
- }
103
- tokens.push({
104
- type: "whitespace",
105
- value: content.slice(start, pos),
106
- start,
107
- end: pos,
108
- line: startLine,
109
- column: startColumn,
110
- });
111
- continue;
112
- }
113
-
114
- // Single-line comment
115
- if (char === "/" && content[pos + 1] === "/") {
116
- while (pos < content.length && content[pos] !== "\n") {
117
- pos++;
118
- column++;
119
- }
120
- tokens.push({
121
- type: "comment",
122
- value: content.slice(start, pos),
123
- start,
124
- end: pos,
125
- line: startLine,
126
- column: startColumn,
127
- });
128
- continue;
129
- }
130
-
131
- // Multi-line comment
132
- if (char === "/" && content[pos + 1] === "*") {
133
- pos += 2;
134
- column += 2;
135
- while (pos < content.length && !(content[pos] === "*" && content[pos + 1] === "/")) {
136
- if (content[pos] === "\n") {
137
- line++;
138
- column = 1;
139
- } else {
140
- column++;
141
- }
142
- pos++;
143
- }
144
- pos += 2;
145
- column += 2;
146
- tokens.push({
147
- type: "comment",
148
- value: content.slice(start, pos),
149
- start,
150
- end: pos,
151
- line: startLine,
152
- column: startColumn,
153
- });
154
- continue;
155
- }
156
-
157
- // String (single quote)
158
- if (char === "'") {
159
- pos++;
160
- column++;
161
- while (pos < content.length && content[pos] !== "'") {
162
- if (content[pos] === "\\") {
163
- pos++;
164
- column++;
165
- }
166
- if (content[pos] === "\n") {
167
- line++;
168
- column = 1;
169
- } else {
170
- column++;
171
- }
172
- pos++;
173
- }
174
- pos++; // closing quote
175
- column++;
176
- tokens.push({
177
- type: "string",
178
- value: content.slice(start, pos),
179
- start,
180
- end: pos,
181
- line: startLine,
182
- column: startColumn,
183
- });
184
- continue;
185
- }
186
-
187
- // String (double quote)
188
- if (char === '"') {
189
- pos++;
190
- column++;
191
- while (pos < content.length && content[pos] !== '"') {
192
- if (content[pos] === "\\") {
193
- pos++;
194
- column++;
195
- }
196
- if (content[pos] === "\n") {
197
- line++;
198
- column = 1;
199
- } else {
200
- column++;
201
- }
202
- pos++;
203
- }
204
- pos++; // closing quote
205
- column++;
206
- tokens.push({
207
- type: "string",
208
- value: content.slice(start, pos),
209
- start,
210
- end: pos,
211
- line: startLine,
212
- column: startColumn,
213
- });
214
- continue;
215
- }
216
-
217
- // Template literal (backtick) - simplified
218
- if (char === "`") {
219
- pos++;
220
- column++;
221
- while (pos < content.length && content[pos] !== "`") {
222
- if (content[pos] === "\\") {
223
- pos++;
224
- column++;
225
- }
226
- if (content[pos] === "\n") {
227
- line++;
228
- column = 1;
229
- } else {
230
- column++;
231
- }
232
- pos++;
233
- }
234
- pos++; // closing backtick
235
- column++;
236
- tokens.push({
237
- type: "string",
238
- value: content.slice(start, pos),
239
- start,
240
- end: pos,
241
- line: startLine,
242
- column: startColumn,
243
- });
244
- continue;
245
- }
246
-
247
- // Punctuation
248
- if (/[{}()\[\];,.*]/.test(char)) {
249
- pos++;
250
- column++;
251
- tokens.push({
252
- type: "punctuation",
253
- value: char,
254
- start,
255
- end: pos,
256
- line: startLine,
257
- column: startColumn,
258
- });
259
- continue;
260
- }
261
-
262
- // Identifier or keyword
263
- if (/[a-zA-Z_$]/.test(char)) {
264
- while (pos < content.length && /[a-zA-Z0-9_$]/.test(content[pos])) {
265
- pos++;
266
- column++;
267
- }
268
- const value = content.slice(start, pos);
269
- const type: TokenType = keywords.has(value)
270
- ? value === "import"
271
- ? "import"
272
- : value === "from"
273
- ? "from"
274
- : value === "require"
275
- ? "require"
276
- : "keyword"
277
- : "identifier";
278
-
279
- tokens.push({
280
- type,
281
- value,
282
- start,
283
- end: pos,
284
- line: startLine,
285
- column: startColumn,
286
- });
287
- continue;
288
- }
289
-
290
- // Skip other characters
291
- pos++;
292
- column++;
293
- }
294
-
295
- tokens.push({
296
- type: "eof",
297
- value: "",
298
- start: pos,
299
- end: pos,
300
- line,
301
- column,
302
- });
303
-
304
- return tokens;
305
- }
306
-
307
- // ═══════════════════════════════════════════════════════════════════════════
308
- // AST Import Extraction
309
- // ═══════════════════════════════════════════════════════════════════════════
310
-
311
- /**
312
- * AST 기반 import 추출
313
- *
314
- * 정규식보다 정확하게 import 문을 추출
315
- */
316
- export function extractImportsAST(content: string): ImportInfo[] {
317
- const tokens = tokenize(content);
318
- const imports: ImportInfo[] = [];
319
-
320
- let i = 0;
321
-
322
- // Skip whitespace, newlines, comments
323
- const skip = () => {
324
- while (
325
- i < tokens.length &&
326
- (tokens[i].type === "whitespace" ||
327
- tokens[i].type === "newline" ||
328
- tokens[i].type === "comment")
329
- ) {
330
- i++;
331
- }
332
- };
333
-
334
- // Get current token
335
- const current = () => tokens[i];
336
-
337
- // Check if current token matches
338
- const is = (type: TokenType, value?: string) => {
339
- const t = current();
340
- return t && t.type === type && (value === undefined || t.value === value);
341
- };
342
-
343
- // Consume token if matches
344
- const consume = (type: TokenType, value?: string) => {
345
- if (is(type, value)) {
346
- const t = current();
347
- i++;
348
- return t;
349
- }
350
- return null;
351
- };
352
-
353
- while (i < tokens.length && current().type !== "eof") {
354
- skip();
355
-
356
- // Static import: import ... from '...'
357
- // Dynamic import: import('...')
358
- if (is("import")) {
359
- const importToken = consume("import")!;
360
- skip();
361
-
362
- // Dynamic import: import('...')
363
- if (is("punctuation", "(")) {
364
- consume("punctuation", "(");
365
- skip();
366
- if (is("string")) {
367
- const pathToken = consume("string")!;
368
- const path = pathToken.value.slice(1, -1);
369
- skip();
370
- consume("punctuation", ")");
371
-
372
- imports.push({
373
- statement: `import('${path}')`,
374
- path,
375
- line: importToken.line,
376
- column: importToken.column,
377
- type: "dynamic",
378
- });
379
- }
380
- continue;
381
- }
382
-
383
- let namedImports: string[] | undefined;
384
- let defaultImport: string | undefined;
385
- let namespaceImport: string | undefined;
386
- let isTypeOnly = false;
387
-
388
- // Check for type-only import
389
- if (is("keyword", "type")) {
390
- consume("keyword", "type");
391
- skip();
392
- isTypeOnly = true;
393
- }
394
-
395
- // Side-effect import: import '...'
396
- if (is("string")) {
397
- const pathToken = consume("string")!;
398
- const path = pathToken.value.slice(1, -1); // Remove quotes
399
-
400
- imports.push({
401
- statement: content.slice(importToken.start, pathToken.end),
402
- path,
403
- line: importToken.line,
404
- column: importToken.column,
405
- type: "static",
406
- });
407
- continue;
408
- }
409
-
410
- // Default import: import X from '...'
411
- if (is("identifier")) {
412
- defaultImport = consume("identifier")!.value;
413
- skip();
414
-
415
- // import X, { ... } from '...'
416
- if (is("punctuation", ",")) {
417
- consume("punctuation", ",");
418
- skip();
419
- }
420
- }
421
-
422
- // Namespace import: import * as X from '...'
423
- if (is("punctuation", "*")) {
424
- consume("punctuation", "*");
425
- skip();
426
- if (is("keyword", "as")) {
427
- consume("keyword", "as");
428
- skip();
429
- if (is("identifier")) {
430
- namespaceImport = consume("identifier")!.value;
431
- skip();
432
- }
433
- }
434
- }
435
-
436
- // Named imports: import { X, Y } from '...'
437
- if (is("punctuation", "{")) {
438
- consume("punctuation", "{");
439
- skip();
440
- namedImports = [];
441
-
442
- while (!is("punctuation", "}") && !is("eof")) {
443
- if (is("identifier") || is("keyword")) {
444
- const name = current().value;
445
- i++;
446
- skip();
447
-
448
- // Handle 'as' alias
449
- if (is("keyword", "as")) {
450
- consume("keyword", "as");
451
- skip();
452
- if (is("identifier")) {
453
- consume("identifier");
454
- skip();
455
- }
456
- }
457
-
458
- namedImports.push(name);
459
- }
460
-
461
- if (is("punctuation", ",")) {
462
- consume("punctuation", ",");
463
- skip();
464
- } else {
465
- break;
466
- }
467
- }
468
-
469
- consume("punctuation", "}");
470
- skip();
471
- }
472
-
473
- // from '...'
474
- if (is("from")) {
475
- consume("from");
476
- skip();
477
-
478
- if (is("string")) {
479
- const pathToken = consume("string")!;
480
- const path = pathToken.value.slice(1, -1);
481
-
482
- imports.push({
483
- statement: content.slice(importToken.start, pathToken.end),
484
- path,
485
- line: importToken.line,
486
- column: importToken.column,
487
- type: "static",
488
- namedImports: namedImports?.length ? namedImports : undefined,
489
- defaultImport,
490
- });
491
- }
492
- }
493
-
494
- continue;
495
- }
496
-
497
- // require('...')
498
- if (is("require")) {
499
- const requireToken = consume("require")!;
500
- skip();
501
-
502
- if (is("punctuation", "(")) {
503
- consume("punctuation", "(");
504
- skip();
505
-
506
- if (is("string")) {
507
- const pathToken = consume("string")!;
508
- const path = pathToken.value.slice(1, -1);
509
- skip();
510
- consume("punctuation", ")");
511
-
512
- imports.push({
513
- statement: `require('${path}')`,
514
- path,
515
- line: requireToken.line,
516
- column: requireToken.column,
517
- type: "require",
518
- });
519
- }
520
- }
521
- continue;
522
- }
523
-
524
- // Move to next token
525
- i++;
526
- }
527
-
528
- return imports;
529
- }
530
-
531
- // ═══════════════════════════════════════════════════════════════════════════
532
- // Export Analysis
533
- // ═══════════════════════════════════════════════════════════════════════════
534
-
535
- /**
536
- * Export 정보
537
- */
538
- export interface ExportInfo {
539
- /** Export 유형 */
540
- type: "named" | "default" | "all" | "type";
541
- /** Export 이름 (named의 경우) */
542
- name?: string;
543
- /** 원본 이름 (as 사용 시) */
544
- originalName?: string;
545
- /** re-export 소스 */
546
- from?: string;
547
- /** 라인 번호 */
548
- line: number;
549
- }
550
-
551
- /**
552
- * Export 문 추출
553
- */
554
- export function extractExportsAST(content: string): ExportInfo[] {
555
- const tokens = tokenize(content);
556
- const exports: ExportInfo[] = [];
557
-
558
- let i = 0;
559
-
560
- const skip = () => {
561
- while (
562
- i < tokens.length &&
563
- (tokens[i].type === "whitespace" ||
564
- tokens[i].type === "newline" ||
565
- tokens[i].type === "comment")
566
- ) {
567
- i++;
568
- }
569
- };
570
-
571
- const current = () => tokens[i];
572
- const is = (type: TokenType, value?: string) => {
573
- const t = current();
574
- return t && t.type === type && (value === undefined || t.value === value);
575
- };
576
- const consume = (type: TokenType, value?: string) => {
577
- if (is(type, value)) {
578
- const t = current();
579
- i++;
580
- return t;
581
- }
582
- return null;
583
- };
584
-
585
- while (i < tokens.length && current().type !== "eof") {
586
- skip();
587
-
588
- if (is("keyword", "export")) {
589
- const exportToken = consume("keyword", "export")!;
590
- skip();
591
-
592
- // export default
593
- if (is("keyword", "default")) {
594
- consume("keyword", "default");
595
- exports.push({
596
- type: "default",
597
- line: exportToken.line,
598
- });
599
- continue;
600
- }
601
-
602
- // export type
603
- if (is("keyword", "type")) {
604
- consume("keyword", "type");
605
- skip();
606
-
607
- if (is("punctuation", "{")) {
608
- // export type { ... }
609
- consume("punctuation", "{");
610
- skip();
611
-
612
- while (!is("punctuation", "}") && !is("eof")) {
613
- if (is("identifier")) {
614
- const name = consume("identifier")!.value;
615
- skip();
616
-
617
- let originalName: string | undefined;
618
- if (is("keyword", "as")) {
619
- consume("keyword", "as");
620
- skip();
621
- originalName = name;
622
- if (is("identifier")) {
623
- consume("identifier");
624
- skip();
625
- }
626
- }
627
-
628
- exports.push({
629
- type: "type",
630
- name,
631
- originalName,
632
- line: exportToken.line,
633
- });
634
- }
635
-
636
- if (is("punctuation", ",")) {
637
- consume("punctuation", ",");
638
- skip();
639
- } else {
640
- break;
641
- }
642
- }
643
- }
644
- continue;
645
- }
646
-
647
- // export * from '...'
648
- if (is("punctuation", "*")) {
649
- consume("punctuation", "*");
650
- skip();
651
-
652
- let from: string | undefined;
653
- if (is("from")) {
654
- consume("from");
655
- skip();
656
- if (is("string")) {
657
- from = consume("string")!.value.slice(1, -1);
658
- }
659
- }
660
-
661
- exports.push({
662
- type: "all",
663
- from,
664
- line: exportToken.line,
665
- });
666
- continue;
667
- }
668
-
669
- // export { ... }
670
- if (is("punctuation", "{")) {
671
- consume("punctuation", "{");
672
- skip();
673
-
674
- const names: { name: string; originalName?: string }[] = [];
675
-
676
- while (!is("punctuation", "}") && !is("eof")) {
677
- if (is("identifier")) {
678
- const name = consume("identifier")!.value;
679
- skip();
680
-
681
- let originalName: string | undefined;
682
- if (is("keyword", "as")) {
683
- consume("keyword", "as");
684
- skip();
685
- originalName = name;
686
- if (is("identifier")) {
687
- consume("identifier");
688
- skip();
689
- }
690
- }
691
-
692
- names.push({ name, originalName });
693
- }
694
-
695
- if (is("punctuation", ",")) {
696
- consume("punctuation", ",");
697
- skip();
698
- } else {
699
- break;
700
- }
701
- }
702
-
703
- consume("punctuation", "}");
704
- skip();
705
-
706
- let from: string | undefined;
707
- if (is("from")) {
708
- consume("from");
709
- skip();
710
- if (is("string")) {
711
- from = consume("string")!.value.slice(1, -1);
712
- }
713
- }
714
-
715
- for (const { name, originalName } of names) {
716
- exports.push({
717
- type: "named",
718
- name,
719
- originalName,
720
- from,
721
- line: exportToken.line,
722
- });
723
- }
724
- continue;
725
- }
726
-
727
- // export const/let/var/function/class
728
- if (
729
- is("keyword", "const") ||
730
- is("keyword", "let") ||
731
- is("keyword", "var") ||
732
- is("keyword", "function") ||
733
- is("keyword", "class") ||
734
- is("keyword", "interface") ||
735
- is("keyword", "enum") ||
736
- is("keyword", "async")
737
- ) {
738
- consume("keyword");
739
- skip();
740
-
741
- // async function
742
- if (is("keyword", "function")) {
743
- consume("keyword", "function");
744
- skip();
745
- }
746
-
747
- if (is("identifier")) {
748
- const name = consume("identifier")!.value;
749
- exports.push({
750
- type: "named",
751
- name,
752
- line: exportToken.line,
753
- });
754
- }
755
- continue;
756
- }
757
- }
758
-
759
- i++;
760
- }
761
-
762
- return exports;
763
- }
764
-
765
- // ═══════════════════════════════════════════════════════════════════════════
766
- // Module Analysis
767
- // ═══════════════════════════════════════════════════════════════════════════
768
-
769
- /**
770
- * 모듈 분석 결과
771
- */
772
- export interface ModuleAnalysis {
773
- /** Import 목록 */
774
- imports: ImportInfo[];
775
- /** Export 목록 */
776
- exports: ExportInfo[];
777
- /** Public API 여부 (index 파일) */
778
- isPublicAPI: boolean;
779
- /** 순수 타입 모듈 여부 */
780
- isTypeOnly: boolean;
781
- }
782
-
783
- /**
784
- * 모듈 전체 분석
785
- */
786
- export function analyzeModuleAST(content: string, filePath: string): ModuleAnalysis {
787
- const imports = extractImportsAST(content);
788
- const exports = extractExportsAST(content);
789
-
790
- const isPublicAPI =
791
- filePath.endsWith("/index.ts") ||
792
- filePath.endsWith("/index.tsx") ||
793
- filePath.endsWith("/index.js");
794
-
795
- // 타입만 있는 모듈인지 확인
796
- const hasRuntimeExport = exports.some((e) => e.type !== "type");
797
- const hasRuntimeImport = imports.some((i) => !i.statement.includes("import type"));
798
- const isTypeOnly = !hasRuntimeExport && !hasRuntimeImport && exports.length > 0;
799
-
800
- return {
801
- imports,
802
- exports,
803
- isPublicAPI,
804
- isTypeOnly,
805
- };
806
- }
1
+ /**
2
+ * Mandu Guard AST Analyzer
3
+ *
4
+ * TypeScript AST 기반 정밀 분석
5
+ *
6
+ * 정규식보다 정확한 import 추출
7
+ * - 주석 내 import 무시
8
+ * - 문자열 내 import 무시
9
+ * - 복잡한 멀티라인 import 처리
10
+ * - Type-only import 구분
11
+ */
12
+
13
+ import type { ImportInfo } from "./types";
14
+
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+ // Token Types
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+
19
+ type TokenType =
20
+ | "import"
21
+ | "export"
22
+ | "from"
23
+ | "require"
24
+ | "string"
25
+ | "identifier"
26
+ | "punctuation"
27
+ | "keyword"
28
+ | "comment"
29
+ | "whitespace"
30
+ | "newline"
31
+ | "eof";
32
+
33
+ interface Token {
34
+ type: TokenType;
35
+ value: string;
36
+ start: number;
37
+ end: number;
38
+ line: number;
39
+ column: number;
40
+ }
41
+
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+ // Lexer (Tokenizer)
44
+ // ═══════════════════════════════════════════════════════════════════════════
45
+
46
+ /**
47
+ * 간단한 TypeScript/JavaScript 토크나이저
48
+ */
49
+ function tokenize(content: string): Token[] {
50
+ const tokens: Token[] = [];
51
+ let pos = 0;
52
+ let line = 1;
53
+ let column = 1;
54
+
55
+ const keywords = new Set([
56
+ "import",
57
+ "export",
58
+ "from",
59
+ "as",
60
+ "type",
61
+ "typeof",
62
+ "const",
63
+ "let",
64
+ "var",
65
+ "function",
66
+ "class",
67
+ "interface",
68
+ "enum",
69
+ "await",
70
+ "async",
71
+ "default",
72
+ "require",
73
+ ]);
74
+
75
+ while (pos < content.length) {
76
+ const start = pos;
77
+ const startLine = line;
78
+ const startColumn = column;
79
+ let char = content[pos];
80
+
81
+ // Newline
82
+ if (char === "\n") {
83
+ pos++;
84
+ line++;
85
+ column = 1;
86
+ tokens.push({
87
+ type: "newline",
88
+ value: "\n",
89
+ start,
90
+ end: pos,
91
+ line: startLine,
92
+ column: startColumn,
93
+ });
94
+ continue;
95
+ }
96
+
97
+ // Whitespace
98
+ if (/\s/.test(char)) {
99
+ while (pos < content.length && /\s/.test(content[pos]) && content[pos] !== "\n") {
100
+ pos++;
101
+ column++;
102
+ }
103
+ tokens.push({
104
+ type: "whitespace",
105
+ value: content.slice(start, pos),
106
+ start,
107
+ end: pos,
108
+ line: startLine,
109
+ column: startColumn,
110
+ });
111
+ continue;
112
+ }
113
+
114
+ // Single-line comment
115
+ if (char === "/" && content[pos + 1] === "/") {
116
+ while (pos < content.length && content[pos] !== "\n") {
117
+ pos++;
118
+ column++;
119
+ }
120
+ tokens.push({
121
+ type: "comment",
122
+ value: content.slice(start, pos),
123
+ start,
124
+ end: pos,
125
+ line: startLine,
126
+ column: startColumn,
127
+ });
128
+ continue;
129
+ }
130
+
131
+ // Multi-line comment
132
+ if (char === "/" && content[pos + 1] === "*") {
133
+ pos += 2;
134
+ column += 2;
135
+ while (pos < content.length && !(content[pos] === "*" && content[pos + 1] === "/")) {
136
+ if (content[pos] === "\n") {
137
+ line++;
138
+ column = 1;
139
+ } else {
140
+ column++;
141
+ }
142
+ pos++;
143
+ }
144
+ pos += 2;
145
+ column += 2;
146
+ tokens.push({
147
+ type: "comment",
148
+ value: content.slice(start, pos),
149
+ start,
150
+ end: pos,
151
+ line: startLine,
152
+ column: startColumn,
153
+ });
154
+ continue;
155
+ }
156
+
157
+ // String (single quote)
158
+ if (char === "'") {
159
+ pos++;
160
+ column++;
161
+ while (pos < content.length && content[pos] !== "'") {
162
+ if (content[pos] === "\\") {
163
+ pos++;
164
+ column++;
165
+ }
166
+ if (content[pos] === "\n") {
167
+ line++;
168
+ column = 1;
169
+ } else {
170
+ column++;
171
+ }
172
+ pos++;
173
+ }
174
+ pos++; // closing quote
175
+ column++;
176
+ tokens.push({
177
+ type: "string",
178
+ value: content.slice(start, pos),
179
+ start,
180
+ end: pos,
181
+ line: startLine,
182
+ column: startColumn,
183
+ });
184
+ continue;
185
+ }
186
+
187
+ // String (double quote)
188
+ if (char === '"') {
189
+ pos++;
190
+ column++;
191
+ while (pos < content.length && content[pos] !== '"') {
192
+ if (content[pos] === "\\") {
193
+ pos++;
194
+ column++;
195
+ }
196
+ if (content[pos] === "\n") {
197
+ line++;
198
+ column = 1;
199
+ } else {
200
+ column++;
201
+ }
202
+ pos++;
203
+ }
204
+ pos++; // closing quote
205
+ column++;
206
+ tokens.push({
207
+ type: "string",
208
+ value: content.slice(start, pos),
209
+ start,
210
+ end: pos,
211
+ line: startLine,
212
+ column: startColumn,
213
+ });
214
+ continue;
215
+ }
216
+
217
+ // Template literal (backtick) - simplified
218
+ if (char === "`") {
219
+ pos++;
220
+ column++;
221
+ while (pos < content.length && content[pos] !== "`") {
222
+ if (content[pos] === "\\") {
223
+ pos++;
224
+ column++;
225
+ }
226
+ if (content[pos] === "\n") {
227
+ line++;
228
+ column = 1;
229
+ } else {
230
+ column++;
231
+ }
232
+ pos++;
233
+ }
234
+ pos++; // closing backtick
235
+ column++;
236
+ tokens.push({
237
+ type: "string",
238
+ value: content.slice(start, pos),
239
+ start,
240
+ end: pos,
241
+ line: startLine,
242
+ column: startColumn,
243
+ });
244
+ continue;
245
+ }
246
+
247
+ // Punctuation
248
+ if (/[{}()\[\];,.*]/.test(char)) {
249
+ pos++;
250
+ column++;
251
+ tokens.push({
252
+ type: "punctuation",
253
+ value: char,
254
+ start,
255
+ end: pos,
256
+ line: startLine,
257
+ column: startColumn,
258
+ });
259
+ continue;
260
+ }
261
+
262
+ // Identifier or keyword
263
+ if (/[a-zA-Z_$]/.test(char)) {
264
+ while (pos < content.length && /[a-zA-Z0-9_$]/.test(content[pos])) {
265
+ pos++;
266
+ column++;
267
+ }
268
+ const value = content.slice(start, pos);
269
+ const type: TokenType = keywords.has(value)
270
+ ? value === "import"
271
+ ? "import"
272
+ : value === "from"
273
+ ? "from"
274
+ : value === "require"
275
+ ? "require"
276
+ : "keyword"
277
+ : "identifier";
278
+
279
+ tokens.push({
280
+ type,
281
+ value,
282
+ start,
283
+ end: pos,
284
+ line: startLine,
285
+ column: startColumn,
286
+ });
287
+ continue;
288
+ }
289
+
290
+ // Skip other characters
291
+ pos++;
292
+ column++;
293
+ }
294
+
295
+ tokens.push({
296
+ type: "eof",
297
+ value: "",
298
+ start: pos,
299
+ end: pos,
300
+ line,
301
+ column,
302
+ });
303
+
304
+ return tokens;
305
+ }
306
+
307
+ // ═══════════════════════════════════════════════════════════════════════════
308
+ // AST Import Extraction
309
+ // ═══════════════════════════════════════════════════════════════════════════
310
+
311
+ /**
312
+ * AST 기반 import 추출
313
+ *
314
+ * 정규식보다 정확하게 import 문을 추출
315
+ */
316
+ export function extractImportsAST(content: string): ImportInfo[] {
317
+ const tokens = tokenize(content);
318
+ const imports: ImportInfo[] = [];
319
+
320
+ let i = 0;
321
+
322
+ // Skip whitespace, newlines, comments
323
+ const skip = () => {
324
+ while (
325
+ i < tokens.length &&
326
+ (tokens[i].type === "whitespace" ||
327
+ tokens[i].type === "newline" ||
328
+ tokens[i].type === "comment")
329
+ ) {
330
+ i++;
331
+ }
332
+ };
333
+
334
+ // Get current token
335
+ const current = () => tokens[i];
336
+
337
+ // Check if current token matches
338
+ const is = (type: TokenType, value?: string) => {
339
+ const t = current();
340
+ return t && t.type === type && (value === undefined || t.value === value);
341
+ };
342
+
343
+ // Consume token if matches
344
+ const consume = (type: TokenType, value?: string) => {
345
+ if (is(type, value)) {
346
+ const t = current();
347
+ i++;
348
+ return t;
349
+ }
350
+ return null;
351
+ };
352
+
353
+ while (i < tokens.length && current().type !== "eof") {
354
+ skip();
355
+
356
+ // Static import: import ... from '...'
357
+ // Dynamic import: import('...')
358
+ if (is("import")) {
359
+ const importToken = consume("import")!;
360
+ skip();
361
+
362
+ // Dynamic import: import('...')
363
+ if (is("punctuation", "(")) {
364
+ consume("punctuation", "(");
365
+ skip();
366
+ if (is("string")) {
367
+ const pathToken = consume("string")!;
368
+ const path = pathToken.value.slice(1, -1);
369
+ skip();
370
+ consume("punctuation", ")");
371
+
372
+ imports.push({
373
+ statement: `import('${path}')`,
374
+ path,
375
+ line: importToken.line,
376
+ column: importToken.column,
377
+ type: "dynamic",
378
+ });
379
+ }
380
+ continue;
381
+ }
382
+
383
+ let namedImports: string[] | undefined;
384
+ let defaultImport: string | undefined;
385
+ let namespaceImport: string | undefined;
386
+ let isTypeOnly = false;
387
+
388
+ // Check for type-only import
389
+ if (is("keyword", "type")) {
390
+ consume("keyword", "type");
391
+ skip();
392
+ isTypeOnly = true;
393
+ }
394
+
395
+ // Side-effect import: import '...'
396
+ if (is("string")) {
397
+ const pathToken = consume("string")!;
398
+ const path = pathToken.value.slice(1, -1); // Remove quotes
399
+
400
+ imports.push({
401
+ statement: content.slice(importToken.start, pathToken.end),
402
+ path,
403
+ line: importToken.line,
404
+ column: importToken.column,
405
+ type: "static",
406
+ });
407
+ continue;
408
+ }
409
+
410
+ // Default import: import X from '...'
411
+ if (is("identifier")) {
412
+ defaultImport = consume("identifier")!.value;
413
+ skip();
414
+
415
+ // import X, { ... } from '...'
416
+ if (is("punctuation", ",")) {
417
+ consume("punctuation", ",");
418
+ skip();
419
+ }
420
+ }
421
+
422
+ // Namespace import: import * as X from '...'
423
+ if (is("punctuation", "*")) {
424
+ consume("punctuation", "*");
425
+ skip();
426
+ if (is("keyword", "as")) {
427
+ consume("keyword", "as");
428
+ skip();
429
+ if (is("identifier")) {
430
+ namespaceImport = consume("identifier")!.value;
431
+ skip();
432
+ }
433
+ }
434
+ }
435
+
436
+ // Named imports: import { X, Y } from '...'
437
+ if (is("punctuation", "{")) {
438
+ consume("punctuation", "{");
439
+ skip();
440
+ namedImports = [];
441
+
442
+ while (!is("punctuation", "}") && !is("eof")) {
443
+ if (is("identifier") || is("keyword")) {
444
+ const name = current().value;
445
+ i++;
446
+ skip();
447
+
448
+ // Handle 'as' alias
449
+ if (is("keyword", "as")) {
450
+ consume("keyword", "as");
451
+ skip();
452
+ if (is("identifier")) {
453
+ consume("identifier");
454
+ skip();
455
+ }
456
+ }
457
+
458
+ namedImports.push(name);
459
+ }
460
+
461
+ if (is("punctuation", ",")) {
462
+ consume("punctuation", ",");
463
+ skip();
464
+ } else {
465
+ break;
466
+ }
467
+ }
468
+
469
+ consume("punctuation", "}");
470
+ skip();
471
+ }
472
+
473
+ // from '...'
474
+ if (is("from")) {
475
+ consume("from");
476
+ skip();
477
+
478
+ if (is("string")) {
479
+ const pathToken = consume("string")!;
480
+ const path = pathToken.value.slice(1, -1);
481
+
482
+ imports.push({
483
+ statement: content.slice(importToken.start, pathToken.end),
484
+ path,
485
+ line: importToken.line,
486
+ column: importToken.column,
487
+ type: "static",
488
+ namedImports: namedImports?.length ? namedImports : undefined,
489
+ defaultImport,
490
+ });
491
+ }
492
+ }
493
+
494
+ continue;
495
+ }
496
+
497
+ // require('...')
498
+ if (is("require")) {
499
+ const requireToken = consume("require")!;
500
+ skip();
501
+
502
+ if (is("punctuation", "(")) {
503
+ consume("punctuation", "(");
504
+ skip();
505
+
506
+ if (is("string")) {
507
+ const pathToken = consume("string")!;
508
+ const path = pathToken.value.slice(1, -1);
509
+ skip();
510
+ consume("punctuation", ")");
511
+
512
+ imports.push({
513
+ statement: `require('${path}')`,
514
+ path,
515
+ line: requireToken.line,
516
+ column: requireToken.column,
517
+ type: "require",
518
+ });
519
+ }
520
+ }
521
+ continue;
522
+ }
523
+
524
+ // Move to next token
525
+ i++;
526
+ }
527
+
528
+ return imports;
529
+ }
530
+
531
+ // ═══════════════════════════════════════════════════════════════════════════
532
+ // Export Analysis
533
+ // ═══════════════════════════════════════════════════════════════════════════
534
+
535
+ /**
536
+ * Export 정보
537
+ */
538
+ export interface ExportInfo {
539
+ /** Export 유형 */
540
+ type: "named" | "default" | "all" | "type";
541
+ /** Export 이름 (named의 경우) */
542
+ name?: string;
543
+ /** 원본 이름 (as 사용 시) */
544
+ originalName?: string;
545
+ /** re-export 소스 */
546
+ from?: string;
547
+ /** 라인 번호 */
548
+ line: number;
549
+ }
550
+
551
+ /**
552
+ * Export 문 추출
553
+ */
554
+ export function extractExportsAST(content: string): ExportInfo[] {
555
+ const tokens = tokenize(content);
556
+ const exports: ExportInfo[] = [];
557
+
558
+ let i = 0;
559
+
560
+ const skip = () => {
561
+ while (
562
+ i < tokens.length &&
563
+ (tokens[i].type === "whitespace" ||
564
+ tokens[i].type === "newline" ||
565
+ tokens[i].type === "comment")
566
+ ) {
567
+ i++;
568
+ }
569
+ };
570
+
571
+ const current = () => tokens[i];
572
+ const is = (type: TokenType, value?: string) => {
573
+ const t = current();
574
+ return t && t.type === type && (value === undefined || t.value === value);
575
+ };
576
+ const consume = (type: TokenType, value?: string) => {
577
+ if (is(type, value)) {
578
+ const t = current();
579
+ i++;
580
+ return t;
581
+ }
582
+ return null;
583
+ };
584
+
585
+ while (i < tokens.length && current().type !== "eof") {
586
+ skip();
587
+
588
+ if (is("keyword", "export")) {
589
+ const exportToken = consume("keyword", "export")!;
590
+ skip();
591
+
592
+ // export default
593
+ if (is("keyword", "default")) {
594
+ consume("keyword", "default");
595
+ exports.push({
596
+ type: "default",
597
+ line: exportToken.line,
598
+ });
599
+ continue;
600
+ }
601
+
602
+ // export type
603
+ if (is("keyword", "type")) {
604
+ consume("keyword", "type");
605
+ skip();
606
+
607
+ if (is("punctuation", "{")) {
608
+ // export type { ... }
609
+ consume("punctuation", "{");
610
+ skip();
611
+
612
+ while (!is("punctuation", "}") && !is("eof")) {
613
+ if (is("identifier")) {
614
+ const name = consume("identifier")!.value;
615
+ skip();
616
+
617
+ let originalName: string | undefined;
618
+ if (is("keyword", "as")) {
619
+ consume("keyword", "as");
620
+ skip();
621
+ originalName = name;
622
+ if (is("identifier")) {
623
+ consume("identifier");
624
+ skip();
625
+ }
626
+ }
627
+
628
+ exports.push({
629
+ type: "type",
630
+ name,
631
+ originalName,
632
+ line: exportToken.line,
633
+ });
634
+ }
635
+
636
+ if (is("punctuation", ",")) {
637
+ consume("punctuation", ",");
638
+ skip();
639
+ } else {
640
+ break;
641
+ }
642
+ }
643
+ }
644
+ continue;
645
+ }
646
+
647
+ // export * from '...'
648
+ if (is("punctuation", "*")) {
649
+ consume("punctuation", "*");
650
+ skip();
651
+
652
+ let from: string | undefined;
653
+ if (is("from")) {
654
+ consume("from");
655
+ skip();
656
+ if (is("string")) {
657
+ from = consume("string")!.value.slice(1, -1);
658
+ }
659
+ }
660
+
661
+ exports.push({
662
+ type: "all",
663
+ from,
664
+ line: exportToken.line,
665
+ });
666
+ continue;
667
+ }
668
+
669
+ // export { ... }
670
+ if (is("punctuation", "{")) {
671
+ consume("punctuation", "{");
672
+ skip();
673
+
674
+ const names: { name: string; originalName?: string }[] = [];
675
+
676
+ while (!is("punctuation", "}") && !is("eof")) {
677
+ if (is("identifier")) {
678
+ const name = consume("identifier")!.value;
679
+ skip();
680
+
681
+ let originalName: string | undefined;
682
+ if (is("keyword", "as")) {
683
+ consume("keyword", "as");
684
+ skip();
685
+ originalName = name;
686
+ if (is("identifier")) {
687
+ consume("identifier");
688
+ skip();
689
+ }
690
+ }
691
+
692
+ names.push({ name, originalName });
693
+ }
694
+
695
+ if (is("punctuation", ",")) {
696
+ consume("punctuation", ",");
697
+ skip();
698
+ } else {
699
+ break;
700
+ }
701
+ }
702
+
703
+ consume("punctuation", "}");
704
+ skip();
705
+
706
+ let from: string | undefined;
707
+ if (is("from")) {
708
+ consume("from");
709
+ skip();
710
+ if (is("string")) {
711
+ from = consume("string")!.value.slice(1, -1);
712
+ }
713
+ }
714
+
715
+ for (const { name, originalName } of names) {
716
+ exports.push({
717
+ type: "named",
718
+ name,
719
+ originalName,
720
+ from,
721
+ line: exportToken.line,
722
+ });
723
+ }
724
+ continue;
725
+ }
726
+
727
+ // export const/let/var/function/class
728
+ if (
729
+ is("keyword", "const") ||
730
+ is("keyword", "let") ||
731
+ is("keyword", "var") ||
732
+ is("keyword", "function") ||
733
+ is("keyword", "class") ||
734
+ is("keyword", "interface") ||
735
+ is("keyword", "enum") ||
736
+ is("keyword", "async")
737
+ ) {
738
+ consume("keyword");
739
+ skip();
740
+
741
+ // async function
742
+ if (is("keyword", "function")) {
743
+ consume("keyword", "function");
744
+ skip();
745
+ }
746
+
747
+ if (is("identifier")) {
748
+ const name = consume("identifier")!.value;
749
+ exports.push({
750
+ type: "named",
751
+ name,
752
+ line: exportToken.line,
753
+ });
754
+ }
755
+ continue;
756
+ }
757
+ }
758
+
759
+ i++;
760
+ }
761
+
762
+ return exports;
763
+ }
764
+
765
+ // ═══════════════════════════════════════════════════════════════════════════
766
+ // Module Analysis
767
+ // ═══════════════════════════════════════════════════════════════════════════
768
+
769
+ /**
770
+ * 모듈 분석 결과
771
+ */
772
+ export interface ModuleAnalysis {
773
+ /** Import 목록 */
774
+ imports: ImportInfo[];
775
+ /** Export 목록 */
776
+ exports: ExportInfo[];
777
+ /** Public API 여부 (index 파일) */
778
+ isPublicAPI: boolean;
779
+ /** 순수 타입 모듈 여부 */
780
+ isTypeOnly: boolean;
781
+ }
782
+
783
+ /**
784
+ * 모듈 전체 분석
785
+ */
786
+ export function analyzeModuleAST(content: string, filePath: string): ModuleAnalysis {
787
+ const imports = extractImportsAST(content);
788
+ const exports = extractExportsAST(content);
789
+
790
+ const isPublicAPI =
791
+ filePath.endsWith("/index.ts") ||
792
+ filePath.endsWith("/index.tsx") ||
793
+ filePath.endsWith("/index.js");
794
+
795
+ // 타입만 있는 모듈인지 확인
796
+ const hasRuntimeExport = exports.some((e) => e.type !== "type");
797
+ const hasRuntimeImport = imports.some((i) => !i.statement.includes("import type"));
798
+ const isTypeOnly = !hasRuntimeExport && !hasRuntimeImport && exports.length > 0;
799
+
800
+ return {
801
+ imports,
802
+ exports,
803
+ isPublicAPI,
804
+ isTypeOnly,
805
+ };
806
+ }