@mandujs/core 0.41.2 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/package.json +21 -4
  2. package/src/auth/__tests__/login.test.ts +420 -419
  3. package/src/auth/__tests__/reset.test.ts +296 -296
  4. package/src/brain/adapters/anthropic-oauth.ts +421 -420
  5. package/src/brain/adapters/index.ts +2 -1
  6. package/src/brain/adapters/ollama.ts +1 -1
  7. package/src/brain/adapters/openai-oauth.ts +534 -533
  8. package/src/brain/brain.ts +2 -1
  9. package/src/brain/redactor.ts +196 -196
  10. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
  11. package/src/bundler/__tests__/cold-start.test.ts +504 -504
  12. package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
  13. package/src/bundler/__tests__/hdr.test.ts +1 -1
  14. package/src/bundler/analyzer.ts +958 -958
  15. package/src/bundler/build.ts +104 -14
  16. package/src/bundler/dev.ts +125 -0
  17. package/src/bundler/hmr-types.ts +1 -0
  18. package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
  19. package/src/bundler/plugins/index.ts +14 -0
  20. package/src/bundler/plugins/react-compiler-lint.ts +253 -0
  21. package/src/bundler/plugins/react-compiler.ts +162 -0
  22. package/src/bundler/types.ts +12 -0
  23. package/src/change/integrity.ts +2 -1
  24. package/src/client/index.ts +10 -0
  25. package/src/client/island.ts +38 -11
  26. package/src/client/router.ts +6 -1
  27. package/src/config/mandu.ts +57 -0
  28. package/src/config/validate.ts +42 -0
  29. package/src/content/collection.ts +844 -809
  30. package/src/content/content-layer.ts +316 -314
  31. package/src/content/content.test.ts +433 -433
  32. package/src/content/digest.ts +133 -133
  33. package/src/content/generate-types.ts +168 -168
  34. package/src/content/index.ts +6 -1
  35. package/src/content/llms-txt.ts +277 -277
  36. package/src/contract/define.ts +474 -474
  37. package/src/contract/route-helpers.ts +2 -1
  38. package/src/contract/zod-utils.ts +158 -155
  39. package/src/db/index.ts +513 -513
  40. package/src/desktop/__tests__/smoke.test.ts +100 -100
  41. package/src/desktop/webview-fallback.ts +583 -583
  42. package/src/desktop/window.ts +3 -1
  43. package/src/dev-error-overlay/overlay-client.ts +300 -300
  44. package/src/devtools/ai/mcp-connector.ts +499 -498
  45. package/src/devtools/client/components/kitchen-root.tsx +7 -2
  46. package/src/email/resend.ts +163 -163
  47. package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
  48. package/src/guard/ast-analyzer.ts +806 -806
  49. package/src/guard/graph.ts +898 -898
  50. package/src/guard/index.ts +16 -0
  51. package/src/guard/statistics.ts +578 -578
  52. package/src/guard/tsgolint-bridge.ts +512 -0
  53. package/src/i18n/locale-resolver.ts +214 -214
  54. package/src/id/__tests__/id.test.ts +120 -120
  55. package/src/intent/index.ts +321 -321
  56. package/src/island/index.ts +39 -23
  57. package/src/kitchen/api/contract-api.ts +15 -8
  58. package/src/kitchen/kitchen-ui.ts +2137 -2137
  59. package/src/lockfile/index.ts +3 -2
  60. package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
  61. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
  62. package/src/middleware/secure/index.ts +417 -417
  63. package/src/observability/event-bus.ts +2 -2
  64. package/src/observability/metrics.ts +334 -334
  65. package/src/observability/tracing.ts +694 -694
  66. package/src/openapi/generator.ts +1 -1
  67. package/src/perf/user-marks.ts +553 -553
  68. package/src/plugins/registry.ts +387 -387
  69. package/src/resource/ddl/diff.ts +392 -392
  70. package/src/resource/ddl/snapshot.ts +448 -447
  71. package/src/resource/generator-schema.ts +477 -476
  72. package/src/resource/parser.ts +4 -2
  73. package/src/resource/schema.ts +1 -1
  74. package/src/router/fs-patterns.ts +422 -422
  75. package/src/runtime/fast-refresh-types.ts +126 -128
  76. package/src/runtime/image-handler.ts +206 -195
  77. package/src/runtime/router.test.ts +476 -476
  78. package/src/runtime/security.ts +155 -155
  79. package/src/runtime/server.ts +36 -19
  80. package/src/runtime/session-key.ts +328 -328
  81. package/src/scheduler/__tests__/scheduler.test.ts +514 -514
  82. package/src/seo/resolve/index.ts +353 -353
  83. package/src/spec/load.ts +1 -1
  84. package/src/testing/reporter.ts +676 -676
  85. package/src/testing/server.ts +196 -196
  86. package/src/testing/snapshot.ts +444 -444
  87. package/src/utils/__tests__/lru-cache.test.ts +186 -186
  88. package/src/utils/bun.ts +8 -8
