@cfp-studio/plugin-react 17.3.0 → 17.3.2

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/preview/index.tsx CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { GetPropsFn, RendererLoader } from "@cfp-studio/iframe";
2
- import React from "react";
3
- import { ErrorBoundary, expectErrorBoundary } from "./error-boundary";
4
2
  // @ts-ignore
5
3
  import { render } from "__PREVIEWJS_PLUGIN_REACT_IMPORT_PATH__";
4
+ import React from "react";
5
+
6
+ import { ErrorBoundary, expectErrorBoundary } from "./error-boundary";
6
7
  import { rootContainer } from "./root";
7
8
 
8
9
  /**
@@ -17,78 +18,70 @@ import { rootContainer } from "./root";
17
18
  * @returns 一个对象,包含渲染函数和 JSX 工厂函数。
18
19
  */
19
20
  export const loadRenderer: RendererLoader = async ({
20
- wrapperModule,
21
- wrapperName,
22
- previewableModule,
23
- previewableName,
24
- renderId,
25
- shouldAbortRender,
21
+ wrapperModule,
22
+ wrapperName,
23
+ previewableModule,
24
+ previewableName,
25
+ renderId,
26
+ shouldAbortRender,
26
27
  }) => {
27
- const isStoryModule = !!previewableModule.default?.component;
28
- const Wrapper = wrapperModule?.[wrapperName || "Wrapper"] || React.Fragment;
29
- const Previewable =
30
- previewableModule[previewableName] ||
31
- previewableModule.PreviewJsEval(
32
- `try { return ${previewableName}; } catch { return null; }`
33
- );
34
- if (!Previewable) {
35
- throw new Error(`No component or story named '${previewableName}'`);
36
- }
37
- const decorators = [
38
- ...(Previewable.decorators || []),
39
- ...(previewableModule.default?.decorators || []),
40
- ];
41
- const RenderComponent = isStoryModule
42
- ? typeof Previewable === "function"
43
- ? Previewable
44
- : Previewable.render ||
45
- Previewable.component ||
46
- previewableModule.default?.render ||
47
- previewableModule.default?.component ||
48
- Previewable
49
- : Previewable;
50
- const Renderer = (props: any) => {
51
- return (
52
- <ErrorBoundary renderId={renderId}>
53
- <Wrapper>
54
- {decorators.reduce(
55
- (component, decorator) => () => decorator(component),
56
- () => <RenderComponent {...props} />
57
- )()}
58
- </Wrapper>
59
- </ErrorBoundary>
60
- );
61
- };
62
- return {
63
- render: async (getProps: GetPropsFn) => {
64
- if (shouldAbortRender()) {
65
- return;
66
- }
67
- await render(
68
- <Renderer
69
- {...getProps({
70
- presetGlobalProps: previewableModule.default?.args || {},
71
- presetProps: Previewable.args || {},
72
- })}
73
- />
74
- );
75
- if (shouldAbortRender()) {
76
- return;
77
- }
78
- const errorBoundary = await expectErrorBoundary(
79
- renderId,
80
- shouldAbortRender
81
- );
82
- if (!errorBoundary) {
83
- return;
84
- }
85
- if (errorBoundary.state.error) {
86
- throw errorBoundary.state.error;
87
- }
88
- if (Previewable.play) {
89
- await Previewable.play({ canvasElement: rootContainer });
90
- }
91
- },
92
- jsxFactory: React.createElement,
93
- };
28
+ const isStoryModule = !!previewableModule.default?.component;
29
+ const Wrapper = wrapperModule?.[wrapperName || "Wrapper"] || React.Fragment;
30
+ const Previewable =
31
+ previewableModule[previewableName] ||
32
+ previewableModule.PreviewJsEval(`try { return ${previewableName}; } catch { return null; }`);
33
+ if (!Previewable) {
34
+ throw new Error(`No component or story named '${previewableName}'`);
35
+ }
36
+ const decorators = [...(Previewable.decorators || []), ...(previewableModule.default?.decorators || [])];
37
+ const RenderComponent = isStoryModule
38
+ ? typeof Previewable === "function"
39
+ ? Previewable
40
+ : Previewable.render ||
41
+ Previewable.component ||
42
+ previewableModule.default?.render ||
43
+ previewableModule.default?.component ||
44
+ Previewable
45
+ : Previewable;
46
+ const Renderer = (props: any) => {
47
+ return (
48
+ <ErrorBoundary renderId={renderId}>
49
+ <Wrapper>
50
+ {decorators.reduce(
51
+ (component, decorator) => () => decorator(component),
52
+ () => <RenderComponent {...props} />,
53
+ )()}
54
+ </Wrapper>
55
+ </ErrorBoundary>
56
+ );
57
+ };
58
+ return {
59
+ render: async (getProps: GetPropsFn) => {
60
+ if (shouldAbortRender()) {
61
+ return;
62
+ }
63
+ await render(
64
+ <Renderer
65
+ {...getProps({
66
+ presetGlobalProps: previewableModule.default?.args || {},
67
+ presetProps: Previewable.args || {},
68
+ })}
69
+ />,
70
+ );
71
+ if (shouldAbortRender()) {
72
+ return;
73
+ }
74
+ const errorBoundary = await expectErrorBoundary(renderId, shouldAbortRender);
75
+ if (!errorBoundary) {
76
+ return;
77
+ }
78
+ if (errorBoundary.state.error) {
79
+ throw errorBoundary.state.error;
80
+ }
81
+ if (Previewable.play) {
82
+ await Previewable.play({ canvasElement: rootContainer });
83
+ }
84
+ },
85
+ jsxFactory: React.createElement,
86
+ };
94
87
  };
