@mandujs/core 0.43.1 → 0.44.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.43.1",
3
+ "version": "0.44.0",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `resolveReactCompilerConfig` — #240 Phase 2 auto-detect tests.
3
+ *
4
+ * The probe only fires when `enabled` is undefined. Explicit `true` /
5
+ * `false` veto the probe so user intent always wins.
6
+ *
7
+ * Cache lifetime is per-process; we reset it between cases so a probe
8
+ * from one fixture doesn't leak to the next.
9
+ */
10
+ import { describe, it, expect, beforeEach } from "bun:test";
11
+ import fs from "node:fs/promises";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+
15
+ import {
16
+ resolveReactCompilerConfig,
17
+ _resetReactCompilerConfigCache,
18
+ } from "../react-compiler-config";
19
+
20
+ async function makeRoot(prefix: string): Promise<string> {
21
+ return fs.mkdtemp(path.join(os.tmpdir(), `mandu-rc-${prefix}-`));
22
+ }
23
+
24
+ beforeEach(() => {
25
+ _resetReactCompilerConfigCache();
26
+ });
27
+
28
+ describe("resolveReactCompilerConfig", () => {
29
+ it("explicit enabled:true honours the user even when peers are missing", async () => {
30
+ const root = await makeRoot("explicit-on");
31
+ const result = resolveReactCompilerConfig({ enabled: true }, root);
32
+ expect(result.enabled).toBe(true);
33
+ expect(result.autoDetected).toBe(false);
34
+ await fs.rm(root, { recursive: true, force: true });
35
+ });
36
+
37
+ it("explicit enabled:false vetos the probe", async () => {
38
+ const root = await makeRoot("explicit-off");
39
+ const result = resolveReactCompilerConfig({ enabled: false }, root);
40
+ expect(result.enabled).toBe(false);
41
+ expect(result.autoDetected).toBe(false);
42
+ await fs.rm(root, { recursive: true, force: true });
43
+ });
44
+
45
+ it("undefined enabled + missing peers → disabled silently", async () => {
46
+ const root = await makeRoot("auto-no-peers");
47
+ // Empty rootDir — no node_modules, no package.json, no peer deps.
48
+ const result = resolveReactCompilerConfig(undefined, root);
49
+ expect(result.enabled).toBe(false);
50
+ expect(result.autoDetected).toBe(false);
51
+ await fs.rm(root, { recursive: true, force: true });
52
+ });
53
+
54
+ it("forwards compilerConfig regardless of enabled state", async () => {
55
+ const root = await makeRoot("compiler-config");
56
+ const cfg = { compilationMode: "annotation" };
57
+ const result = resolveReactCompilerConfig(
58
+ { enabled: true, compilerConfig: cfg },
59
+ root,
60
+ );
61
+ expect(result.compilerConfig).toBe(cfg);
62
+ await fs.rm(root, { recursive: true, force: true });
63
+ });
64
+
65
+ it("caches by (rootDir, explicit-enabled) — second call hits cache", async () => {
66
+ const root = await makeRoot("cache");
67
+ const a = resolveReactCompilerConfig(undefined, root);
68
+ const b = resolveReactCompilerConfig(undefined, root);
69
+ // Same identity — cache hit returns the stored object.
70
+ expect(a).toBe(b);
71
+ await fs.rm(root, { recursive: true, force: true });
72
+ });
73
+
74
+ it("treats explicit-true vs auto as separate cache keys", async () => {
75
+ const root = await makeRoot("cache-key");
76
+ const auto = resolveReactCompilerConfig(undefined, root);
77
+ const explicit = resolveReactCompilerConfig({ enabled: true }, root);
78
+ expect(auto).not.toBe(explicit);
79
+ expect(auto.enabled).toBe(false);
80
+ expect(explicit.enabled).toBe(true);
81
+ await fs.rm(root, { recursive: true, force: true });
82
+ });
83
+ });
@@ -36,6 +36,12 @@ export {
36
36
  type FormatCompilerReportOptions,
37
37
  } from "./react-compiler-lint";
38
38
 
