@mandujs/core 0.54.25 → 0.54.27

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.54.25",
3
+ "version": "0.54.27",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -1,51 +1,86 @@
1
1
  /**
2
- * Regression guard for issue #322 — dev island hydration broke because the
3
- * generated `_jsx-dev-runtime` shim imported `jsxDEV` from bare `"react"`.
4
- * React's main entry does NOT export `jsxDEV` (it lives in
5
- * `react/jsx-dev-runtime`), so `jsxDEV` resolved to `undefined` and every
6
- * island threw `TypeError: jsxDEV is not a function` in dev mode.
2
+ * Regression guard for issues #322 / #323 — dev island hydration broke with
3
+ * `TypeError: jsxDEV is not a function`.
7
4
  *
8
- * These tests assert the generated shim sources reference the correct React
9
- * subpaths and never pull JSX runtime functions from bare `"react"`.
5
+ * #322: the shim imported `jsxDEV` from bare `"react"`, which (via the import
6
+ * map `_react.js`) only worked if `_react.js` re-exported jsxDEV.
7
+ * #323: the "fix" changed the import to `"react/jsx-dev-runtime"`, but the
8
+ * import map maps `react/jsx-dev-runtime` to THIS shim — a circular
9
+ * self-import, so jsxDEV stayed `undefined`.
10
+ *
11
+ * The correct source: import JSX runtime functions from bare `"react"`, which
12
+ * the import map resolves to the self-contained `_react.js` vendor bundle
13
+ * (built with no `external`, so the real jsx/jsxs/jsxDEV/Fragment are inlined
14
+ * and re-exported). The shim must NEVER import from the import-map-aliased
15
+ * subpath that points back at itself.
10
16
  */
11
17
  import { describe, expect, test } from "bun:test";
18
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
19
+ import { tmpdir } from "node:os";
20
+ import path from "node:path";
12
21
  import {
13
22
  _testOnly_generateJsxDevRuntimeShimSource,
14
23
  _testOnly_generateJsxRuntimeShimSource,
15
24
  } from "../build";
16
25
 