@@ -0,0 +1,253 @@
1
+ /**
2
+ * React Compiler bailout detector (#240 Phase 2).
3
+ *
4
+ * React Compiler silently skips components it can't safely memoize
5
+ * ("15% bailout"). Developers have no way to know which components
6
+ * are in the skipped set without this static pass.
7
+ *
8
+ * This module runs `eslint-plugin-react-compiler` in-process against a
9
+ * caller-supplied list of files (islands / `"use client"` pages /
10
+ * partials — the same files the bundler's `manduClientPlugins` gate
11
+ * picks up) and returns normalized diagnostics. `mandu check` wires
12
+ * this up behind `experimental.reactCompiler.enabled`.
13
+ *
14
+ * ## Why ESLint, not oxlint?
15
+ *
16
+ * The canonical rule lives at `react-compiler/react-compiler` in
17
+ * `eslint-plugin-react-compiler`. Oxlint has no port at the time of
18
+ * writing. When oxlint gains the rule we can swap this runner for a
19
+ * thin `oxlint --rule react-compiler/react-compiler` shell without
20
+ * changing callers — the `runReactCompilerLint` signature stays.
21
+ *
22
+ * ## Graceful degradation
23
+ *
24
+ * `eslint` and `eslint-plugin-react-compiler` are **optional** peer
25
+ * dependencies. If either is missing the runner returns `[]` and logs
26
+ * a single warning — the build is never blocked.
27
+ */
28
+
29
+ import path from "node:path";
30
+
31
+ export interface ReactCompilerDiagnostic {
32
+ /** Absolute path to the offending file. */
33
+ file: string;
34
+ /** 1-indexed line. */
35
+ line: number;
36
+ /** 1-indexed column. */
37
+ column: number;
38
+ /** Human-readable message produced by the rule. */
39
+ message: string;
40
+ /** Always `"react-compiler/react-compiler"`. */
41
+ ruleId: string;
42
+ /** ESLint severity — "error" (`2`) or "warning" (`1`). */
43
+ severity: "warning" | "error";
44
+ }
45
+
46
+ export interface RunReactCompilerLintOptions {
47
+ /**
48
+ * Project root — used to resolve `eslint` / the plugin from the
49
+ * project's own `node_modules` before falling back to Mandu's.
50
+ */
51
+ projectRoot: string;
52
+ /**
53
+ * Absolute file paths to lint. Callers should pass only files the
54
+ * bundler would run through the Compiler (islands / client pages /
55
+ * partials). Passing server files wastes CPU — the rule still runs
56
+ * but the diagnostics are irrelevant.
57
+ */
58
+ targetFiles: readonly string[];
59
+ /**
60
+ * Severity level used for the rule. Default `"warning"`. `"error"`
61
+ * makes the diagnostic a hard failure in strict mode.
62
+ */
63
+ severity?: "warning" | "error";
64
+ }
65
+
66
+ export interface RunReactCompilerLintResult {
67
+ /** All diagnostics found. Empty array on success or graceful skip. */
68
+ diagnostics: ReactCompilerDiagnostic[];
69
+ /** When `true` the runner could not load its peers and returned `[]`. */
70
+ skipped: boolean;
71
+ /** Human-readable explanation when `skipped` is `true`. */
72
+ skipReason?: string;
73
+ }
74
+
75
+ // Minimal structural types for ESLint's flat-config API. We avoid a
76
+ // hard dependency on `@types/eslint` because eslint is an optional
77
+ // peer dep; the real module is imported dynamically at runtime.
78
+ type ESLintMessage = {
79
+ ruleId: string | null;
80
+ severity: number;
81
+ message: string;
82
+ line?: number;
83
+ column?: number;
84
+ };
85
+ type ESLintLintResult = {
86
+ filePath: string;
87
+ messages: ESLintMessage[];
88
+ };
89
+ type ESLintCtor = new (opts: { overrideConfigFile?: boolean; overrideConfig?: unknown }) => {
90
+ lintFiles: (patterns: readonly string[]) => Promise<ESLintLintResult[]>;
91
+ };
92
+
93
+ async function resolvePeers(
94
+ _projectRoot: string,
95
+ ): Promise<{ ESLint: ESLintCtor; plugin: unknown } | { error: string }> {
96
+ try {
97
+ const eslintMod = (await import(/* @vite-ignore */ "eslint" as string)) as {
98
+ ESLint?: ESLintCtor;
99
+ default?: { ESLint: ESLintCtor };
100
+ };
101
+ const ESLint = eslintMod.ESLint ?? eslintMod.default?.ESLint;
102
+ if (!ESLint) {
103
+ return { error: "`eslint` loaded but `ESLint` constructor not exported" };
104
+ }
105
+ const pluginMod = (await import(
106
+ /* @vite-ignore */ "eslint-plugin-react-compiler" as string
107
+ )) as { default?: unknown };
108
+ const plugin = pluginMod.default ?? pluginMod;
109
+ return { ESLint, plugin };
110
+ } catch (err) {
111
+ return {
112
+ error: err instanceof Error ? err.message : String(err),
113
+ };
114
+ }
115
+ }
116
+
117
+ function mapSeverity(raw: number): "warning" | "error" {
118
+ return raw >= 2 ? "error" : "warning";
119
+ }
120
+
121
+ /**
122
+ * Run the `react-compiler/react-compiler` rule against a list of
123
+ * files. Does **not** read any ESLint config from disk — we assemble
124
+ * an inline flat config so the result is deterministic regardless of
125
+ * the host project's ESLint setup.
126
+ */
127
+ export async function runReactCompilerLint(
128
+ options: RunReactCompilerLintOptions,
129
+ ): Promise<RunReactCompilerLintResult> {
130
+ if (options.targetFiles.length === 0) {
131
+ return { diagnostics: [], skipped: false };
132
+ }
133
+
134
+ const peers = await resolvePeers(options.projectRoot);
135
+ if ("error" in peers) {
136
+ console.warn(
137
+ "[Mandu React Compiler] Skipping bailout diagnostics — " +
138
+ "install `eslint` and `eslint-plugin-react-compiler` to enable. " +
139
+ `Reason: ${peers.error}`,
140
+ );
141
+ return {
142
+ diagnostics: [],
143
+ skipped: true,
144
+ skipReason: peers.error,
145
+ };
146
+ }
147
+ const { ESLint, plugin } = peers;
148
+ const severityNumber = options.severity === "error" ? 2 : 1;
149
+
150
+ // Flat config — no eslintrc resolution, no plugin auto-discovery.
151
+ // The override is evaluated for every target file.
152
+ const overrideConfig = [
153
+ {
154
+ files: ["**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}"],
155
+ plugins: { "react-compiler": plugin },
156
+ rules: { "react-compiler/react-compiler": severityNumber },
157
+ },
158
+ ];
159
+
160
+ let linter;
161
+ try {
162
+ linter = new ESLint({ overrideConfigFile: true, overrideConfig });
163
+ } catch (err) {
164
+ const reason = err instanceof Error ? err.message : String(err);
165
+ console.warn(
166
+ `[Mandu React Compiler] ESLint construction failed — skipping diagnostics. ${reason}`,
167
+ );
168
+ return { diagnostics: [], skipped: true, skipReason: reason };
169
+ }
170
+
171
+ const results = await linter.lintFiles(options.targetFiles.slice());
172
+ const diagnostics: ReactCompilerDiagnostic[] = [];
173
+ for (const result of results) {
174
+ for (const msg of result.messages) {
175
+ if (msg.ruleId !== "react-compiler/react-compiler") continue;
176
+ diagnostics.push({
177
+ file: result.filePath,
178
+ line: msg.line ?? 1,
179
+ column: msg.column ?? 1,
180
+ message: msg.message,
181
+ ruleId: "react-compiler/react-compiler",
182
+ severity: mapSeverity(msg.severity),
183
+ });
184
+ }
185
+ }
186
+ return { diagnostics, skipped: false };
187
+ }
188
+
189
+ // ─────────────────────────────────────────────────────────────────────────
190
+ // Report formatter — kept pure so the CLI can render or tests can snapshot
191
+ // ─────────────────────────────────────────────────────────────────────────
192
+
193
+ export interface FormatCompilerReportOptions {
194
+ /** Omit absolute-path prefixes starting with this root. */
195
+ projectRoot?: string;
196
+ /** Max diagnostics shown. Excess is summarised as "... N more". Default 25. */
197
+ limit?: number;
198
+ }
199
+
200
+ /**
201
+ * Format diagnostics as a plain-text block suitable for the `mandu
202
+ * check` console output. Returns a single string so the caller can
203
+ * `log()` it in one shot.
204
+ */
205
+ export function formatCompilerReport(
206
+ diagnostics: readonly ReactCompilerDiagnostic[],
207
+ options: FormatCompilerReportOptions = {},
208
+ ): string {
209
+ if (diagnostics.length === 0) {
210
+ return "🧠 React Compiler — no bailouts detected";
211
+ }
212
+ const projectRoot = options.projectRoot;
213
+ const limit = options.limit ?? 25;
214
+ const byFile = new Map<string, ReactCompilerDiagnostic[]>();
215
+ for (const d of diagnostics) {
216
+ const list = byFile.get(d.file) ?? [];
217
+ list.push(d);
218
+ byFile.set(d.file, list);
219
+ }
220
+
221
+ const fileCount = byFile.size;
222
+ const lines: string[] = [];
223
+ lines.push(
224
+ `🧠 React Compiler — ${diagnostics.length} bailout(s) in ${fileCount} file(s)`,
225
+ );
226
+ lines.push("");
227
+
228
+ const shown: ReactCompilerDiagnostic[] = [];
229
+ for (const ds of byFile.values()) {
230
+ for (const d of ds) {
231
+ if (shown.length >= limit) break;
232
+ shown.push(d);
233
+ }
234
+ if (shown.length >= limit) break;
235
+ }
236
+ for (const d of shown) {
237
+ const relative = projectRoot ? path.relative(projectRoot, d.file) : d.file;
238
+ lines.push(` ${relative}:${d.line}:${d.column}`);
239
+ lines.push(` ${d.message}`);
240
+ }
241
+ if (diagnostics.length > shown.length) {
242
+ lines.push(` … and ${diagnostics.length - shown.length} more`);
243
+ }
244
+ lines.push("");
245
+ lines.push(
246
+ "→ These components will NOT be auto-memoized. Most bailouts come from " +
247
+ "conditional hook calls, ref escape, or mutation of shared values.",
248
+ );
249
+ lines.push(
250
+ "→ See https://react.dev/learn/react-compiler for common patterns.",
251
+ );
252
+ return lines.join("\n");
253
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Bundler plugin — React Compiler (#240).
3
+ *
4
+ * Auto-memoizes React components at build time via the official
5
+ * `babel-plugin-react-compiler`. Runs as a Bun `onLoad` transform:
6
+ * source → Babel + react-compiler → transformed code → Bun's remaining
7
+ * pipeline.
8
+ *
9
+ * Ported inline rather than depending on `bun-plugin-react-compiler`
10
+ * (single-maintainer, 9★ at 2026-04). The substantive logic is ~60
11
+ * lines; keeping it in-repo avoids the supply-chain risk without adding
12
+ * real maintenance cost.
13
+ *
14
+ * ## Scope
15
+ *
16
+ * Only invoked by the bundler's island / `"use client"` / partial build
17
+ * paths. Server-rendered files (`page.tsx` + `layout.tsx` SSR) never
18
+ * hit this plugin because React Compiler's memoization is a re-render
19
+ * optimization; SSR renders once and serializes to HTML. See
20
+ * `bundler/build.ts` for the gate.
21
+ *
22
+ * ## Opt-in
23
+ *
24
+ * Disabled by default. Projects enable via `mandu.config.ts`:
25
+ *
26
+ * ```ts
27
+ * export default {
28
+ * experimental: {
29
+ * reactCompiler: { enabled: true },
30
+ * },
31
+ * } satisfies ManduConfig;
32
+ * ```
33
+ *
34
+ * Failure mode: if `@babel/core` or `babel-plugin-react-compiler` are
35
+ * missing, the plugin returns the original source verbatim and logs
36
+ * once per build. This preserves the "enable the flag → try it" UX
37
+ * without blocking anyone who forgot to install the peer deps.
38
+ */
39
+
40
+ import type { BunPlugin } from "bun";
41
+ import fs from "node:fs/promises";
42
+
43
+ export interface ReactCompilerPluginOptions {
44
+ /**
45
+ * File filter — only matching paths get the transform. Default:
46
+ * `.jsx` / `.tsx`. The bundler's own client-path gate narrows this
47
+ * further; the regex here is a belt-and-braces check.
48
+ */
49
+ filter?: RegExp;
50
+ /**
51
+ * Options forwarded to `babel-plugin-react-compiler`. Common keys:
52
+ * - `compilationMode: "annotation" | "infer" | "all"`
53
+ * - `target: "19" | "18" | "17"`
54
+ * - `panicThreshold: "none" | "all_errors" | "critical_errors"`
55
+ * Omit to use react-compiler defaults (`compilationMode: "infer"` +
56
+ * `target` derived from the installed `react` version).
57
+ */
58
+ reactCompilerConfig?: Record<string, unknown>;
59
+ }
60
+
61
+ const DEFAULT_FILTER = /\.(?:jsx|tsx)$/;
62
+
63
+ /**
64
+ * Create the Bun plugin instance. Dynamic imports inside `onLoad` keep
65
+ * `@babel/core` out of the bundler's import graph when the plugin is
66
+ * never activated — the cost is a one-shot `await import()` on first
67
+ * use, which Bun caches.
68
+ */
69
+ export function reactCompiler(
70
+ options: ReactCompilerPluginOptions = {},
71
+ ): BunPlugin {
72
+ const filter = options.filter ?? DEFAULT_FILTER;
73
+ const compilerConfig = options.reactCompilerConfig;
74
+
75
+ // `@babel/core` + `babel-plugin-react-compiler` are optional peer
76
+ // dependencies — keep them out of the type graph so projects that
77
+ // don't enable the compiler never need `@types/babel__core`.
78
+ type BabelLike = { transformAsync: (src: string, opts: unknown) => Promise<{ code?: string | null } | null> };
79
+ let babelMod: BabelLike | null = null;
80
+ let compilerPlugin: unknown = null;
81
+ let resolutionFailed = false;
82
+
83
+ const resolveBabel = async (): Promise<boolean> => {
84
+ if (resolutionFailed) return false;
85
+ if (babelMod && compilerPlugin) return true;
86
+ try {
87
+ // Both imports are peerDependencies declared `optional: true` so
88
+ // projects without React Compiler never pay the install cost. We
89
+ // import each separately so a missing react-compiler package
90
+ // doesn't mask a Babel install failure.
91
+ if (!babelMod) {
92
+ const mod = (await import(/* @vite-ignore */ "@babel/core" as string)) as unknown as BabelLike;
93
+ babelMod = mod;
94
+ }
95
+ if (!compilerPlugin) {
96
+ const cm = (await import(/* @vite-ignore */ "babel-plugin-react-compiler" as string)) as unknown as {
97
+ default?: unknown;
98
+ };
99
+ compilerPlugin = cm.default ?? cm;
100
+ }
101
+ return true;
102
+ } catch (err) {
103
+ resolutionFailed = true;
104
+ console.warn(
105
+ "[Mandu React Compiler] Peer dependency missing — skipping transform. " +
106
+ "Install @babel/core + babel-plugin-react-compiler to enable. " +
107
+ `Reason: ${err instanceof Error ? err.message : String(err)}`,
108
+ );
109
+ return false;
110
+ }
111
+ };
112
+
113
+ return {
114
+ name: "mandu:react-compiler",
115
+ setup(build) {
116
+ build.onLoad({ filter }, async ({ path: filePath }) => {
117
+ const ok = await resolveBabel();
118
+ if (!ok || !babelMod || !compilerPlugin) return undefined;
119
+
120
+ let source: string;
121
+ try {
122
+ source = await fs.readFile(filePath, "utf-8");
123
+ } catch {
124
+ // Bun will surface the original read error when it retries
125
+ // the load itself; returning undefined gives it the chance.
126
+ return undefined;
127
+ }
128
+
129
+ try {
130
+ const result = await babelMod.transformAsync(source, {
131
+ filename: filePath,
132
+ sourceMaps: "inline",
133
+ babelrc: false,
134
+ configFile: false,
135
+ parserOpts: {
136
+ plugins: [
137
+ "jsx",
138
+ "typescript",
139
+ "explicitResourceManagement",
140
+ ],
141
+ },
142
+ plugins: [[compilerPlugin, compilerConfig ?? {}]],
143
+ });
144
+ if (!result?.code) return undefined;
145
+ return {
146
+ contents: result.code,
147
+ loader: filePath.endsWith(".tsx") ? "tsx" : "jsx",
148
+ };
149
+ } catch (err) {
150
+ // Return original so Bun falls back to its normal transform —
151
+ // react-compiler skips plenty of components by design
152
+ // ("15% bailout") and we don't want a bailout to kill the
153
+ // build. Log once per file path with the reason.
154
+ console.warn(
155
+ `[Mandu React Compiler] skip ${filePath}: ${err instanceof Error ? err.message : String(err)}`,
156
+ );
157
+ return undefined;
158
+ }
159
+ });
160
+ },
161
+ };
162
+ }
@@ -165,6 +165,18 @@ export interface BundlerOptions {
165
165
  */
166
166
  blockGeneratedImport?: boolean;
167
167
 
168
+ /**
169
+ * Issue #240 — React Compiler opt-in. When `enabled: true` the
170
+ * bundler's client-path invocations install
171
+ * `mandu:react-compiler` (Babel + `babel-plugin-react-compiler`)
172
+ * on every `.jsx`/`.tsx` file. SSR-only builds ignore this —
173
+ * memoization has no effect on a one-shot HTML render.
174
+ */
175
+ reactCompiler?: {
176
+ enabled?: boolean;
177
+ compilerConfig?: Record<string, unknown>;
178
+ };
179
+
168
180
  /**
169
181
  * Phase 18.τ — consumer-supplied `BunPlugin`s contributed via plugin
170
182
  * `defineBundlerPlugin()` hook. Composed AFTER Mandu's defaults so
@@ -20,7 +20,8 @@ export async function computeFileHash(filePath: string): Promise<string> {
20
20
  throw error;
21
21
  }
22
22
  throw new Error(
23
- `Failed to compute hash for ${filePath}: ${error instanceof Error ? error.message : String(error)}`
23
+ `Failed to compute hash for ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
24
+ { cause: error }
24
25
  );
25
26
  }
26
27
  }
@@ -232,3 +232,13 @@ export const ManduClient = {
232
232
  */
233
233
  createPartialGroup,
234
234
  };
235
+
236
+ /**
237
+ * `Mandu` — README-facing alias of `ManduClient`. Kept so
238
+ * `import { Mandu } from "@mandujs/core/client"` — the form shown in
239
+ * `packages/core/README.md` — resolves to the same namespace object at
240
+ * runtime. `ManduClient` is the canonical export; this re-export exists
241
+ * because the README (and third-party docs scaffolded from it) uses the
242
+ * shorter `Mandu.island(...)` / `Mandu.filling(...)` shape.
243
+ */
244
+ export const Mandu = ManduClient;
@@ -51,16 +51,27 @@ export interface IslandMetadata {
51
51
  }
52
52
 
53
53
  /**
54
- * 컴파일된 Island 컴포넌트 타입
54
+ * 컴파일된 Island 컴포넌트 타입.
55
+ *
56
+ * An island is a **page-level client bundle**, not an inline JSX element.
57
+ * The runtime representation is a React component **whose body unconditionally
58
+ * throws**, decorated with the `definition` + `__mandu_island` marker so the
59
+ * build pipeline can still recognise it.
60
+ *
61
+ * Why callable? A plain `{ definition, __mandu_island }` object rendered as
62
+ * `<MyIsland />` produced React's opaque *"Element type is invalid... got:
63
+ * object"* error. Making it a function lets React invoke it like any other
64
+ * component, which triggers our clear diagnostic below — users immediately
65
+ * learn to switch to `partial()` for embedded client regions.
55
66
  */
56
- export interface CompiledIsland<TServerData, TSetupResult> {
67
+ export type CompiledIsland<TServerData, TSetupResult> = (() => never) & {
57
68
  /** Island 정의 */
58
69
  definition: IslandDefinition<TServerData, TSetupResult>;
59
70
  /** Island 메타데이터 (빌드 시 주입) */
60
71
  __mandu_island: true;
61
72
  /** Island ID (빌드 시 주입) */
62
73
  __mandu_island_id?: string;
63
- }
74
+ };
64
75
 
65
76
  /**
66
77
  * Island 컴포넌트 생성
@@ -104,10 +115,23 @@ export function island<TServerData, TSetupResult = TServerData>(
104
115
  throw new Error("[Mandu Island] render must be a function");
105
116
  }
106
117
 
107
- return {
108
- definition,
109
- __mandu_island: true,
110
- };
118
+ // Function body: reached only when a server page (or any caller) tries to
119
+ // render the island as an inline React element — `<MyIsland />`. Islands
120
+ // are not inline elements; they are page-level client bundles. Throw a
121
+ // clear message pointing at `partial()` (the right API for embedded
122
+ // client regions) instead of React's generic "Element type is invalid".
123
+ const IslandElement = (() => {
124
+ throw new Error(
125
+ "[Mandu Island] Islands are page-level client bundles — they cannot " +
126
+ "be rendered as inline JSX elements. For an embedded client region " +
127
+ "inside a server page, use `partial()` instead (it returns a " +
128
+ "renderable component). See " +
129
+ "https://mandujs.com/docs/architect/client-rendering",
130
+ );
131
+ }) as CompiledIsland<TServerData, TSetupResult>;
132
+ IslandElement.definition = definition;
133
+ IslandElement.__mandu_island = true;
134
+ return IslandElement;
111
135
  }
112
136
 
113
137
  /**
@@ -233,7 +257,7 @@ export interface WrapComponentOptions<TServerData, TProps> {
233
257
  loading?: () => ReactNode;
234
258
  }
235
259
 
236
- export function wrapComponent<TProps extends Record<string, any>>(
260
+ export function wrapComponent<TProps extends Record<string, unknown>>(
237
261
  Component: React.ComponentType<TProps>,
238
262
  options?: WrapComponentOptions<TProps, TProps>
239
263
  ): CompiledIsland<TProps, TProps>;
@@ -355,7 +379,7 @@ export interface CompiledPartial<TProps> {
355
379
  * }
356
380
  * ```
357
381
  */
358
- export function partial<TProps extends Record<string, any>>(
382
+ export function partial<TProps extends Record<string, unknown>>(
359
383
  definition: PartialDefinition<TProps>
360
384
  ): CompiledPartial<TProps> & {
361
385
  Render: React.ComponentType<TProps>;
@@ -425,7 +449,7 @@ export interface CompiledSlot<TData, TProps> {
425
449
  * });
426
450
  * ```
427
451
  */
428
- export function slot<TData, TProps extends Record<string, any>>(
452
+ export function slot<TData, TProps extends Record<string, unknown>>(
429
453
  definition: SlotDefinition<TData, TProps>
430
454
  ): CompiledSlot<TData, TProps> {
431
455
  if (!definition.id) {
@@ -473,7 +497,10 @@ export interface PartialGroup {
473
497
  * ```
474
498
  */
475
499
  export function createPartialGroup(): PartialGroup {
476
- // React ComponentType variance requires `any` for heterogeneous component storage
500
+ // React `ComponentType<P>` is contravariant on `P`, so a heterogeneous
501
+ // component map genuinely needs `any` here — any narrower bag type would
502
+ // reject concrete `CompiledPartial<{userId: string}>` entries at `.set()`.
503
+ // oxlint-disable-next-line no-explicit-any -- heterogeneous React component storage
477
504
  const partials = new Map<string, CompiledPartial<any>>();
478
505
 
479
506
  return {
@@ -343,7 +343,12 @@ export async function navigate(
343
343
 
344
344
  // View Transitions API — 브라우저 지원 시 URL + DOM 전환을 동기화
345
345
  if (!replace && "startViewTransition" in document) {
346
- (document as any).startViewTransition(applyUpdate);
346
+ // `startViewTransition` is part of the View Transitions API which
347
+ // is not yet in every lib.dom.d.ts. Narrow the cast to the only
348
+ // method we call rather than widening to `any`.
349
+ (document as Document & {
350
+ startViewTransition: (callback: () => void) => unknown;
351
+ }).startViewTransition(applyUpdate);
347
352
  } else {
348
353
  applyUpdate();
349
354
  }
@@ -204,6 +204,20 @@ export interface ManduConfig {
204
204
  * Default: `true`.
205
205
  */
206
206
  blockGeneratedImport?: boolean;
207
+ /**
208
+ * Follow-up E — opt-in type-aware lint layer backed by
209
+ * `oxlint --type-aware` (tsgolint). Declaring this block makes
210
+ * `mandu guard` invoke oxlint after the architecture check and
211
+ * merge the results into the final report; MCP's
212
+ * `mandu_guard_check` surface gains a `typeAware` field.
213
+ *
214
+ * @see `@mandujs/core/guard/tsgolint-bridge`
215
+ */
216
+ typeAware?: {
217
+ rules?: string[];
218
+ severity?: "off" | "warn" | "error";
219
+ configPath?: string;
220
+ };
207
221
  };
208
222
  build?: {
209
223
  outDir?: string;
@@ -657,6 +671,49 @@ export interface ManduConfig {
657
671
  ollama?: { model?: string; baseUrl?: string };
658
672
  telemetryOptOut?: boolean;
659
673
  };
674
+ /**
675
+ * Issue #240 — Experimental feature flags. Every field under here is
676
+ * opt-in and may change shape between minor releases. Stable features
677
+ * graduate out into top-level config keys.
678
+ */
679
+ experimental?: {
680
+ /**
681
+ * React Compiler (`babel-plugin-react-compiler`) integration. When
682
+ * `enabled: true` the bundler runs every `.jsx`/`.tsx` source on
683
+ * the **client build path** (islands, `"use client"` pages,
684
+ * partials) through Babel + react-compiler to auto-memoize
685
+ * components and values.
686
+ *
687
+ * SSR-only files (server `page.tsx` / `layout.tsx`) are NOT
688
+ * transformed — React Compiler memoizes re-renders and SSR only
689
+ * renders once before serializing to HTML, so the transform would
690
+ * add build time with zero runtime benefit.
691
+ *
692
+ * Requires peer dependencies: `@babel/core` + `babel-plugin-react-
693
+ * compiler`. Missing deps degrade gracefully — the plugin logs a
694
+ * warning and returns sources unchanged so the build still
695
+ * succeeds.
696
+ *
697
+ * @see packages/core/src/bundler/plugins/react-compiler.ts
698
+ */
699
+ reactCompiler?: {
700
+ /** Master switch. Default: `false`. */
701
+ enabled?: boolean;
702
+ /**
703
+ * Options forwarded verbatim to `babel-plugin-react-compiler`.
704
+ * Useful keys: `compilationMode`, `target`, `panicThreshold`.
705
+ * Omit to use react-compiler defaults.
706
+ */
707
+ compilerConfig?: Record<string, unknown>;
708
+ /**
709
+ * Phase 2 — CI-strict mode. When `true`, `mandu check` exits
710
+ * non-zero if the bailout linter reports any diagnostic. Useful
711
+ * as a regression gate once a project's client components are
712
+ * known-good. Default `false` (report without failing).
713
+ */
714
+ strict?: boolean;
715
+ };
716
+ };
660
717
  }
661
718
 
662
719
  export const CONFIG_FILES = [