@mandujs/core 0.54.12 → 0.54.13

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 (76) hide show
  1. package/package.json +9 -1
  2. package/scripts/postinstall-lock.ts +153 -153
  3. package/src/a11y/run-audit.ts +15 -15
  4. package/src/agent/__tests__/context.test.ts +49 -1
  5. package/src/agent/context.ts +535 -535
  6. package/src/agent/index.ts +6 -6
  7. package/src/agent/plan.ts +282 -282
  8. package/src/agent/repair.ts +171 -171
  9. package/src/agent/sync.ts +200 -200
  10. package/src/agent/types.ts +8 -0
  11. package/src/agent/verify.ts +100 -2
  12. package/src/brain/doctor/analyzer.ts +7 -7
  13. package/src/bundler/__tests__/build-runner.ts +5 -4
  14. package/src/bundler/__tests__/cold-start.test.ts +60 -60
  15. package/src/bundler/__tests__/css.test.ts +20 -20
  16. package/src/bundler/analyzer.ts +15 -15
  17. package/src/bundler/build.test.ts +66 -15
  18. package/src/bundler/build.ts +165 -103
  19. package/src/bundler/css.ts +42 -42
  20. package/src/bundler/manifest-schema.ts +21 -21
  21. package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
  22. package/src/bundler/plugins/block-generated-imports.ts +13 -13
  23. package/src/bundler/types.ts +31 -31
  24. package/src/client/island.ts +79 -79
  25. package/src/config/validate.ts +1 -1
  26. package/src/contract/schema.ts +7 -0
  27. package/src/deploy/inference/context.ts +82 -82
  28. package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
  29. package/src/devtools/client/components/panel/panel-container.tsx +1 -1
  30. package/src/error/formatter.ts +10 -1
  31. package/src/experimental/index.ts +10 -0
  32. package/src/filling/context.ts +17 -17
  33. package/src/filling/filling.ts +22 -1
  34. package/src/filling/index.ts +15 -1
  35. package/src/generator/generate.ts +30 -30
  36. package/src/generator/index.ts +3 -3
  37. package/src/generator/templates.ts +210 -210
  38. package/src/guard/check.ts +9 -9
  39. package/src/guard/config-guard.ts +13 -13
  40. package/src/guard/fs-routes-policy.ts +51 -51
  41. package/src/guard/index.ts +11 -11
  42. package/src/index.ts +0 -10
  43. package/src/internal/index.ts +25 -0
  44. package/src/kitchen/api/file-api.ts +11 -11
  45. package/src/report/index.ts +1 -1
  46. package/src/resource/__tests__/generator.test.ts +6 -6
  47. package/src/resource/__tests__/schema.test.ts +14 -14
  48. package/src/resource/ddl/__tests__/emit.test.ts +165 -165
  49. package/src/resource/ddl/emit.ts +146 -146
  50. package/src/resource/generator-schema.ts +11 -11
  51. package/src/resource/generators/slot.ts +72 -72
  52. package/src/resource/schema.ts +21 -21
  53. package/src/router/client-entry.test.ts +69 -33
  54. package/src/router/client-entry.ts +134 -74
  55. package/src/router/fs-routes.ts +24 -22
  56. package/src/router/fs-scanner.ts +21 -17
  57. package/src/router/fs-types.ts +8 -5
  58. package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
  59. package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
  60. package/src/runtime/__tests__/page-render-response.test.ts +103 -103
  61. package/src/runtime/__tests__/request-middleware.test.ts +70 -70
  62. package/src/runtime/devtools-adapter.ts +68 -68
  63. package/src/runtime/escape.ts +34 -34
  64. package/src/runtime/image-feature.ts +15 -0
  65. package/src/runtime/observability-lifecycle.ts +290 -290
  66. package/src/runtime/page-render-response.ts +106 -106
  67. package/src/runtime/rate-limit.ts +231 -0
  68. package/src/runtime/request-middleware.ts +31 -31
  69. package/src/runtime/scheduler-lifecycle.ts +64 -0
  70. package/src/runtime/server.ts +27 -295
  71. package/src/runtime/ssr.ts +59 -59
  72. package/src/runtime/static-files.ts +289 -289
  73. package/src/runtime/streaming-ssr.ts +22 -22
  74. package/src/spec/schema.ts +4 -3
  75. package/src/watcher/__tests__/watcher.test.ts +59 -59
  76. package/src/watcher/watcher.ts +61 -61
