@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.
- package/package.json +21 -4
- package/src/auth/__tests__/login.test.ts +420 -419
- package/src/auth/__tests__/reset.test.ts +296 -296
- package/src/brain/adapters/anthropic-oauth.ts +421 -420
- package/src/brain/adapters/index.ts +2 -1
- package/src/brain/adapters/ollama.ts +1 -1
- package/src/brain/adapters/openai-oauth.ts +534 -533
- package/src/brain/brain.ts +2 -1
- package/src/brain/redactor.ts +196 -196
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
- package/src/bundler/__tests__/cold-start.test.ts +504 -504
- package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
- package/src/bundler/__tests__/hdr.test.ts +1 -1
- package/src/bundler/analyzer.ts +958 -958
- package/src/bundler/build.ts +104 -14
- package/src/bundler/dev.ts +125 -0
- package/src/bundler/hmr-types.ts +1 -0
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
- package/src/bundler/plugins/index.ts +14 -0
- package/src/bundler/plugins/react-compiler-lint.ts +253 -0
- package/src/bundler/plugins/react-compiler.ts +162 -0
- package/src/bundler/types.ts +12 -0
- package/src/change/integrity.ts +2 -1
- package/src/client/index.ts +10 -0
- package/src/client/island.ts +38 -11
- package/src/client/router.ts +6 -1
- package/src/config/mandu.ts +57 -0
- package/src/config/validate.ts +42 -0
- package/src/content/collection.ts +844 -809
- package/src/content/content-layer.ts +316 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/digest.ts +133 -133
- package/src/content/generate-types.ts +168 -168
- package/src/content/index.ts +6 -1
- package/src/content/llms-txt.ts +277 -277
- package/src/contract/define.ts +474 -474
- package/src/contract/route-helpers.ts +2 -1
- package/src/contract/zod-utils.ts +158 -155
- package/src/db/index.ts +513 -513
- package/src/desktop/__tests__/smoke.test.ts +100 -100
- package/src/desktop/webview-fallback.ts +583 -583
- package/src/desktop/window.ts +3 -1
- package/src/dev-error-overlay/overlay-client.ts +300 -300
- package/src/devtools/ai/mcp-connector.ts +499 -498
- package/src/devtools/client/components/kitchen-root.tsx +7 -2
- package/src/email/resend.ts +163 -163
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/graph.ts +898 -898
- package/src/guard/index.ts +16 -0
- package/src/guard/statistics.ts +578 -578
- package/src/guard/tsgolint-bridge.ts +512 -0
- package/src/i18n/locale-resolver.ts +214 -214
- package/src/id/__tests__/id.test.ts +120 -120
- package/src/intent/index.ts +321 -321
- package/src/island/index.ts +39 -23
- package/src/kitchen/api/contract-api.ts +15 -8
- package/src/kitchen/kitchen-ui.ts +2137 -2137
- package/src/lockfile/index.ts +3 -2
- package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
- package/src/middleware/secure/index.ts +417 -417
- package/src/observability/event-bus.ts +2 -2
- package/src/observability/metrics.ts +334 -334
- package/src/observability/tracing.ts +694 -694
- package/src/openapi/generator.ts +1 -1
- package/src/perf/user-marks.ts +553 -553
- package/src/plugins/registry.ts +387 -387
- package/src/resource/ddl/diff.ts +392 -392
- package/src/resource/ddl/snapshot.ts +448 -447
- package/src/resource/generator-schema.ts +477 -476
- package/src/resource/parser.ts +4 -2
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.ts +422 -422
- package/src/runtime/fast-refresh-types.ts +126 -128
- package/src/runtime/image-handler.ts +206 -195
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +36 -19
- package/src/runtime/session-key.ts +328 -328
- package/src/scheduler/__tests__/scheduler.test.ts +514 -514
- package/src/seo/resolve/index.ts +353 -353
- package/src/spec/load.ts +1 -1
- package/src/testing/reporter.ts +676 -676
- package/src/testing/server.ts +196 -196
- package/src/testing/snapshot.ts +444 -444
- package/src/utils/__tests__/lru-cache.test.ts +186 -186
- 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
|
+
}
|
package/src/bundler/types.ts
CHANGED
|
@@ -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
|
package/src/change/integrity.ts
CHANGED
|
@@ -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
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -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;
|
package/src/client/island.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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 {
|
package/src/client/router.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/config/mandu.ts
CHANGED
|
@@ -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 = [
|