@mandujs/core 0.54.11 → 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 +208 -200
- 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 +73 -7
- package/src/bundler/build.ts +139 -31
- 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
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
*
|
|
29
29
|
* URL safety model (applies to `shared.runtime`, `shared.vendor`,
|
|
30
30
|
* `shared.router`, `shared.fastRefresh.glue`, `shared.fastRefresh.runtime`,
|
|
31
|
-
* `bundles[].js`, `bundles[].css`, `islands[].js`, `partials[].js`,
|
|
32
|
-
* `importMap.imports[*]`):
|
|
31
|
+
* `bundles[].js`, `bundles[].css`, `islands[].js`, `partials[].js`,
|
|
32
|
+
* `importMap.imports[*]`):
|
|
33
33
|
*
|
|
34
34
|
* ALLOW: absolute paths rooted at `/.mandu/client/` ending in `.js` or `.css`.
|
|
35
35
|
* The bundler itself only ever emits this shape.
|
|
@@ -162,20 +162,20 @@ const BundleEntrySchema = z.object({
|
|
|
162
162
|
priority: PrioritySchema,
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
const IslandEntrySchema = z.object({
|
|
166
|
-
js: safeManduUrl("islands[].js"),
|
|
167
|
-
route: z.string().min(1),
|
|
168
|
-
priority: PrioritySchema,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const PartialEntrySchema = z.object({
|
|
172
|
-
js: safeManduUrl("partials[].js"),
|
|
173
|
-
priority: PrioritySchema,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const FastRefreshSchema = z.object({
|
|
177
|
-
runtime: safeManduUrl("shared.fastRefresh.runtime"),
|
|
178
|
-
glue: safeManduUrl("shared.fastRefresh.glue"),
|
|
165
|
+
const IslandEntrySchema = z.object({
|
|
166
|
+
js: safeManduUrl("islands[].js"),
|
|
167
|
+
route: z.string().min(1),
|
|
168
|
+
priority: PrioritySchema,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const PartialEntrySchema = z.object({
|
|
172
|
+
js: safeManduUrl("partials[].js"),
|
|
173
|
+
priority: PrioritySchema,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const FastRefreshSchema = z.object({
|
|
177
|
+
runtime: safeManduUrl("shared.fastRefresh.runtime"),
|
|
178
|
+
glue: safeManduUrl("shared.fastRefresh.glue"),
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
const SharedSchema = z.object({
|
|
@@ -225,11 +225,11 @@ export const BundleManifestSchema = z
|
|
|
225
225
|
.object({
|
|
226
226
|
version: z.number().int().min(1),
|
|
227
227
|
buildTime: z.string().min(1),
|
|
228
|
-
env: z.enum(["development", "production"]),
|
|
229
|
-
bundles: z.record(z.string(), BundleEntrySchema),
|
|
230
|
-
islands: z.record(z.string(), IslandEntrySchema).optional(),
|
|
231
|
-
partials: z.record(z.string(), PartialEntrySchema).optional(),
|
|
232
|
-
shared: SharedSchema,
|
|
228
|
+
env: z.enum(["development", "production"]),
|
|
229
|
+
bundles: z.record(z.string(), BundleEntrySchema),
|
|
230
|
+
islands: z.record(z.string(), IslandEntrySchema).optional(),
|
|
231
|
+
partials: z.record(z.string(), PartialEntrySchema).optional(),
|
|
232
|
+
shared: SharedSchema,
|
|
233
233
|
importMap: ImportMapSchema.optional(),
|
|
234
234
|
})
|
|
235
235
|
.strict();
|
|
@@ -80,19 +80,19 @@ describe("defaultAllowImporter", () => {
|
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
describe("DEFAULT_BLOCK_FILTER", () => {
|
|
83
|
-
test("matches __generated__ specifiers", () => {
|
|
84
|
-
expect("./__generated__/foo").toMatch(DEFAULT_BLOCK_FILTER);
|
|
85
|
-
expect("../../src/__generated__/routes").toMatch(DEFAULT_BLOCK_FILTER);
|
|
86
|
-
});
|
|
87
|
-
test("matches direct .mandu/generated relative specifiers", () => {
|
|
88
|
-
expect("../.mandu/generated/routes").toMatch(DEFAULT_BLOCK_FILTER);
|
|
89
|
-
expect("../../../.mandu/generated/server/repos/party.repo").toMatch(DEFAULT_BLOCK_FILTER);
|
|
90
|
-
});
|
|
91
|
-
test("does NOT match look-alikes without double underscores", () => {
|
|
92
|
-
expect("./generated/foo".match(DEFAULT_BLOCK_FILTER)).toBeNull();
|
|
93
|
-
expect("./src/generate/foo".match(DEFAULT_BLOCK_FILTER)).toBeNull();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
83
|
+
test("matches __generated__ specifiers", () => {
|
|
84
|
+
expect("./__generated__/foo").toMatch(DEFAULT_BLOCK_FILTER);
|
|
85
|
+
expect("../../src/__generated__/routes").toMatch(DEFAULT_BLOCK_FILTER);
|
|
86
|
+
});
|
|
87
|
+
test("matches direct .mandu/generated relative specifiers", () => {
|
|
88
|
+
expect("../.mandu/generated/routes").toMatch(DEFAULT_BLOCK_FILTER);
|
|
89
|
+
expect("../../../.mandu/generated/server/repos/party.repo").toMatch(DEFAULT_BLOCK_FILTER);
|
|
90
|
+
});
|
|
91
|
+
test("does NOT match look-alikes without double underscores", () => {
|
|
92
|
+
expect("./generated/foo".match(DEFAULT_BLOCK_FILTER)).toBeNull();
|
|
93
|
+
expect("./src/generate/foo".match(DEFAULT_BLOCK_FILTER)).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
96
|
|
|
97
97
|
describe("defaultBundlerPlugins", () => {
|
|
98
98
|
test("installs block-generated-imports by default", () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bun bundler plugin — hard-fail on direct generated-artifact imports.
|
|
2
|
+
* Bun bundler plugin — hard-fail on direct generated-artifact imports.
|
|
3
3
|
*
|
|
4
4
|
* Background
|
|
5
5
|
* ──────────
|
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
* but it only runs when the user (or CI) invokes `mandu guard check`.
|
|
9
9
|
* Autonomous coding agents routinely bypass that step. This plugin closes
|
|
10
10
|
* the gap at the bundler level: every `mandu dev` / `mandu build` pass
|
|
11
|
-
* installs it by default, and any import whose specifier targets
|
|
12
|
-
* `__generated__` or `.mandu/generated` fails the build with a structured,
|
|
13
|
-
* actionable error.
|
|
11
|
+
* installs it by default, and any import whose specifier targets
|
|
12
|
+
* `__generated__` or `.mandu/generated` fails the build with a structured,
|
|
13
|
+
* actionable error.
|
|
14
14
|
*
|
|
15
15
|
* Design
|
|
16
16
|
* ──────
|
|
17
|
-
* - `onResolve({ filter: DEFAULT_BLOCK_FILTER })` — Bun hands us every
|
|
18
|
-
* import whose *specifier* matches the regex, along with the importer's
|
|
19
|
-
* path (`args.importer`). We never return a result; we always throw.
|
|
17
|
+
* - `onResolve({ filter: DEFAULT_BLOCK_FILTER })` — Bun hands us every
|
|
18
|
+
* import whose *specifier* matches the regex, along with the importer's
|
|
19
|
+
* path (`args.importer`). We never return a result; we always throw.
|
|
20
20
|
* - The error is `ForbiddenGeneratedImportError`, a named subclass of
|
|
21
21
|
* `Error`. Tests can `instanceof`-check; Bun surfaces `error.message` in
|
|
22
22
|
* its `result.logs` output for CLI display.
|
|
@@ -91,8 +91,8 @@ export interface BlockGeneratedImportsOptions {
|
|
|
91
91
|
allowImporter?: (importerPath: string) => boolean;
|
|
92
92
|
/**
|
|
93
93
|
* Custom filter regex applied to the import specifier. Defaults to
|
|
94
|
-
* generated-artifact paths. Mandu ships a single default — exposing this
|
|
95
|
-
* for test harnesses that want to narrow or broaden the filter.
|
|
94
|
+
* generated-artifact paths. Mandu ships a single default — exposing this
|
|
95
|
+
* for test harnesses that want to narrow or broaden the filter.
|
|
96
96
|
*/
|
|
97
97
|
filter?: RegExp;
|
|
98
98
|
}
|
|
@@ -115,7 +115,7 @@ export function defaultAllowImporter(importerPath: string): boolean {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* Build a `BunPlugin` that blocks direct generated-artifact imports.
|
|
118
|
+
* Build a `BunPlugin` that blocks direct generated-artifact imports.
|
|
119
119
|
*
|
|
120
120
|
* Usage — call from `defaultBundlerPlugins(config)` (see `./index.ts`).
|
|
121
121
|
* Every `safeBuild` / `Bun.build` invocation in Mandu funnels through
|
|
@@ -124,7 +124,7 @@ export function defaultAllowImporter(importerPath: string): boolean {
|
|
|
124
124
|
export function blockGeneratedImports(
|
|
125
125
|
options: BlockGeneratedImportsOptions = {},
|
|
126
126
|
): BunPlugin {
|
|
127
|
-
const filter = options.filter ?? DEFAULT_BLOCK_FILTER;
|
|
127
|
+
const filter = options.filter ?? DEFAULT_BLOCK_FILTER;
|
|
128
128
|
const allowImporter = options.allowImporter ?? defaultAllowImporter;
|
|
129
129
|
|
|
130
130
|
return {
|
|
@@ -152,5 +152,5 @@ export function blockGeneratedImports(
|
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/** Exported for unit-test convenience — keep the filter text assertable. */
|
|
156
|
-
export const DEFAULT_BLOCK_FILTER = /__generated__|(?:^|[\/\\])\.mandu[\/\\]generated(?:[\/\\]|$)/;
|
|
155
|
+
/** Exported for unit-test convenience — keep the filter text assertable. */
|
|
156
|
+
export const DEFAULT_BLOCK_FILTER = /__generated__|(?:^|[\/\\])\.mandu[\/\\]generated(?:[\/\\]|$)/;
|
package/src/bundler/types.ts
CHANGED
|
@@ -55,28 +55,28 @@ export interface BundleManifest {
|
|
|
55
55
|
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
56
56
|
}
|
|
57
57
|
>;
|
|
58
|
-
/** Per-island bundles (code splitting: each island file gets its own JS bundle) */
|
|
59
|
-
islands?: Record<
|
|
60
|
-
string,
|
|
61
|
-
{
|
|
62
|
-
/** JavaScript bundle path */
|
|
58
|
+
/** Per-island bundles (code splitting: each island file gets its own JS bundle) */
|
|
59
|
+
islands?: Record<
|
|
60
|
+
string,
|
|
61
|
+
{
|
|
62
|
+
/** JavaScript bundle path */
|
|
63
63
|
js: string;
|
|
64
64
|
/** Route that owns this island */
|
|
65
65
|
route: string;
|
|
66
66
|
/** Hydration priority */
|
|
67
|
-
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
68
|
-
}
|
|
69
|
-
>;
|
|
70
|
-
/** Inline partial bundles (from *.partial.tsx / *.partial.ts files) */
|
|
71
|
-
partials?: Record<
|
|
72
|
-
string,
|
|
73
|
-
{
|
|
74
|
-
/** JavaScript bundle path */
|
|
75
|
-
js: string;
|
|
76
|
-
/** Hydration priority */
|
|
77
|
-
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
78
|
-
}
|
|
79
|
-
>;
|
|
67
|
+
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
68
|
+
}
|
|
69
|
+
>;
|
|
70
|
+
/** Inline partial bundles (from *.partial.tsx / *.partial.ts files) */
|
|
71
|
+
partials?: Record<
|
|
72
|
+
string,
|
|
73
|
+
{
|
|
74
|
+
/** JavaScript bundle path */
|
|
75
|
+
js: string;
|
|
76
|
+
/** Hydration priority */
|
|
77
|
+
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
78
|
+
}
|
|
79
|
+
>;
|
|
80
80
|
/** 공유 청크 */
|
|
81
81
|
shared: {
|
|
82
82
|
/** Hydration 런타임 */
|
|
@@ -123,19 +123,19 @@ export interface BundleStats {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/** Per-island code splitting entry (used by scanIslandFiles) */
|
|
126
|
-
export interface IslandFileEntry {
|
|
127
|
-
name: string;
|
|
128
|
-
filePath: string;
|
|
129
|
-
routeId: string;
|
|
130
|
-
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Inline partial bundle entry (used by scanPartialFiles) */
|
|
134
|
-
export interface PartialFileEntry {
|
|
135
|
-
name: string;
|
|
136
|
-
filePath: string;
|
|
137
|
-
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
138
|
-
}
|
|
126
|
+
export interface IslandFileEntry {
|
|
127
|
+
name: string;
|
|
128
|
+
filePath: string;
|
|
129
|
+
routeId: string;
|
|
130
|
+
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Inline partial bundle entry (used by scanPartialFiles) */
|
|
134
|
+
export interface PartialFileEntry {
|
|
135
|
+
name: string;
|
|
136
|
+
filePath: string;
|
|
137
|
+
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
138
|
+
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* 번들러 옵션
|
package/src/client/island.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Hydration을 위한 클라이언트 사이드 컴포넌트 정의
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ReactNode } from "react";
|
|
7
|
-
import { serializeProps } from "./serialize";
|
|
8
|
-
import { getServerData as getGlobalServerData } from "./window-state";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import { serializeProps } from "./serialize";
|
|
8
|
+
import { getServerData as getGlobalServerData } from "./window-state";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Island 정의 타입
|
|
@@ -336,45 +336,45 @@ export interface PartialConfig {
|
|
|
336
336
|
/**
|
|
337
337
|
* Partial Island 정의 타입
|
|
338
338
|
*/
|
|
339
|
-
export interface PartialDefinition<TProps> {
|
|
340
|
-
/** Partial 고유 ID. 기본값은 component displayName/name 입니다. */
|
|
341
|
-
id?: string;
|
|
342
|
-
/** Partial 컴포넌트 */
|
|
343
|
-
component: React.ComponentType<TProps>;
|
|
344
|
-
/** 초기 props (SSR에서 전달) */
|
|
345
|
-
initialProps?: TProps;
|
|
346
|
-
/** 하이드레이션 우선순위 */
|
|
347
|
-
priority?: "immediate" | "visible" | "idle" | "interaction";
|
|
348
|
-
/** 명시적 번들 URL. 기본값은 `/.mandu/client/{id}.partial.js` 입니다. */
|
|
349
|
-
src?: string;
|
|
350
|
-
/** 에러 시 표시할 UI */
|
|
351
|
-
errorBoundary?: (error: Error, reset: () => void) => ReactNode;
|
|
352
|
-
/** 로딩 중 표시할 UI */
|
|
353
|
-
loading?: () => ReactNode;
|
|
354
|
-
}
|
|
339
|
+
export interface PartialDefinition<TProps> {
|
|
340
|
+
/** Partial 고유 ID. 기본값은 component displayName/name 입니다. */
|
|
341
|
+
id?: string;
|
|
342
|
+
/** Partial 컴포넌트 */
|
|
343
|
+
component: React.ComponentType<TProps>;
|
|
344
|
+
/** 초기 props (SSR에서 전달) */
|
|
345
|
+
initialProps?: TProps;
|
|
346
|
+
/** 하이드레이션 우선순위 */
|
|
347
|
+
priority?: "immediate" | "visible" | "idle" | "interaction";
|
|
348
|
+
/** 명시적 번들 URL. 기본값은 `/.mandu/client/{id}.partial.js` 입니다. */
|
|
349
|
+
src?: string;
|
|
350
|
+
/** 에러 시 표시할 UI */
|
|
351
|
+
errorBoundary?: (error: Error, reset: () => void) => ReactNode;
|
|
352
|
+
/** 로딩 중 표시할 UI */
|
|
353
|
+
loading?: () => ReactNode;
|
|
354
|
+
}
|
|
355
355
|
|
|
356
356
|
/**
|
|
357
357
|
* 컴파일된 Partial
|
|
358
358
|
*/
|
|
359
|
-
export interface CompiledPartial<TProps> {
|
|
359
|
+
export interface CompiledPartial<TProps> {
|
|
360
360
|
/** Partial 정의 */
|
|
361
361
|
definition: PartialDefinition<TProps>;
|
|
362
362
|
/** Mandu Partial 마커 */
|
|
363
363
|
__mandu_partial: true;
|
|
364
364
|
/** Partial ID */
|
|
365
|
-
__mandu_partial_id?: string;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function normalizePartialId(id: string): string {
|
|
369
|
-
return id
|
|
370
|
-
.trim()
|
|
371
|
-
.replace(/[^A-Za-z0-9_-]/g, "-")
|
|
372
|
-
.replace(/^-+|-+$/g, "") || "partial";
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function priorityToHydrate(priority: NonNullable<PartialDefinition<unknown>["priority"]>): string {
|
|
376
|
-
return priority === "immediate" ? "load" : priority;
|
|
377
|
-
}
|
|
365
|
+
__mandu_partial_id?: string;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function normalizePartialId(id: string): string {
|
|
369
|
+
return id
|
|
370
|
+
.trim()
|
|
371
|
+
.replace(/[^A-Za-z0-9_-]/g, "-")
|
|
372
|
+
.replace(/^-+|-+$/g, "") || "partial";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function priorityToHydrate(priority: NonNullable<PartialDefinition<unknown>["priority"]>): string {
|
|
376
|
+
return priority === "immediate" ? "load" : priority;
|
|
377
|
+
}
|
|
378
378
|
|
|
379
379
|
/**
|
|
380
380
|
* Partial Island 생성
|
|
@@ -399,56 +399,56 @@ function priorityToHydrate(priority: NonNullable<PartialDefinition<unknown>["pri
|
|
|
399
399
|
* }
|
|
400
400
|
* ```
|
|
401
401
|
*/
|
|
402
|
-
export function partial<TProps extends Record<string, unknown>>(
|
|
403
|
-
definition: PartialDefinition<TProps>
|
|
404
|
-
): CompiledPartial<TProps> & {
|
|
405
|
-
Render: React.ComponentType<TProps>;
|
|
406
|
-
} {
|
|
402
|
+
export function partial<TProps extends Record<string, unknown>>(
|
|
403
|
+
definition: PartialDefinition<TProps>
|
|
404
|
+
): CompiledPartial<TProps> & {
|
|
405
|
+
Render: React.ComponentType<TProps>;
|
|
406
|
+
} {
|
|
407
407
|
if (!definition.component) {
|
|
408
408
|
throw new Error("[Mandu Partial] component is required");
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
const partialId = normalizePartialId(
|
|
412
|
-
definition.id ||
|
|
413
|
-
definition.component.displayName ||
|
|
414
|
-
definition.component.name ||
|
|
415
|
-
"partial",
|
|
416
|
-
);
|
|
417
|
-
const normalizedDefinition: PartialDefinition<TProps> = {
|
|
418
|
-
...definition,
|
|
419
|
-
id: partialId,
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
const compiled: CompiledPartial<TProps> = {
|
|
423
|
-
definition: normalizedDefinition,
|
|
424
|
-
__mandu_partial: true,
|
|
425
|
-
__mandu_partial_id: partialId,
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
// Render 컴포넌트 생성
|
|
429
|
-
const React = require("react");
|
|
430
|
-
|
|
431
|
-
const RenderComponent: React.FC<TProps> = (props) => {
|
|
432
|
-
const renderProps = Object.keys(props).length > 0
|
|
433
|
-
? props
|
|
434
|
-
: (normalizedDefinition.initialProps ?? props);
|
|
435
|
-
const priority = normalizedDefinition.priority ?? "visible";
|
|
436
|
-
const bundleSrc = normalizedDefinition.src ?? `/.mandu/client/${partialId}.partial.js`;
|
|
437
|
-
|
|
438
|
-
return React.createElement(
|
|
439
|
-
"div",
|
|
440
|
-
{
|
|
441
|
-
"data-mandu-island": partialId,
|
|
442
|
-
"data-mandu-partial": partialId,
|
|
443
|
-
"data-mandu-src": bundleSrc,
|
|
444
|
-
"data-mandu-priority": priority,
|
|
445
|
-
"data-hydrate": priorityToHydrate(priority),
|
|
446
|
-
"data-props": serializeProps(renderProps),
|
|
447
|
-
style: { display: "contents" },
|
|
448
|
-
},
|
|
449
|
-
React.createElement(normalizedDefinition.component, renderProps),
|
|
450
|
-
);
|
|
451
|
-
};
|
|
411
|
+
const partialId = normalizePartialId(
|
|
412
|
+
definition.id ||
|
|
413
|
+
definition.component.displayName ||
|
|
414
|
+
definition.component.name ||
|
|
415
|
+
"partial",
|
|
416
|
+
);
|
|
417
|
+
const normalizedDefinition: PartialDefinition<TProps> = {
|
|
418
|
+
...definition,
|
|
419
|
+
id: partialId,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const compiled: CompiledPartial<TProps> = {
|
|
423
|
+
definition: normalizedDefinition,
|
|
424
|
+
__mandu_partial: true,
|
|
425
|
+
__mandu_partial_id: partialId,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// Render 컴포넌트 생성
|
|
429
|
+
const React = require("react");
|
|
430
|
+
|
|
431
|
+
const RenderComponent: React.FC<TProps> = (props) => {
|
|
432
|
+
const renderProps = Object.keys(props).length > 0
|
|
433
|
+
? props
|
|
434
|
+
: (normalizedDefinition.initialProps ?? props);
|
|
435
|
+
const priority = normalizedDefinition.priority ?? "visible";
|
|
436
|
+
const bundleSrc = normalizedDefinition.src ?? `/.mandu/client/${partialId}.partial.js`;
|
|
437
|
+
|
|
438
|
+
return React.createElement(
|
|
439
|
+
"div",
|
|
440
|
+
{
|
|
441
|
+
"data-mandu-island": partialId,
|
|
442
|
+
"data-mandu-partial": partialId,
|
|
443
|
+
"data-mandu-src": bundleSrc,
|
|
444
|
+
"data-mandu-priority": priority,
|
|
445
|
+
"data-hydrate": priorityToHydrate(priority),
|
|
446
|
+
"data-props": serializeProps(renderProps),
|
|
447
|
+
style: { display: "contents" },
|
|
448
|
+
},
|
|
449
|
+
React.createElement(normalizedDefinition.component, renderProps),
|
|
450
|
+
);
|
|
451
|
+
};
|
|
452
452
|
|
|
453
453
|
return Object.assign(compiled, { Render: RenderComponent });
|
|
454
454
|
}
|
package/src/config/validate.ts
CHANGED
|
@@ -43,7 +43,7 @@ function _strictWithWarnings<T extends z.ZodRawShape>(
|
|
|
43
43
|
*/
|
|
44
44
|
const ServerConfigSchema = z
|
|
45
45
|
.object({
|
|
46
|
-
port: z.number().min(1).max(65535).default(3333),
|
|
46
|
+
port: z.number().min(1).max(65535).default(3333),
|
|
47
47
|
// Default `"::"` (IPv6 wildcard, dual-stack): accepts both IPv4 and
|
|
48
48
|
// IPv6 clients on one socket. Fixes Windows Node 17+ fetch failing
|
|
49
49
|
// with `ECONNREFUSED ::1:PORT` because `localhost` resolves to `::1`
|
package/src/contract/schema.ts
CHANGED
|
@@ -152,6 +152,13 @@ export interface ContractInstance extends ContractSchema {
|
|
|
152
152
|
_validated?: boolean;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Public API schema boundary. Use this alias in docs and generated examples
|
|
157
|
+
* when distinguishing a request/response contract from an executable filling.
|
|
158
|
+
*/
|
|
159
|
+
export type ApiContract<T extends ContractDefinition = ContractDefinition> =
|
|
160
|
+
T & ContractInstance;
|
|
161
|
+
|
|
155
162
|
/** Validation error detail */
|
|
156
163
|
export interface ContractValidationIssue {
|
|
157
164
|
/** Field path (e.g., "body.email") */
|
|
@@ -78,8 +78,8 @@ export async function buildDeployInferenceContext(
|
|
|
78
78
|
// still gets the manifest metadata and falls back to defaults.
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const imports = extractImports(source);
|
|
82
|
-
const dependencyClasses = classifySourceDependencies(source, imports);
|
|
81
|
+
const imports = extractImports(source);
|
|
82
|
+
const dependencyClasses = classifySourceDependencies(source, imports);
|
|
83
83
|
// Mandu's manifest patterns use `:param` / `*` (path-pattern style),
|
|
84
84
|
// not bracket-form. Detect both shapes so the heuristic doesn't
|
|
85
85
|
// misclassify dynamic routes as prerenderable. Examples:
|
|
@@ -139,82 +139,82 @@ export function extractImports(source: string): string[] {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/** Map import specifiers to coarse dependency classes. */
|
|
142
|
-
export function classifyImports(imports: string[]): ReadonlySet<DependencyClass> {
|
|
143
|
-
const classes = new Set<DependencyClass>();
|
|
144
|
-
for (const spec of imports) {
|
|
142
|
+
export function classifyImports(imports: string[]): ReadonlySet<DependencyClass> {
|
|
143
|
+
const classes = new Set<DependencyClass>();
|
|
144
|
+
for (const spec of imports) {
|
|
145
145
|
const cls = classifyOne(spec);
|
|
146
146
|
if (cls) classes.add(cls);
|
|
147
147
|
}
|
|
148
148
|
if (classes.size === 0) classes.add("fetch-only");
|
|
149
|
-
return classes;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Classify dependency signals that do not always appear as bare imports.
|
|
154
|
-
*
|
|
155
|
-
* Dogfooding surfaced Mandu API routes importing project-local
|
|
156
|
-
* `src/server/infra/db` helpers and using `db` tagged templates. Those
|
|
157
|
-
* are server-only even when the bare imports look edge-safe.
|
|
158
|
-
*/
|
|
159
|
-
export function classifySourceDependencies(
|
|
160
|
-
source: string,
|
|
161
|
-
imports: string[] = extractImports(source),
|
|
162
|
-
): ReadonlySet<DependencyClass> {
|
|
163
|
-
const classes = new Set<DependencyClass>(classifyImports(imports));
|
|
164
|
-
if (classes.size === 1 && classes.has("fetch-only")) {
|
|
165
|
-
classes.delete("fetch-only");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const allImports = extractAllImportSpecifiers(source);
|
|
169
|
-
if (allImports.some(isServerInfraImport)) {
|
|
170
|
-
classes.add("db");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (/\bBun\.SQL\b|\bBun\.sqlite\b/i.test(source)) {
|
|
174
|
-
classes.add("bun-native");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
/(?:^|[^\w$])db\s*`/.test(source) ||
|
|
179
|
-
/\bctx\.deps\.db\b/.test(source) ||
|
|
180
|
-
/\bdb\.(?:query|execute|select|insert|update|delete)\b/.test(source)
|
|
181
|
-
) {
|
|
182
|
-
classes.add("db");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (classes.size === 0) {
|
|
186
|
-
classes.add("fetch-only");
|
|
187
|
-
}
|
|
188
|
-
return classes;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function extractAllImportSpecifiers(source: string): string[] {
|
|
192
|
-
const out = new Set<string>();
|
|
193
|
-
const staticImport = /^\s*import\b[^"']*?["']([^"']+)["']/gm;
|
|
194
|
-
const dynamicImport = /\bimport\(\s*["']([^"']+)["']\s*\)/g;
|
|
195
|
-
for (const re of [staticImport, dynamicImport]) {
|
|
196
|
-
let m: RegExpExecArray | null;
|
|
197
|
-
while ((m = re.exec(source)) !== null) {
|
|
198
|
-
out.add(m[1]!);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return [...out].sort();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function isServerInfraImport(specifier: string): boolean {
|
|
205
|
-
const s = specifier.replace(/\\/g, "/").toLowerCase();
|
|
206
|
-
return (
|
|
207
|
-
s === "@/server/infra" ||
|
|
208
|
-
s.startsWith("@/server/infra/") ||
|
|
209
|
-
s === "src/server/infra" ||
|
|
210
|
-
s.startsWith("src/server/infra/") ||
|
|
211
|
-
s.endsWith("/server/infra") ||
|
|
212
|
-
s.includes("/server/infra/")
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function classifyOne(spec: string): DependencyClass | null {
|
|
217
|
-
const s = spec.toLowerCase();
|
|
149
|
+
return classes;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Classify dependency signals that do not always appear as bare imports.
|
|
154
|
+
*
|
|
155
|
+
* Dogfooding surfaced Mandu API routes importing project-local
|
|
156
|
+
* `src/server/infra/db` helpers and using `db` tagged templates. Those
|
|
157
|
+
* are server-only even when the bare imports look edge-safe.
|
|
158
|
+
*/
|
|
159
|
+
export function classifySourceDependencies(
|
|
160
|
+
source: string,
|
|
161
|
+
imports: string[] = extractImports(source),
|
|
162
|
+
): ReadonlySet<DependencyClass> {
|
|
163
|
+
const classes = new Set<DependencyClass>(classifyImports(imports));
|
|
164
|
+
if (classes.size === 1 && classes.has("fetch-only")) {
|
|
165
|
+
classes.delete("fetch-only");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const allImports = extractAllImportSpecifiers(source);
|
|
169
|
+
if (allImports.some(isServerInfraImport)) {
|
|
170
|
+
classes.add("db");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (/\bBun\.SQL\b|\bBun\.sqlite\b/i.test(source)) {
|
|
174
|
+
classes.add("bun-native");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
/(?:^|[^\w$])db\s*`/.test(source) ||
|
|
179
|
+
/\bctx\.deps\.db\b/.test(source) ||
|
|
180
|
+
/\bdb\.(?:query|execute|select|insert|update|delete)\b/.test(source)
|
|
181
|
+
) {
|
|
182
|
+
classes.add("db");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (classes.size === 0) {
|
|
186
|
+
classes.add("fetch-only");
|
|
187
|
+
}
|
|
188
|
+
return classes;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractAllImportSpecifiers(source: string): string[] {
|
|
192
|
+
const out = new Set<string>();
|
|
193
|
+
const staticImport = /^\s*import\b[^"']*?["']([^"']+)["']/gm;
|
|
194
|
+
const dynamicImport = /\bimport\(\s*["']([^"']+)["']\s*\)/g;
|
|
195
|
+
for (const re of [staticImport, dynamicImport]) {
|
|
196
|
+
let m: RegExpExecArray | null;
|
|
197
|
+
while ((m = re.exec(source)) !== null) {
|
|
198
|
+
out.add(m[1]!);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return [...out].sort();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isServerInfraImport(specifier: string): boolean {
|
|
205
|
+
const s = specifier.replace(/\\/g, "/").toLowerCase();
|
|
206
|
+
return (
|
|
207
|
+
s === "@/server/infra" ||
|
|
208
|
+
s.startsWith("@/server/infra/") ||
|
|
209
|
+
s === "src/server/infra" ||
|
|
210
|
+
s.startsWith("src/server/infra/") ||
|
|
211
|
+
s.endsWith("/server/infra") ||
|
|
212
|
+
s.includes("/server/infra/")
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function classifyOne(spec: string): DependencyClass | null {
|
|
217
|
+
const s = spec.toLowerCase();
|
|
218
218
|
if (
|
|
219
219
|
s === "bun:sqlite" ||
|
|
220
220
|
s === "bun:ffi" ||
|
|
@@ -231,14 +231,14 @@ function classifyOne(spec: string): DependencyClass | null {
|
|
|
231
231
|
if (s === "node:child_process" || s === "child_process" || s === "node:worker_threads" || s === "worker_threads") {
|
|
232
232
|
return "node-child";
|
|
233
233
|
}
|
|
234
|
-
if (
|
|
235
|
-
/^(postgres|pg|mysql2?|drizzle-orm(\/.*)?|@prisma\/client|prisma|mongodb|mongoose|@neondatabase\/.+|kysely|sqlite3|better-sqlite3|@planetscale\/.+)$/.test(s)
|
|
236
|
-
) {
|
|
237
|
-
return "db";
|
|
238
|
-
}
|
|
239
|
-
if (s === "@mandujs/core/db" || s.startsWith("@mandujs/core/db/")) {
|
|
240
|
-
return "db";
|
|
241
|
-
}
|
|
234
|
+
if (
|
|
235
|
+
/^(postgres|pg|mysql2?|drizzle-orm(\/.*)?|@prisma\/client|prisma|mongodb|mongoose|@neondatabase\/.+|kysely|sqlite3|better-sqlite3|@planetscale\/.+)$/.test(s)
|
|
236
|
+
) {
|
|
237
|
+
return "db";
|
|
238
|
+
}
|
|
239
|
+
if (s === "@mandujs/core/db" || s.startsWith("@mandujs/core/db/")) {
|
|
240
|
+
return "db";
|
|
241
|
+
}
|
|
242
242
|
if (/^(@anthropic-ai\/sdk|openai|ai|@ai-sdk\/.+|@google\/generative-ai|cohere-ai)$/.test(s)) {
|
|
243
243
|
return "ai-sdk";
|
|
244
244
|
}
|