39
+ export {
40
+ resolveReactCompilerConfig,
41
+ type RawReactCompilerConfig,
42
+ type ResolvedReactCompilerConfig,
43
+ } from "./react-compiler-config";
44
+
39
45
  /**
40
46
  * Subset of `ManduConfig.guard` consumed by `defaultBundlerPlugins()`.
41
47
  * We deliberately don't import the full `ManduConfig` type to keep the
@@ -0,0 +1,108 @@
1
+ /**
2
+ * React Compiler config resolver (#240 Phase 2 — auto-detect).
3
+ *
4
+ * The `experimental.reactCompiler` block in `mandu.config.ts` has three
5
+ * meaningful states for the `enabled` field:
6
+ *
7
+ * - `true` — user explicitly opts in. The transform plugin runs and
8
+ * warns if peer deps (`@babel/core`, `babel-plugin-react-compiler`)
9
+ * are missing.
10
+ * - `false` — user explicitly opts out. Plugin never runs.
11
+ * - `undefined` (the default) — Phase 2: probe whether the peer deps
12
+ * are installed in the project. If both resolve, treat as enabled
13
+ * so installing `babel-plugin-react-compiler` is the only step
14
+ * needed to turn auto-memoization on (zero-config goal of #240).
15
+ * If either is missing, stay disabled silently — no warning, no
16
+ * surface change for projects that haven't asked for the Compiler.
17
+ *
18
+ * The probe is synchronous (`Bun.resolveSync`) so it composes with the
19
+ * non-async `manduClientPlugins()` gate. Resolutions are cached per
20
+ * `(rootDir, enabled)` pair because the bundler asks for plugins many
21
+ * times during a single build (one for each entry / shim / island).
22
+ *
23
+ * @module core/bundler/plugins/react-compiler-config
24
+ */
25
+
26
+ export interface RawReactCompilerConfig {
27
+ enabled?: boolean;
28
+ compilerConfig?: Record<string, unknown>;
29
+ }
30
+
31
+ export interface ResolvedReactCompilerConfig {
32
+ /**
33
+ * Final on/off decision after applying auto-detect. Always a concrete
34
+ * boolean — callers do not need to repeat the probe.
35
+ */
36
+ enabled: boolean;
37
+ /** Forwarded to `babel-plugin-react-compiler`. */
38
+ compilerConfig?: Record<string, unknown>;
39
+ /**
40
+ * `true` when `enabled` was implicitly resolved from peer-dep probe
41
+ * (vs. set explicitly by the user). Surfaced so the bundler's plugin
42
+ * can suppress the "peer dep missing" warning — the implicit path
43
+ * already short-circuits before the plugin runs, but a future caller
44
+ * that bypasses this resolver would otherwise spam the warning.
45
+ */
46
+ autoDetected: boolean;
47
+ }
48
+
49
+ const cache = new Map<string, ResolvedReactCompilerConfig>();
50
+
51
+ /**
52
+ * Probe whether `@babel/core` and `babel-plugin-react-compiler` resolve
53
+ * from `rootDir`. Both must be present — the transform plugin loads
54
+ * them as a pair. Returns `false` on any resolution failure (missing
55
+ * dep, broken symlink, weird workspace layout) so the failure mode is
56
+ * "stay off" rather than "blow up boot".
57
+ */
58
+ function peerDepsInstalled(rootDir: string): boolean {
59
+ try {
60
+ Bun.resolveSync("@babel/core", rootDir);
61
+ Bun.resolveSync("babel-plugin-react-compiler", rootDir);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Resolve the user's `experimental.reactCompiler` block into a final
70
+ * on/off decision plus carried-over compiler options.
71
+ *
72
+ * Cache key includes `rootDir` and the explicit-enabled value so we
73
+ * can have, in tests, two projects in the same process with different
74
+ * enablement states.
75
+ */
76
+ export function resolveReactCompilerConfig(
77
+ raw: RawReactCompilerConfig | undefined,
78
+ rootDir: string,
79
+ ): ResolvedReactCompilerConfig {
80
+ const explicit = raw?.enabled;
81
+ const cacheKey = `${rootDir}::${explicit ?? "auto"}`;
82
+ const hit = cache.get(cacheKey);
83
+ if (hit) return hit;
84
+
85
+ let enabled: boolean;
86
+ let autoDetected = false;
87
+ if (explicit === true) {
88
+ enabled = true;
89
+ } else if (explicit === false) {
90
+ enabled = false;
91
+ } else {
92
+ enabled = peerDepsInstalled(rootDir);
93
+ autoDetected = enabled;
94
+ }
95
+
96
+ const result: ResolvedReactCompilerConfig = {
97
+ enabled,
98
+ compilerConfig: raw?.compilerConfig,
99
+ autoDetected,
100
+ };
101
+ cache.set(cacheKey, result);
102
+ return result;
103
+ }
104
+
105
+ /** Test-only — drop cached probes between fixture setups. */
106
+ export function _resetReactCompilerConfigCache(): void {
107
+ cache.clear();
108
+ }