17
- describe("JSX runtime shim sources (issue #322)", () => {
18
- test("dev shim imports jsxDEV/Fragment from react/jsx-dev-runtime", () => {
26
+ describe("JSX runtime shim sources (issues #322 / #323)", () => {
27
+ test("dev shim imports jsxDEV/Fragment from bare 'react', never the self-aliased subpath", () => {
19
28
  const source = _testOnly_generateJsxDevRuntimeShimSource();
20
29
 
21
- // The fix: jsxDEV + Fragment must come from the real subpath.
22
- expect(source).toContain("react/jsx-dev-runtime");
23
- expect(source).toMatch(
24
- /import\s*\{\s*jsxDEV\s*,\s*Fragment\s*\}\s*from\s*['"]react\/jsx-dev-runtime['"]/,
25
- );
30
+ // #323: must NOT import from 'react/jsx-dev-runtime' that is this shim's
31
+ // own import-map alias, which would be a circular self-import. (Targets
32
+ // the import statement, not comment mentions of the path.)
33
+ expect(source).not.toMatch(/from\s*['"]react\/jsx-dev-runtime['"]/);
26
34
 
27
- // Regression: must NOT import jsxDEV from bare "react".
28
- expect(source).not.toMatch(
29
- /import\s*\{[^}]*jsxDEV[^}]*\}\s*from\s*['"]react['"]/,
35
+ // Correct: jsxDEV + Fragment come from bare 'react' (→ _react.js).
36
+ expect(source).toMatch(
37
+ /import\s*\{\s*jsxDEV\s*,\s*Fragment\s*\}\s*from\s*['"]react['"]/,
30
38
  );
31
39
 
32
40
  // Shim still re-exports what the import map expects.
33
41
  expect(source).toContain("export { jsxDEV, Fragment }");
34
42
  });
35
43
 
36
- test("prod shim imports jsx/jsxs/Fragment from react/jsx-runtime", () => {
44
+ test("prod shim imports jsx/jsxs/Fragment from bare 'react', never the self-aliased subpath", () => {
37
45
  const source = _testOnly_generateJsxRuntimeShimSource();
38
46
 
39
- expect(source).toContain("react/jsx-runtime");
40
- expect(source).toMatch(
41
- /import\s*\{\s*jsx\s*,\s*jsxs\s*,\s*Fragment\s*\}\s*from\s*['"]react\/jsx-runtime['"]/,
42
- );
47
+ // #323: must NOT import from 'react/jsx-runtime' (this shim's own alias).
48
+ expect(source).not.toMatch(/from\s*['"]react\/jsx-runtime['"]/);
43
49
 
44
- // Regression: must NOT import jsx/jsxs from bare "react".
45
- expect(source).not.toMatch(
46
- /import\s*\{[^}]*\bjsx\b[^}]*\}\s*from\s*['"]react['"]/,
50
+ expect(source).toMatch(
51
+ /import\s*\{\s*jsx\s*,\s*jsxs\s*,\s*Fragment\s*\}\s*from\s*['"]react['"]/,
47
52
  );
48
53
 
49
54
  expect(source).toContain("export { jsx, jsxs, Fragment }");
50
55
  });
56
+
57
+ // #323 build-level guard: the SOURCE test alone missed the real bug because
58
+ // the broken 0.54.24 build still emitted `from "react/jsx-dev-runtime"` in
59
+ // the OUTPUT (the import-map alias pointing back at this shim). Build the
60
+ // shim the same way buildVendorShims does (external: ["react"]) and assert
61
+ // the emitted module never self-imports the alias.
62
+ test("built dev shim never self-imports react/jsx-dev-runtime", async () => {
63
+ const dir = await mkdtemp(path.join(tmpdir(), "mandu-jsx-shim-"));
64
+ try {
65
+ const srcPath = path.join(dir, "_jsx-dev-runtime.js");
66
+ await writeFile(srcPath, _testOnly_generateJsxDevRuntimeShimSource(), "utf-8");
67
+
68
+ const result = await Bun.build({
69
+ entrypoints: [srcPath],
70
+ external: ["react"],
71
+ target: "browser",
72
+ });
73
+
74
+ expect(result.success).toBe(true);
75
+ const out = await result.outputs[0]!.text();
76
+
77
+ // The emitted shim must source jsxDEV from bare 'react' (→ _react.js),
78
+ // NOT from the import-map alias that resolves back to itself.
79
+ expect(out).not.toMatch(/from\s*["']react\/jsx-dev-runtime["']/);
80
+ expect(out).toMatch(/from\s*["']react["']/);
81
+ expect(out).toContain("jsxDEV");
82
+ } finally {
83
+ await rm(dir, { recursive: true, force: true });
84
+ }
85
+ });
51
86
  });
@@ -633,11 +633,13 @@ function generateJsxRuntimeShimSource(): string {
633
633
  /**
634
634
  * Mandu JSX Runtime Shim (Generated)
635
635
  * Production JSX 변환용
636
- * jsx/jsxs/Fragment는 'react/jsx-runtime' 원본에서 직접 가져온다.
637
- * import map의 'react/jsx-runtime'이 다시 이 셰임을 가리키므로,
638
- * 셰임 내부는 실제 원본 경로를 직접 참조해야 순환/누락이 생기지 않는다.
636
+ *
637
+ * #323: import map의 'react/jsx-runtime'이 다시 이 셰임을 가리키므로
638
+ * 'react/jsx-runtime'에서 import하면 순환 self-import가 된다. 대신
639
+ * 'react'(→ _react.js, external 없이 react 전체를 인라인하며 jsx/jsxs/
640
+ * Fragment를 re-export하는 vendor 번들)에서 가져와 순환을 끊는다.
639
641
  */
640
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
642
+ import { jsx, jsxs, Fragment } from 'react';
641
643
 
642
644
  // Named exports
643
645
  export { jsx, jsxs, Fragment };
@@ -656,11 +658,14 @@ function generateJsxDevRuntimeShimSource(): string {
656
658
  /**
657
659
  * Mandu JSX Dev Runtime Shim (Generated)
658
660
  * Development JSX 변환용
659
- * jsxDEV/Fragment는 'react'가 아니라 'react/jsx-dev-runtime'에서만 export된다.
660
- * import map의 'react/jsx-dev-runtime'이 다시 이 셰임을 가리키므로,
661
- * 셰임 내부는 실제 원본 경로를 직접 참조해야 순환/누락(jsxDEV undefined)이 생기지 않는다.
661
+ *
662
+ * #323: import map의 'react/jsx-dev-runtime'이 다시 이 셰임을 가리키므로
663
+ * 'react/jsx-dev-runtime'에서 import하면 순환 self-import가 되어 jsxDEV
664
+ * undefined가 된다. 대신 'react'(→ _react.js, external 없이 react 전체를
665
+ * 인라인하며 jsxDEV/Fragment를 re-export하는 vendor 번들)에서 가져와
666
+ * 순환을 끊는다.
662
667
  */
663
- import { jsxDEV, Fragment } from 'react/jsx-dev-runtime';
668
+ import { jsxDEV, Fragment } from 'react';
664
669
 
665
670
  // Named exports
666
671
  export { jsxDEV, Fragment };
@@ -56,6 +56,8 @@ const DEFAULT_OPTIONS: Required<NetworkProxyOptions> = {
56
56
  ignorePatterns: [
57
57
  // DevTools 자체 요청
58
58
  /__mandu/,
59
+ // DevTools 에러 리포트 엔드포인트 (자기추적/피드백 루프 방지)
60
+ /__kitchen/,
59
61
  // HMR
60
62
  /__vite/,
61
63
  /\.hot-update\./,
@@ -174,11 +174,22 @@ export class SourceContextProvider {
174
174
  * 안전한 경로 확인 (Path Traversal 방지)
175
175
  */
176
176
  private resolveSafePath(file: string): string | null {
177
+ // Reject absolute inputs outright: path.resolve(root, "/etc/passwd")
178
+ // returns "/etc/passwd", escaping the project root.
179
+ if (path.isAbsolute(file)) {
180
+ return null;
181
+ }
182
+
177
183
  const absolutePath = path.resolve(this.options.projectRoot, file);
178
184
  const normalizedRoot = path.normalize(this.options.projectRoot);
179
-
180
- // 확인된 경로가 프로젝트 루트 내에 있는지 확인
181
- if (!absolutePath.startsWith(normalizedRoot)) {
185
+ // Compare with a trailing separator so a sibling sharing the root's
186
+ // prefix (e.g. "/app/project-secrets" vs root "/app/project") is NOT
187
+ // treated as inside the root. Allow the root itself.
188
+ const rootWithSep = normalizedRoot.endsWith(path.sep)
189
+ ? normalizedRoot
190
+ : normalizedRoot + path.sep;
191
+
192
+ if (absolutePath !== normalizedRoot && !absolutePath.startsWith(rootWithSep)) {
182
193
  return null;
183
194
  }
184
195
 
@@ -245,6 +245,8 @@ export class WorkerManager {
245
245
  { source: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}', label: 'EMAIL' },
246
246
  { source: '\\\\+?[1-9]\\\\d{1,14}', label: 'PHONE' },
247
247
  { source: '\\\\b(?:\\\\d{1,3}\\\\.){3}\\\\d{1,3}\\\\b', label: 'IP' },
248
+ { source: '\\\\b(?:\\\\d{4}[- ]?){3}\\\\d{4}\\\\b', label: 'CARD' },
249
+ { source: '\\\\b\\\\d{3}-\\\\d{2}-\\\\d{4}\\\\b', label: 'SSN' },
248
250
  ];
249
251
 
250
252
  function applyPattern(text, pattern) {