@@ -1,6 +1,7 @@
1
1
  import ReactDOM from "react-dom";
2
+
2
3
  import { rootContainer } from "./root";
3
4
 
4
5
  export function render(node: JSX.Element) {
5
- ReactDOM.render(node, rootContainer);
6
+ ReactDOM.render(node, rootContainer);
6
7
  }
@@ -1,11 +1,12 @@
1
1
  import type { Root } from "react-dom/client";
2
2
  import { createRoot } from "react-dom/client";
3
+
3
4
  import { rootContainer } from "./root";
4
5
 
5
6
  let root: Root | null = null;
6
7
  export function render(node: JSX.Element) {
7
- if (!root) {
8
- root = createRoot(rootContainer);
9
- }
10
- root.render(node);
8
+ if (!root) {
9
+ root = createRoot(rootContainer);
10
+ }
11
+ root.render(node);
11
12
  }
@@ -0,0 +1,158 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { generatePath, matchPath, resolvePath } from "./umi.js";
4
+
5
+ /**
6
+ * 对照测试:shim 里 react-router 纯函数的转导出必须与 `react-router@6.3.0`
7
+ * 的**真实输出**一致。
8
+ *
9
+ * 每个期望值都是在真实包上跑出来的(不是手写的猜测):
10
+ *
11
+ * ```sh
12
+ * node -e "const rr=require('<...>/react-router/main.js'); console.log(rr.matchPath('/users/:id','/users/42'))"
13
+ * ```
14
+ *
15
+ * ## 为什么这份测试重要
16
+ *
17
+ * 早期 shim 用的是「按 `/` 切段逐个比」的简版,错四处且都很难发现:
18
+ *
19
+ * | # | 输入 | 简版错误 | 后果 |
20
+ * | --- | --- | --- | --- |
21
+ * | 1 | 任意 | 返回 `params` 本体而非 `{ params, pathname, pathnameBase, pattern }` | 写 `match?.params.id` 的组件拿到 `undefined` |
22
+ * | 2 | `/files/*` + `/files/a/b/c` | 丢掉 splat 捕获 | 组件看到「匹配成功」但 `params["*"]` 为空 |
23
+ * | 3 | `/users/:id` + `/Users/42` | 大小写敏感 → `null` | 大小写不同的 URL 导不了 |
24
+ * | 4 | `/users/:id?` + `/users/42` | 产出 `"id?"` 这个脏键 | 参数名带问号 |
25
+ *
26
+ * 简版还让 e2e 用例 3e 断言的是 `match?.id`(简版的形状)—— 测试是绿的,
27
+ * 但语义是错的。这份单测把真实形状固定下来。
28
+ *
29
+ * ## 为什么测试放在 `src/` 而不是 `preview/`
30
+ *
31
+ * `preview/` 整体被 `vitest.config.ts` 排除(里面是浏览器端渲染入口)。
32
+ * 这份用例虽然测的是 `preview/shims/umi.tsx`,但它只是普通函数、不碰 DOM,
33
+ * 从 `src/` 引用即可。
34
+ */
35
+ describe("matchPath 与 react-router 6.3 对齐", () => {
36
+ it("返回 `{ params, pathname, pathnameBase, pattern }` 四键(不是 params 本体)", () => {
37
+ const match = matchPath("/users/:id", "/users/42");
38
+ expect(Object.keys(match ?? {}).sort()).toEqual(["params", "pathname", "pathnameBase", "pattern"]);
39
+ });
40
+
41
+ it("基本参数捕获", () => {
42
+ expect(matchPath("/users/:id", "/users/42")).toEqual({
43
+ params: { id: "42" },
44
+ pathname: "/users/42",
45
+ pathnameBase: "/users/42",
46
+ pattern: { path: "/users/:id", caseSensitive: false, end: true },
47
+ });
48
+ });
49
+
50
+ it("默认大小写不敏感(`caseSensitive: false`)", () => {
51
+ expect(matchPath("/users/:id", "/Users/42")?.params.id).toBe("42");
52
+ });
53
+
54
+ it("`caseSensitive: true` 时大小写敏感", () => {
55
+ expect(matchPath({ path: "/users/:id", caseSensitive: true }, "/Users/42")).toBeNull();
56
+ });
57
+
58
+ it('尾部 splat 是具名捕获组,值进 `params["*"]`', () => {
59
+ const match = matchPath("/files/*", "/files/a/b/c");
60
+ expect(match?.params["*"]).toBe("a/b/c");
61
+ // `pathnameBase` 不含 splat 部分。
62
+ expect(match?.pathnameBase).toBe("/files");
63
+ });
64
+
65
+ it("`/files/*` 也能匹配 `/files` 本身(splat 为空字符串,不是 undefined)", () => {
66
+ expect(matchPath("/files/*", "/files")?.params["*"]).toBe("");
67
+ });
68
+
69
+ it("`/users/:id/*` 同时捕获参数与 splat", () => {
70
+ const match = matchPath("/users/:id/*", "/users/42/extra/deep");
71
+ expect(match?.params.id).toBe("42");
72
+ expect(match?.params["*"]).toBe("extra/deep");
73
+ });
74
+
75
+ it("`end: false` 允许前缀匹配", () => {
76
+ expect(matchPath({ path: "/users", end: false }, "/users/42/extra")?.pathnameBase).toBe("/users");
77
+ });
78
+
79
+ it("默认 `end: true`:多出的段不匹配", () => {
80
+ expect(matchPath("/users/:id", "/users/42/extra")).toBeNull();
81
+ });
82
+
83
+ it("忽略尾部斜杠", () => {
84
+ expect(matchPath("/users/:id/", "/users/42")?.params.id).toBe("42");
85
+ });
86
+
87
+ it("根路径", () => {
88
+ expect(matchPath("/", "/")).toEqual({
89
+ params: {},
90
+ pathname: "/",
91
+ pathnameBase: "/",
92
+ pattern: { path: "/", caseSensitive: false, end: true },
93
+ });
94
+ });
95
+
96
+ it("纯 `*` 匹配任意路径", () => {
97
+ expect(matchPath("*", "/anything/at/all")?.params["*"]).toBe("anything/at/all");
98
+ });
99
+
100
+ it("URL 段会被解码", () => {
101
+ expect(matchPath("/u/:id", "/u/%E4%B8%AD")?.params.id).toBe("中");
102
+ });
103
+
104
+ it("malformed 百分号编码不抛错,原样返回", () => {
105
+ expect(matchPath("/u/:id", "/u/%ZZ")?.params.id).toBe("%ZZ");
106
+ });
107
+
108
+ it("`:id?` 在 v6 不是可选段,且不产出 `id?` 这种脏键", () => {
109
+ // 与真实 react-router 一致:`?` 被当作字面字符,匹配不上。
110
+ expect(matchPath("/users/:id?", "/users/42")).toBeNull();
111
+ expect(matchPath("/users/:id?", "/users")).toBeNull();
112
+ });
113
+ });
114
+
115
+ describe("resolvePath 与 react-router 6.3 对齐", () => {
116
+ const cases: Array<[string, string, string]> = [
117
+ ["child", "/parent/", "/parent/child"],
118
+ ["child", "/parent", "/parent/child"],
119
+ ["../sibling", "/a/b/", "/a/sibling"],
120
+ ["../../up", "/a/b/c", "/a/up"],
121
+ ["./same", "/a/b", "/a/b/same"],
122
+ ["", "/a/b", "/a/b"],
123
+ ["/abs", "/a/b", "/abs"],
124
+ ["child", "/", "/child"],
125
+ ];
126
+
127
+ for (const [to, from, expected] of cases) {
128
+ it(`resolvePath(${JSON.stringify(to)}, ${JSON.stringify(from)}) → ${expected}`, () => {
129
+ expect(resolvePath(to, from).pathname).toBe(expected);
130
+ });
131
+ }
132
+
133
+ it("保留 search 与 hash(并补上前缀符号)", () => {
134
+ expect(resolvePath("x?q=1#h", "/base/")).toEqual({ pathname: "/base/x", search: "?q=1", hash: "#h" });
135
+ });
136
+ });
137
+
138
+ describe("generatePath 与 react-router 6.3 对齐", () => {
139
+ it("替换 `:param`", () => {
140
+ expect(generatePath("/users/:id/posts/:postId", { id: "7", postId: "9" })).toBe("/users/7/posts/9");
141
+ });
142
+
143
+ it("不做 URL 编码(中文原样)", () => {
144
+ expect(generatePath("/u/:id", { id: "中文" })).toBe("/u/中文");
145
+ });
146
+
147
+ it("支持尾部 splat", () => {
148
+ expect(generatePath("/files/*", { "*": "a/b" })).toBe("/files/a/b");
149
+ });
150
+
151
+ it("splat 为空时去掉末尾星号", () => {
152
+ expect(generatePath("/files/*", {})).toBe("/files");
153
+ });
154
+
155
+ it("缺失参数抛错(与真实实现一致)", () => {
156
+ expect(() => generatePath("/u/:id", {})).toThrow('Missing ":id" param');
157
+ });
158
+ });