@@ -1,4 +1,4 @@
1
- import { needsHydration, type RouteSpec } from "../spec/schema";
1
+ import { needsHydration, type RouteSpec } from "../spec/schema";
2
2
  import { GENERATED_RELATIVE_PATHS } from "../paths";
3
3
 
4
4
  export function generateApiHandler(route: RouteSpec): string {
@@ -238,34 +238,34 @@ function computeSlotImportPath(slotModule: string, fromDir: string): string {
238
238
  return result;
239
239
  }
240
240
 
241
- export function generatePageComponent(route: RouteSpec): string {
242
- // Island-First: clientModule이 있으면 Island render를 SSR에서 직접 사용
243
- if (route.clientModule) {
244
- return generatePageComponentWithIsland(route);
245
- }
246
-
247
- if (needsHydration(route)) {
248
- throw new Error(
249
- `[${route.id}] Route has hydration strategy "${route.hydration?.strategy}" but no clientModule. ` +
250
- "Refusing to generate a placeholder page because it would disagree with runtime hydration state.",
251
- );
252
- }
253
-
254
- // slotModule이 있으면 PageHandler 형식으로 생성 (filling 포함)
255
- if (route.slotModule) {
256
- return generatePageHandlerWithSlot(route);
257
- }
258
-
259
- if (route.kind === "page" && route.componentModule) {
260
- return generatePageComponentFromModule(route);
261
- }
262
-
263
- const pageName = toPascalCase(route.id);
264
-
265
- // Legacy fallback for malformed historical manifests that lack componentModule.
266
- return `// Generated by Mandu - DO NOT EDIT DIRECTLY
267
- // Route ID: ${route.id}
268
- // Pattern: ${route.pattern}
241
+ export function generatePageComponent(route: RouteSpec): string {
242
+ // Island-First: clientModule이 있으면 Island render를 SSR에서 직접 사용
243
+ if (route.clientModule) {
244
+ return generatePageComponentWithIsland(route);
245
+ }
246
+
247
+ if (needsHydration(route)) {
248
+ throw new Error(
249
+ `[${route.id}] Route has hydration strategy "${route.hydration?.strategy}" but no clientModule. ` +
250
+ "Refusing to generate a placeholder page because it would disagree with runtime hydration state.",
251
+ );
252
+ }
253
+
254
+ // slotModule이 있으면 PageHandler 형식으로 생성 (filling 포함)
255
+ if (route.slotModule) {
256
+ return generatePageHandlerWithSlot(route);
257
+ }
258
+
259
+ if (route.kind === "page" && route.componentModule) {
260
+ return generatePageComponentFromModule(route);
261
+ }
262
+
263
+ const pageName = toPascalCase(route.id);
264
+
265
+ // Legacy fallback for malformed historical manifests that lack componentModule.
266
+ return `// Generated by Mandu - DO NOT EDIT DIRECTLY
267
+ // Route ID: ${route.id}
268
+ // Pattern: ${route.pattern}
269
269
 
270
270
  import React from "react";
271
271
 
@@ -281,31 +281,31 @@ export default function ${pageName}Page({ params }: Props): React.ReactElement {
281
281
  React.createElement("p", null, "Pattern: ${route.pattern}")
282
282
  );
283
283
  }
284
- `;
285
- }
286
-
287
- export function generatePageComponentFromModule(route: RouteSpec): string {
288
- const pageName = toPascalCase(route.id);
289
- const pageImportPath = computeSlotImportPath(route.componentModule!, GENERATED_RELATIVE_PATHS.webRoutes);
290
-
291
- return `// Generated by Mandu - DO NOT EDIT DIRECTLY
292
- // Route ID: ${route.id}
293
- // Pattern: ${route.pattern}
294
- // Page Module: ${route.componentModule}
295
-
296
- import React from "react";
297
- import pageModule from "${pageImportPath}";
298
-
299
- interface Props {
300
- params: Record<string, string>;
301
- loaderData?: unknown;
302
- }
303
-
304
- export default function ${pageName}Page(props: Props): React.ReactElement {
305
- return React.createElement(pageModule as React.ComponentType<Props>, props);
306
- }
307
- `;
308
- }
284
+ `;
285
+ }
286
+
287
+ export function generatePageComponentFromModule(route: RouteSpec): string {
288
+ const pageName = toPascalCase(route.id);
289
+ const pageImportPath = computeSlotImportPath(route.componentModule!, GENERATED_RELATIVE_PATHS.webRoutes);
290
+
291
+ return `// Generated by Mandu - DO NOT EDIT DIRECTLY
292
+ // Route ID: ${route.id}
293
+ // Pattern: ${route.pattern}
294
+ // Page Module: ${route.componentModule}
295
+
296
+ import React from "react";
297
+ import pageModule from "${pageImportPath}";
298
+
299
+ interface Props {
300
+ params: Record<string, string>;
301
+ loaderData?: unknown;
302
+ }
303
+
304
+ export default function ${pageName}Page(props: Props): React.ReactElement {
305
+ return React.createElement(pageModule as React.ComponentType<Props>, props);
306
+ }
307
+ `;
308
+ }
309
309
 
