@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.
- package/package.json +9 -1
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/run-audit.ts +15 -15
- package/src/agent/__tests__/context.test.ts +49 -1
- package/src/agent/context.ts +535 -535
- package/src/agent/index.ts +6 -6
- package/src/agent/plan.ts +282 -282
- package/src/agent/repair.ts +171 -171
- package/src/agent/sync.ts +200 -200
- package/src/agent/types.ts +8 -0
- package/src/agent/verify.ts +100 -2
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__tests__/build-runner.ts +5 -4
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +20 -20
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/build.test.ts +66 -15
- package/src/bundler/build.ts +165 -103
- package/src/bundler/css.ts +42 -42
- package/src/bundler/manifest-schema.ts +21 -21
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- package/src/bundler/plugins/block-generated-imports.ts +13 -13
- package/src/bundler/types.ts +31 -31
- package/src/client/island.ts +79 -79
- package/src/config/validate.ts +1 -1
- package/src/contract/schema.ts +7 -0
- package/src/deploy/inference/context.ts +82 -82
- package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/error/formatter.ts +10 -1
- package/src/experimental/index.ts +10 -0
- package/src/filling/context.ts +17 -17
- package/src/filling/filling.ts +22 -1
- package/src/filling/index.ts +15 -1
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +210 -210
- package/src/guard/check.ts +9 -9
- package/src/guard/config-guard.ts +13 -13
- package/src/guard/fs-routes-policy.ts +51 -51
- package/src/guard/index.ts +11 -11
- package/src/index.ts +0 -10
- package/src/internal/index.ts +25 -0
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +6 -6
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/emit.ts +146 -146
- package/src/resource/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +69 -33
- package/src/router/client-entry.ts +134 -74
- package/src/router/fs-routes.ts +24 -22
- package/src/router/fs-scanner.ts +21 -17
- package/src/router/fs-types.ts +8 -5
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +103 -103
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/image-feature.ts +15 -0
- package/src/runtime/observability-lifecycle.ts +290 -290
- package/src/runtime/page-render-response.ts +106 -106
- package/src/runtime/rate-limit.ts +231 -0
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/scheduler-lifecycle.ts +64 -0
- package/src/runtime/server.ts +27 -295
- package/src/runtime/ssr.ts +59 -59
- package/src/runtime/static-files.ts +289 -289
- package/src/runtime/streaming-ssr.ts +22 -22
- package/src/spec/schema.ts +4 -3
- package/src/watcher/__tests__/watcher.test.ts +59 -59
- 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
|
+
}
|
package/src/guard/check.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/guard/index.ts
CHANGED
|
@@ -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
|