310
310
  /**
311
311
  * Island-First Rendering: SSR이 island의 render 함수를 직접 사용
@@ -313,51 +313,51 @@ export default function ${pageName}Page(props: Props): React.ReactElement {
313
313
  * - SSR과 클라이언트가 동일한 렌더링 로직 사용 → 불일치 구조적 방지
314
314
  * - slotModule 유무에 따라 두 가지 변형 생성
315
315
  */
316
- export function generatePageComponentWithIsland(route: RouteSpec): string {
317
- const pageName = toPascalCase(route.id);
318
- const clientImportPath = computeSlotImportPath(route.clientModule!, GENERATED_RELATIVE_PATHS.webRoutes);
319
- const pageImportPath = route.kind === "page" && route.componentModule
320
- ? computeSlotImportPath(route.componentModule, GENERATED_RELATIVE_PATHS.webRoutes)
321
- : null;
322
- const pageModuleComment = route.kind === "page" && route.componentModule
323
- ? `// Page Module: ${route.componentModule}\n`
324
- : "";
325
- const shouldImportPageModule = !!pageImportPath &&
326
- normalizeRouteModulePath(route.componentModule) !== normalizeRouteModulePath(route.clientModule);
327
- const pageImport = shouldImportPageModule ? `import pageModule from "${pageImportPath}";\n` : "";
328
- const pageRenderTarget = shouldImportPageModule ? "pageModule" : "islandModule";
329
- const renderHelper = generateClientBackedPageRenderHelper(pageRenderTarget, route.id);
330
-
331
- // clientModule + slotModule → PageRegistration 형식
332
- if (route.slotModule) {
333
- const slotImportPath = computeSlotImportPath(route.slotModule!, GENERATED_RELATIVE_PATHS.webRoutes);
316
+ export function generatePageComponentWithIsland(route: RouteSpec): string {
317
+ const pageName = toPascalCase(route.id);
318
+ const clientImportPath = computeSlotImportPath(route.clientModule!, GENERATED_RELATIVE_PATHS.webRoutes);
319
+ const pageImportPath = route.kind === "page" && route.componentModule
320
+ ? computeSlotImportPath(route.componentModule, GENERATED_RELATIVE_PATHS.webRoutes)
321
+ : null;
322
+ const pageModuleComment = route.kind === "page" && route.componentModule
323
+ ? `// Page Module: ${route.componentModule}\n`
324
+ : "";
325
+ const shouldImportPageModule = !!pageImportPath &&
326
+ normalizeRouteModulePath(route.componentModule) !== normalizeRouteModulePath(route.clientModule);
327
+ const pageImport = shouldImportPageModule ? `import pageModule from "${pageImportPath}";\n` : "";
328
+ const pageRenderTarget = shouldImportPageModule ? "pageModule" : "islandModule";
329
+ const renderHelper = generateClientBackedPageRenderHelper(pageRenderTarget, route.id);
330
+
331
+ // clientModule + slotModule → PageRegistration 형식
332
+ if (route.slotModule) {
333
+ const slotImportPath = computeSlotImportPath(route.slotModule!, GENERATED_RELATIVE_PATHS.webRoutes);
334
334
 
335
335
  return `// Generated by Mandu - DO NOT EDIT DIRECTLY
336
336
  // Island-First Rendering + Slot Module
337
337
  // Route ID: ${route.id}
338
- // Pattern: ${route.pattern}
339
- // Client Module: ${route.clientModule}
340
- // Slot Module: ${route.slotModule}
341
- ${pageModuleComment}
342
-
343
- import React from "react";
344
- import filling from "${slotImportPath}";
345
- import islandModule from "${clientImportPath}";
346
- ${pageImport}
347
-
348
- interface Props {
349
- params: Record<string, string>;
350
- loaderData?: unknown;
351
- }
352
-
353
- ${renderHelper}
354
-
355
- function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
356
- const serverData = (loaderData || {}) as Record<string, unknown>;
357
- return renderClientBackedPage({ params, loaderData }, serverData);
358
- }
359
-
360
- // PageRegistration 형식으로 export (server.ts의 registerPageHandler용)
338
+ // Pattern: ${route.pattern}
339
+ // Client Module: ${route.clientModule}
340
+ // Slot Module: ${route.slotModule}
341
+ ${pageModuleComment}
342
+
343
+ import React from "react";
344
+ import filling from "${slotImportPath}";
345
+ import islandModule from "${clientImportPath}";
346
+ ${pageImport}
347
+
348
+ interface Props {
349
+ params: Record<string, string>;
350
+ loaderData?: unknown;
351
+ }
352
+
353
+ ${renderHelper}
354
+
355
+ function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
356
+ const serverData = (loaderData || {}) as Record<string, unknown>;
357
+ return renderClientBackedPage({ params, loaderData }, serverData);
358
+ }
359
+
360
+ // PageRegistration 형식으로 export (server.ts의 registerPageHandler용)
361
361
  export default {
362
362
  component: ${pageName}Page,
363
363
  filling: filling,
@@ -368,124 +368,124 @@ export default {
368
368
  // clientModule만 (slotModule 없음) → default export 컴포넌트
369
369
  return `// Generated by Mandu - DO NOT EDIT DIRECTLY
370
370
  // Island-First Rendering: SSR이 island render 직접 사용
371
- // Route ID: ${route.id}
372
- // Pattern: ${route.pattern}
373
- // Client Module: ${route.clientModule}
374
- ${pageModuleComment}
375
-
376
- import React from "react";
377
- import islandModule from "${clientImportPath}";
378
- ${pageImport}
379
-
380
- interface Props {
381
- params: Record<string, string>;
382
- loaderData?: unknown;
383
- }
384
-
385
- ${renderHelper}
386
-
387
- export default function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
388
- const serverData = (loaderData || {}) as Record<string, unknown>;
389
- return renderClientBackedPage({ params, loaderData }, serverData);
390
- }
391
- `;
392
- }
371
+ // Route ID: ${route.id}
372
+ // Pattern: ${route.pattern}
373
+ // Client Module: ${route.clientModule}
374
+ ${pageModuleComment}
375
+
376
+ import React from "react";
377
+ import islandModule from "${clientImportPath}";
378
+ ${pageImport}
379
+
380
+ interface Props {
381
+ params: Record<string, string>;
382
+ loaderData?: unknown;
383
+ }
384
+
385
+ ${renderHelper}
386
+
387
+ export default function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
388
+ const serverData = (loaderData || {}) as Record<string, unknown>;
389
+ return renderClientBackedPage({ params, loaderData }, serverData);
390
+ }
391
+ `;
392
+ }
393
393
 
394
394
  /**
395
395
  * slotModule이 있는 Page Route용 Handler 생성
396
396
  * - component와 filling을 함께 export
397
397
  * - server.ts에서 filling.executeLoader() 호출 가능
398
398
  */
399
- export function generatePageHandlerWithSlot(route: RouteSpec): string {
400
- const pageName = toPascalCase(route.id);
401
- const slotImportPath = computeSlotImportPath(route.slotModule!, GENERATED_RELATIVE_PATHS.serverRoutes);
402
- const pageImportPath = route.kind === "page" && route.componentModule
403
- ? computeSlotImportPath(route.componentModule, GENERATED_RELATIVE_PATHS.serverRoutes)
404
- : null;
405
- const pageImport = pageImportPath ? `import pageModule from "${pageImportPath}";\n` : "";
406
- const pageRender = pageImportPath
407
- ? ` return React.createElement(pageModule as React.ComponentType<Props>, { params, loaderData });`
408
- : ` return React.createElement("div", null,
409
- React.createElement("h1", null, "${pageName} Page"),
410
- React.createElement("p", null, "Route ID: ${route.id}"),
411
- React.createElement("p", null, "Pattern: ${route.pattern}"),
412
- loaderData ? React.createElement("pre", null, JSON.stringify(loaderData, null, 2)) : null
413
- );`;
414
-
415
- return `// Generated by Mandu - DO NOT EDIT DIRECTLY
416
- // Route ID: ${route.id}
417
- // Pattern: ${route.pattern}
418
- // Slot Module: ${route.slotModule}
419
-
420
- import React from "react";
421
- import filling from "${slotImportPath}";
422
- ${pageImport}
423
-
424
- interface Props {
425
- params: Record<string, string>;
426
- loaderData?: unknown;
427
- }
428
-
429
- function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
430
- ${pageRender}
431
- }
432
-
433
- // PageRegistration 형식으로 export (server.ts의 registerPageHandler용)
399
+ export function generatePageHandlerWithSlot(route: RouteSpec): string {
400
+ const pageName = toPascalCase(route.id);
401
+ const slotImportPath = computeSlotImportPath(route.slotModule!, GENERATED_RELATIVE_PATHS.serverRoutes);
402
+ const pageImportPath = route.kind === "page" && route.componentModule
403
+ ? computeSlotImportPath(route.componentModule, GENERATED_RELATIVE_PATHS.serverRoutes)
404
+ : null;
405
+ const pageImport = pageImportPath ? `import pageModule from "${pageImportPath}";\n` : "";
406
+ const pageRender = pageImportPath
407
+ ? ` return React.createElement(pageModule as React.ComponentType<Props>, { params, loaderData });`
408
+ : ` return React.createElement("div", null,
409
+ React.createElement("h1", null, "${pageName} Page"),
410
+ React.createElement("p", null, "Route ID: ${route.id}"),
411
+ React.createElement("p", null, "Pattern: ${route.pattern}"),
412
+ loaderData ? React.createElement("pre", null, JSON.stringify(loaderData, null, 2)) : null
413
+ );`;
414
+
415
+ return `// Generated by Mandu - DO NOT EDIT DIRECTLY
416
+ // Route ID: ${route.id}
417
+ // Pattern: ${route.pattern}
418
+ // Slot Module: ${route.slotModule}
419
+
420
+ import React from "react";
421
+ import filling from "${slotImportPath}";
422
+ ${pageImport}
423
+
424
+ interface Props {
425
+ params: Record<string, string>;
426
+ loaderData?: unknown;
427
+ }
428
+
429
+ function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
430
+ ${pageRender}
431
+ }
432
+
433
+ // PageRegistration 형식으로 export (server.ts의 registerPageHandler용)
434
434
  export default {
435
435
  component: ${pageName}Page,
436
436
  filling: filling,
437
437
  };
438
- `;
439
- }
440
-
441
- function generateClientBackedPageRenderHelper(pageRenderTarget: string, routeId: string): string {
442
- return `type ManduIslandModule = {
443
- __mandu_island: true;
444
- definition: {
445
- setup: (serverData: Record<string, unknown>) => unknown;
446
- render: (props: unknown) => React.ReactNode;
447
- };
448
- };
449
-
450
- function isManduIslandModule(value: unknown): value is ManduIslandModule {
451
- const candidate = value as Partial<ManduIslandModule> | null;
452
- return !!(
453
- candidate &&
454
- typeof candidate === "object" &&
455
- candidate.__mandu_island === true &&
456
- typeof candidate.definition?.setup === "function" &&
457
- typeof candidate.definition?.render === "function"
458
- );
459
- }
460
-
461
- function renderClientBackedPage(props: Props, serverData: Record<string, unknown>): React.ReactElement {
462
- if (isManduIslandModule(islandModule)) {
463
- const setupResult = islandModule.definition.setup(serverData);
464
- return islandModule.definition.render(setupResult) as React.ReactElement;
465
- }
466
-
467
- if (typeof ${pageRenderTarget} === "function") {
468
- return React.createElement(${pageRenderTarget} as React.ComponentType<Props>, props);
469
- }
470
-
471
- if (typeof islandModule === "function") {
472
- return React.createElement(islandModule as React.ComponentType<Record<string, unknown>>, serverData);
473
- }
474
-
475
- if (React.isValidElement(${pageRenderTarget})) {
476
- return ${pageRenderTarget} as React.ReactElement;
477
- }
478
-
479
- if (React.isValidElement(islandModule)) {
480
- return islandModule as React.ReactElement;
481
- }
482
-
483
- throw new Error("[Mandu] Route ${routeId} clientModule must export a Mandu island or React component.");
484
- }`;
485
- }
486
-
487
- /**
488
- * Convert string to PascalCase (handles kebab-case, snake_case)
438
+ `;
439
+ }
440
+
441
+ function generateClientBackedPageRenderHelper(pageRenderTarget: string, routeId: string): string {
442
+ return `type ManduIslandModule = {
443
+ __mandu_island: true;
444
+ definition: {
445
+ setup: (serverData: Record<string, unknown>) => unknown;
446
+ render: (props: unknown) => React.ReactNode;
447
+ };
448
+ };
449
+
450
+ function isManduIslandModule(value: unknown): value is ManduIslandModule {
451
+ const candidate = value as Partial<ManduIslandModule> | null;
452
+ return !!(
453
+ candidate &&
454
+ typeof candidate === "object" &&
455
+ candidate.__mandu_island === true &&
456
+ typeof candidate.definition?.setup === "function" &&
457
+ typeof candidate.definition?.render === "function"
458
+ );
459
+ }
460
+
461
+ function renderClientBackedPage(props: Props, serverData: Record<string, unknown>): React.ReactElement {
462
+ if (isManduIslandModule(islandModule)) {
463
+ const setupResult = islandModule.definition.setup(serverData);
464
+ return islandModule.definition.render(setupResult) as React.ReactElement;
465
+ }
466
+
467
+ if (typeof ${pageRenderTarget} === "function") {
468
+ return React.createElement(${pageRenderTarget} as React.ComponentType<Props>, props);
469
+ }
470
+
471
+ if (typeof islandModule === "function") {
472
+ return React.createElement(islandModule as React.ComponentType<Record<string, unknown>>, serverData);
473
+ }
474
+
475
+ if (React.isValidElement(${pageRenderTarget})) {
476
+ return ${pageRenderTarget} as React.ReactElement;
477
+ }
478
+
479
+ if (React.isValidElement(islandModule)) {
480
+ return islandModule as React.ReactElement;
481
+ }
482
+
483
+ throw new Error("[Mandu] Route ${routeId} clientModule must export a Mandu island or React component.");
484
+ }`;
485
+ }
486
+
487
+ /**
488
+ * Convert string to PascalCase (handles kebab-case, snake_case)
489
489
  * "todo-page" → "TodoPage"
490
490
  * "user_profile" → "UserProfile"
491
491
  */
@@ -496,10 +496,10 @@ function toPascalCase(str: string): string {
496
496
  .join("");
497
497
  }
498
498
 
499
- function pathDirname(filePath: string): string {
500
- return filePath.replace(/\\/g, "/").split("/").slice(0, -1).join("/");
501
- }
502
-
503
- function normalizeRouteModulePath(filePath: string | undefined): string {
504
- return (filePath ?? "").replace(/\\/g, "/").replace(/^\.\//, "");
505
- }
499
+ function pathDirname(filePath: string): string {
500
+ return filePath.replace(/\\/g, "/").split("/").slice(0, -1).join("/");
501
+ }
502
+
503
+ function normalizeRouteModulePath(filePath: string | undefined): string {
504
+ return (filePath ?? "").replace(/\\/g, "/").replace(/^\.\//, "");
505
+ }
@@ -30,21 +30,21 @@ export const GENERATED_IMPORT_DOCS_URL =
30
30
  "https://mandujs.com/docs/architect/generated-access";
31
31
 
32
32
  /**
33
- * Build the user-facing message for a detected direct generated-artifact
34
- * import. `specifier` is the literal import string that tripped the guard
35
- * (not the resolved path).
33
+ * Build the user-facing message for a detected direct generated-artifact
34
+ * import. `specifier` is the literal import string that tripped the guard
35
+ * (not the resolved path).
36
36
  *
37
37
  * This helper is the single source of truth for the message text — both
38
38
  * the static Guard pass (`checkInvalidGeneratedImport`) and the bundler
39
39
  * plugin (`blockGeneratedImports`) call through it so the two paths
40
40
  * cannot drift.
41
41
  */
42
- export function buildForbiddenGeneratedImportMessage(specifier: string): string {
43
- return (
44
- `Direct generated artifact imports are forbidden: ${specifier}. ` +
45
- `Use the runtime registry: see ${GENERATED_IMPORT_DOCS_URL}`
46
- );
47
- }
42
+ export function buildForbiddenGeneratedImportMessage(specifier: string): string {
43
+ return (
44
+ `Direct generated artifact imports are forbidden: ${specifier}. ` +
45
+ `Use the runtime registry: see ${GENERATED_IMPORT_DOCS_URL}`
46
+ );
47
+ }
48
48
 
49
49
  /**
50
50
  * Shared remediation hint. Points at `getGenerated()` from
@@ -187,19 +187,19 @@ export function formatConfigGuardResult(result: ConfigGuardResult): string {
187
187
  } else if (!result.lockfileExists) {
188
188
  lines.push("💡 Lockfile 없음");
189
189
  lines.push(" 'mandu lock'으로 생성 권장");
190
- } else {
191
- lines.push("❌ 설정 무결성 검증 실패");
192
-
193
- for (const error of result.errors) {
194
- lines.push(` 🔴 ${error.message}`);
195
- }
196
-
197
- lines.push("");
198
- lines.push(" 다음 단계:");
199
- lines.push(" ↳ 변경 확인: mandu lock --diff");
200
- lines.push(" ↳ 의도한 변경이면: mandu lock");
201
- lines.push(" ↳ 패키지 업데이트 직후라면 새 Mandu 기본값으로 lockfile을 갱신하세요.");
202
- }
190
+ } else {
191
+ lines.push("❌ 설정 무결성 검증 실패");
192
+
193
+ for (const error of result.errors) {
194
+ lines.push(` 🔴 ${error.message}`);
195
+ }
196
+
197
+ lines.push("");
198
+ lines.push(" 다음 단계:");
199
+ lines.push(" ↳ 변경 확인: mandu lock --diff");
200
+ lines.push(" ↳ 의도한 변경이면: mandu lock");
201
+ lines.push(" ↳ 패키지 업데이트 직후라면 새 Mandu 기본값으로 lockfile을 갱신하세요.");
202
+ }
203
203
 
204
204
  if (result.warnings.length > 0 && result.lockfileExists) {
205
205
  lines.push("");
@@ -1,51 +1,51 @@
1
- import type { FSRoutesGuardConfig } from "./types";
2
-
3
- export const DEFAULT_FS_ROUTES_GUARD_POLICY: FSRoutesGuardConfig = {
4
- noPageToPage: true,
5
- pageCanImport: [
6
- "client/pages",
7
- "client/widgets",
8
- "client/features",
9
- "client/entities",
10
- "client/shared",
11
- "shared/contracts",
12
- "shared/types",
13
- "shared/utils/client",
14
- ],
15
- layoutCanImport: [
16
- "client/app",
17
- "client/widgets",
18
- "client/shared",
19
- "shared/contracts",
20
- "shared/types",
21
- "shared/utils/client",
22
- ],
23
- routeCanImport: [
24
- "server/api",
25
- "server/application",
26
- "server/domain",
27
- "server/infra",
28
- "server/core",
29
- "shared/contracts",
30
- "shared/schema",
31
- "shared/types",
32
- "shared/utils/client",
33
- "shared/utils/server",
34
- "shared/env",
35
- ],
36
- };
37
-
38
- function clonePolicy(policy: FSRoutesGuardConfig): FSRoutesGuardConfig {
39
- return {
40
- noPageToPage: policy.noPageToPage,
41
- pageCanImport: policy.pageCanImport ? [...policy.pageCanImport] : undefined,
42
- layoutCanImport: policy.layoutCanImport ? [...policy.layoutCanImport] : undefined,
43
- routeCanImport: policy.routeCanImport ? [...policy.routeCanImport] : undefined,
44
- };
45
- }
46
-
47
- export function getDefaultFsRoutesGuardPolicy(
48
- enabled: boolean
49
- ): FSRoutesGuardConfig | undefined {
50
- return enabled ? clonePolicy(DEFAULT_FS_ROUTES_GUARD_POLICY) : undefined;
51
- }
1
+ import type { FSRoutesGuardConfig } from "./types";
2
+
3
+ export const DEFAULT_FS_ROUTES_GUARD_POLICY: FSRoutesGuardConfig = {
4
+ noPageToPage: true,
5
+ pageCanImport: [
6
+ "client/pages",
7
+ "client/widgets",
8
+ "client/features",
9
+ "client/entities",
10
+ "client/shared",
11
+ "shared/contracts",
12
+ "shared/types",
13
+ "shared/utils/client",
14
+ ],
15
+ layoutCanImport: [
16
+ "client/app",
17
+ "client/widgets",
18
+ "client/shared",
19
+ "shared/contracts",
20
+ "shared/types",
21
+ "shared/utils/client",
22
+ ],
23
+ routeCanImport: [
24
+ "server/api",
25
+ "server/application",
26
+ "server/domain",
27
+ "server/infra",
28
+ "server/core",
29
+ "shared/contracts",
30
+ "shared/schema",
31
+ "shared/types",
32
+ "shared/utils/client",
33
+ "shared/utils/server",
34
+ "shared/env",
35
+ ],
36
+ };
37
+
38
+ function clonePolicy(policy: FSRoutesGuardConfig): FSRoutesGuardConfig {
39
+ return {
40
+ noPageToPage: policy.noPageToPage,
41
+ pageCanImport: policy.pageCanImport ? [...policy.pageCanImport] : undefined,
42
+ layoutCanImport: policy.layoutCanImport ? [...policy.layoutCanImport] : undefined,
43
+ routeCanImport: policy.routeCanImport ? [...policy.routeCanImport] : undefined,
44
+ };
45
+ }
46
+
47
+ export function getDefaultFsRoutesGuardPolicy(
48
+ enabled: boolean
49
+ ): FSRoutesGuardConfig | undefined {
50
+ return enabled ? clonePolicy(DEFAULT_FS_ROUTES_GUARD_POLICY) : undefined;
51
+ }
@@ -120,17 +120,17 @@ export {
120
120
  // Architecture Guard - Watcher
121
121
  // ═══════════════════════════════════════════════════════════════════════════
122
122
 
123
- export {
124
- createGuardWatcher,
125
- checkFile,
126
- checkDirectory,
127
- clearAnalysisCache,
128
- } from "./watcher";
129
-
130
- export {
131
- DEFAULT_FS_ROUTES_GUARD_POLICY,
132
- getDefaultFsRoutesGuardPolicy,
133
- } from "./fs-routes-policy";
123
+ export {
124
+ createGuardWatcher,
125
+ checkFile,
126
+ checkDirectory,
127
+ clearAnalysisCache,
128
+ } from "./watcher";
129
+
130
+ export {
131
+ DEFAULT_FS_ROUTES_GUARD_POLICY,
132
+ getDefaultFsRoutesGuardPolicy,
133
+ } from "./fs-routes-policy";
134
134
 
135
135
  // ═══════════════════════════════════════════════════════════════════════════
136
136
  // Architecture Guard - Presets