@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.
@@ -0,0 +1,580 @@
1
+ /**
2
+ * `umi` / `@umijs/max` 的最小 shim(预览环境专用)。
3
+ *
4
+ * 见 `docs/spec-umijs-support.md` §4.2。**这个文件不进业务代码**,
5
+ * 只在预览服务器里通过 `resolve.alias` 顶替真实的 `umi` 包。
6
+ *
7
+ * ## 为什么要 shim
8
+ *
9
+ * `umi` 是**服务端 + 客户端混合**的包,直接 import 会用到 `process.platform`、
10
+ * `fs` 等 Node API,在浏览器里直接崩(实测:
11
+ * `TypeError: Cannot read properties of undefined (reading 'platform')`)。
12
+ *
13
+ * ## 设计原则
14
+ *
15
+ * 1. **渲染路径上的高频 API 给降级值**(`useModel` / `useRequest`)——
16
+ * 抛错会让整棵组件树渲染不出来,预览就失去意义。
17
+ * 2. **不在渲染路径的 API 显式抛错**(`request`)—— 静默 `undefined`
18
+ * 会让人以为"跑通了但没数据",比报错更难排查。
19
+ * 3. **不伪造数据**:空态就是空态,不编造看起来合理的假值。
20
+ */
21
+ import { type AnchorHTMLAttributes, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
22
+
23
+ /** 内存版位置栈:`history.push` 后可被 `useLocation` 观察到。 */
24
+ interface MemoryLocation {
25
+ pathname: string;
26
+ search: string;
27
+ hash: string;
28
+ state: unknown;
29
+ }
30
+
31
+ let currentLocation: MemoryLocation = { pathname: "/", search: "", hash: "", state: null };
32
+ const locationListeners = new Set<(location: MemoryLocation) => void>();
33
+
34
+ function parsePath(to: string): MemoryLocation {
35
+ const [beforeHash, hash = ""] = to.split("#");
36
+ const [pathname, search = ""] = (beforeHash ?? "/").split("?");
37
+ return { pathname: pathname || "/", search, hash, state: null };
38
+ }
39
+
40
+ function navigate(to: string): void {
41
+ currentLocation = parsePath(to);
42
+ for (const listener of locationListeners) {
43
+ listener(currentLocation);
44
+ }
45
+ }
46
+
47
+ export function useLocation() {
48
+ const [location, setLocation] = useState(currentLocation);
49
+ useEffect(() => {
50
+ locationListeners.add(setLocation);
51
+ return () => {
52
+ locationListeners.delete(setLocation);
53
+ };
54
+ }, []);
55
+ return location;
56
+ }
57
+
58
+ export function useNavigate() {
59
+ return useCallback((to: string) => {
60
+ navigate(to);
61
+ }, []);
62
+ }
63
+
64
+ /** 路由参数:预览里没有路由,恒为空对象。 */
65
+ export function useParams(): Record<string, string> {
66
+ return useMemo(() => ({}) as Record<string, string>, []);
67
+ }
68
+
69
+ export function useSearchParams(): URLSearchParams {
70
+ return useMemo(() => new URLSearchParams(currentLocation.search), []);
71
+ }
72
+
73
+ export const history = {
74
+ get location() {
75
+ return currentLocation;
76
+ },
77
+ push: navigate,
78
+ replace: navigate,
79
+ goBack: () => undefined,
80
+ listen: (listener: (location: MemoryLocation) => void) => {
81
+ locationListeners.add(listener);
82
+ return () => {
83
+ locationListeners.delete(listener);
84
+ };
85
+ },
86
+ };
87
+
88
+ type LinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & { to?: string; children?: ReactNode };
89
+
90
+ /** `Link` 渲染成 `<a>`,点击被拦截(不真正导航,避免预览页跳走)。 */
91
+ export function Link({ to = "#", children, onClick, ...rest }: LinkProps) {
92
+ return (
93
+ <a
94
+ {...rest}
95
+ href={to}
96
+ onClick={(event) => {
97
+ event.preventDefault();
98
+ onClick?.(event);
99
+ navigate(to);
100
+ }}
101
+ >
102
+ {children}
103
+ </a>
104
+ );
105
+ }
106
+
107
+ /** `NavLink` 与 `Link` 同构;预览里恒为「未激活」。 */
108
+ export function NavLink({ children, ...rest }: LinkProps) {
109
+ return <Link {...rest}>{children}</Link>;
110
+ }
111
+
112
+ /**
113
+ * `useModel` —— **恒返回空对象**。
114
+ *
115
+ * P0 不猜初值(见 spec §9.1 的评审修正 2):需要真实初值时,
116
+ * 由用户在 `__previewjs__/Wrapper.tsx` 里包 Provider。
117
+ *
118
+ * selector 形式 `useModel(ns, s => …)` 正常运行(对 `{}` 求值)。
119
+ */
120
+ export function useModel<T = Record<string, unknown>>(
121
+ _namespace: string,
122
+ selector?: (model: Record<string, unknown>) => T,
123
+ ): T {
124
+ const model = useMemo(() => ({}), []);
125
+ return (selector ? selector(model) : model) as T;
126
+ }
127
+
128
+ /** `connect` 直通 HOC:注入空 props,不做状态映射。 */
129
+ export function connect<T>(Component: T): T {
130
+ return Component;
131
+ }
132
+
133
+ /* ------------------------------------------------------------------ *
134
+ * 应用上下文(`@umijs/renderer-react` 的 appContext)
135
+ *
136
+ * 这几个都在**渲染路径上**,且 `@umijs/max` 的页面组件很常见 ——
137
+ * 缺任何一个,整个模块会因 ESM 静态导出校验直接 `SyntaxError`,组件连挂载的机会都没有。
138
+ * 所以统一给「结构完整、内容为空」的降级值,而不是抛错。
139
+ * ------------------------------------------------------------------ */
140
+
141
+ /**
142
+ * `useAppData()` —— 返回空的应用上下文。
143
+ *
144
+ * 真实返回值包含 `routes` / `clientRoutes` / `clientLoaderData` / `pluginManager` 等。
145
+ * 这里只保证**字段存在且类型形状对**,不伪造路由表(组件若遍历 `routes` 会得到空循环,
146
+ * 而不是 `Cannot read properties of undefined`)。
147
+ */
148
+ export function useAppData() {
149
+ return useMemo(
150
+ () => ({
151
+ routes: {} as Record<string, unknown>,
152
+ routeComponents: {} as Record<string, unknown>,
153
+ clientRoutes: [] as unknown[],
154
+ pluginManager: undefined as unknown,
155
+ clientLoaderData: {} as Record<string, unknown>,
156
+ serverLoaderData: {} as Record<string, unknown>,
157
+ preloadRoute: (_to: string) => undefined,
158
+ basename: "/",
159
+ history,
160
+ }),
161
+ [],
162
+ );
163
+ }
164
+
165
+ /** 当前选中的路由链:预览里没有路由,恒为空数组。 */
166
+ export function useSelectedRoutes(): unknown[] {
167
+ return useMemo(() => [], []);
168
+ }
169
+
170
+ /**
171
+ * 当前路由的配置(`IRoute`)。
172
+ *
173
+ * 恒返回 `{}`:真实的 `path` / `component` / `routes` 都由约定式路由生成,
174
+ * 预览不重建那套映射(那是 `specs/0001` 应用预览的领域)。
175
+ */
176
+ export function useRouteProps<T = Record<string, unknown>>(): T {
177
+ return useMemo(() => ({}) as T, []);
178
+ }
179
+
180
+ /**
181
+ * 路由上下文(`IRouteContextType`)。
182
+ *
183
+ * 注意它与 `useRouteProps()` 不同:后者是路由配置对象本身,前者是
184
+ * `{ route: IRoute }` 的包装 —— 组件常见写法是 `useRouteData().route`。
185
+ */
186
+ export function useRouteData(): { route: Record<string, unknown> } {
187
+ return useMemo(() => ({ route: {} }), []);
188
+ }
189
+
190
+ /** loader 数据:三个 loader hook 都返回空态,形状对齐真实签名。 */
191
+ export function useServerLoaderData(): { data: undefined } {
192
+ return useMemo(() => ({ data: undefined }), []);
193
+ }
194
+
195
+ export function useClientLoaderData(): { data: undefined } {
196
+ return useMemo(() => ({ data: undefined }), []);
197
+ }
198
+
199
+ /** `useLoaderData()` 直接返回数据本身(不是 `{ data }` 包装)。 */
200
+ export function useLoaderData(): undefined {
201
+ return undefined;
202
+ }
203
+
204
+ /* ------------------------------------------------------------------ *
205
+ * react-router 转导出
206
+ *
207
+ * `@umijs/max` 的 `umi` 入口会把这些 react-router 的 API 再导出一遍。
208
+ * 预览不引入 react-router(它有自己的 Router 上下文),所以全部给静态降级值。
209
+ * ------------------------------------------------------------------ */
210
+
211
+ /**
212
+ * `useFetcher()` —— 返回空态的 fetcher。
213
+ *
214
+ * 真实签名里 `state` 取 `"idle" | "loading" | "submitting"`,这里恒为 `idle`,
215
+ * 让组件的 `if (fetcher.state !== "idle") return <Spin/>` 分支走正常路径。
216
+ */
217
+ export function useFetcher() {
218
+ return useMemo(
219
+ () => ({
220
+ state: "idle" as const,
221
+ data: undefined,
222
+ formData: undefined,
223
+ load: async () => undefined,
224
+ submit: async () => undefined,
225
+ }),
226
+ [],
227
+ );
228
+ }
229
+
230
+ /** `useMatch()` —— 没有路由,匹配不中,恒 `null`。 */
231
+ export function useMatch(_pattern?: unknown): null {
232
+ return null;
233
+ }
234
+
235
+ /** `useResolvedPath()` —— 返回自身路径的解析结果(不解析相对路径)。 */
236
+ export function useResolvedPath(to: string) {
237
+ return useMemo(() => parsePath(to), [to]);
238
+ }
239
+
240
+ /** `useRoutes()` —— 预览不渲染路由表,恒渲染 `null`。 */
241
+ export function useRoutes(_routes?: unknown): null {
242
+ return null;
243
+ }
244
+
245
+ /** `useOutlet()` —— 没有嵌套路由,恒 `null`。 */
246
+ export function useOutlet(_context?: unknown): null {
247
+ return null;
248
+ }
249
+
250
+ /** `useOutletContext()` —— 没有 Outlet 提供者,恒空对象。 */
251
+ export function useOutletContext<T = Record<string, unknown>>(): T {
252
+ return useMemo(() => ({}) as T, []);
253
+ }
254
+
255
+ /**
256
+ * `<Outlet />` —— 渲染 `null`。
257
+ *
258
+ * 子路由的内容由 Umi 的 layout / 约定式路由注入,预览不重建。
259
+ */
260
+ export function Outlet(): null {
261
+ return null;
262
+ }
263
+
264
+ /**
265
+ * `<Navigate />` —— 渲染 `null`,并同步内存 location。
266
+ *
267
+ * 真实行为会立刻跳转;预览里不真的导航(避免渲染期间改状态导致循环),
268
+ * 只把目标路径记进内存栈,让后续 `useLocation()` 能读到。
269
+ */
270
+ export function Navigate({ to }: { to: string }): null {
271
+ useEffect(() => {
272
+ navigate(to);
273
+ }, [to]);
274
+ return null;
275
+ }
276
+
277
+ /** 真实导出名是 `__useFetcher`,`useFetcher` 是它无下划线的别名,两个都要给。 */
278
+ export const __useFetcher = useFetcher;
279
+
280
+ /* ------------------------------------------------------------------ *
281
+ * react-router 的纯函数转导出
282
+ *
283
+ * 都是无状态的工具函数,可以给出**真实语义**(不需要路由上下文),
284
+ * 所以这里不降级为 `undefined` —— 降级反而会让组件的输入变成期望外的值。
285
+ * ------------------------------------------------------------------ */
286
+
287
+ /** `createSearchParams(init)` —— 与 react-router 一致,返回 `URLSearchParams`。 */
288
+ export function createSearchParams(
289
+ init?: string | Record<string, string> | Array<[string, string]> | URLSearchParams,
290
+ ): URLSearchParams {
291
+ return new URLSearchParams(init as string | Array<[string, string]> | Record<string, string> | undefined);
292
+ }
293
+
294
+ /**
295
+ * `resolvePath(to, fromPathname)` —— 相对路径解析成绝对路径。
296
+ *
297
+ * 移植自 react-router 6.3 的 `resolvePath` + `resolvePathname`:
298
+ * 处理 `.` / `..`、空的 `pathname`(回退到 `fromPathname`)与尾斜杠保留。
299
+ * 返回 `{ pathname, search, hash }`,**不含** `state`。
300
+ */
301
+ export function resolvePath(to: string, fromPathname = "/"): { pathname: string; search: string; hash: string } {
302
+ const parsed = parsePathForResolve(to);
303
+ const { search = "", hash = "" } = parsed;
304
+ // 空 `to`(或只有 search/hash)应该 **保留 `fromPathname`**,而不是回到根。
305
+ // `parsePath()` 把空串规整成了 `/`,所以这里要看原始 `to` 而不是解析结果。
306
+ const toPathname = to === "" || to.startsWith("?") || to.startsWith("#") ? "" : (to.split(/[?#]/)[0] ?? "");
307
+ const pathname = toPathname
308
+ ? toPathname.startsWith("/")
309
+ ? toPathname
310
+ : resolvePathname(toPathname, fromPathname)
311
+ : fromPathname;
312
+ return { pathname, search: normalizeSearch(search), hash: normalizeHash(hash) };
313
+ }
314
+
315
+ /** 本模块私有的 parsePath(不依赖 shim 的版本,避免互相 import)。 */
316
+ function parsePathForResolve(to: string): { pathname: string; search?: string; hash?: string } {
317
+ const [beforeHash, hash = ""] = to.split("#");
318
+ const [pathname, search = ""] = (beforeHash ?? "/").split("?");
319
+ return { pathname: pathname || "/", search, hash };
320
+ }
321
+
322
+ /** 相对路径拼接(与 react-router 的 `resolvePathname` 同算法)。 */
323
+ function resolvePathname(relativePath: string, fromPathname: string): string {
324
+ const segments = fromPathname.replace(/\/+$/, "").split("/");
325
+ for (const segment of relativePath.split("/")) {
326
+ if (segment === "..") {
327
+ // 保留根部的空段,让 pathname 仍以 / 开头
328
+ if (segments.length > 1) {
329
+ segments.pop();
330
+ }
331
+ } else if (segment !== ".") {
332
+ segments.push(segment);
333
+ }
334
+ }
335
+ return segments.length > 1 ? segments.join("/") : "/";
336
+ }
337
+
338
+ function normalizeSearch(search: string): string {
339
+ return !search || search === "?" ? "" : search.startsWith("?") ? search : `?${search}`;
340
+ }
341
+
342
+ function normalizeHash(hash: string): string {
343
+ return !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : `#${hash}`;
344
+ }
345
+
346
+ /**
347
+ * `matchPath(pattern, pathname)` —— 与 react-router 6.3 语义一致。
348
+ *
349
+ * ## 为什么不能用「按 `/` 切段逐个比」的简版
350
+ *
351
+ * 简版会错三处,而且错得很难发现:
352
+ *
353
+ * 1. **返回形状**:真实返回 `{ params, pathname, pathnameBase, pattern }`,
354
+ * 不是 `params` 本体。写 `match?.params.id` 的组件拿到的是 `undefined`。
355
+ * 2. **splat 捕获**:`/files/*` 的 `*` 是**具名捕获组**(`params["*"]`),
356
+ * 简版直接丢掉,组件会以为匹配上了但没有尾部路径。
357
+ * 3. **大小写**:默认 `caseSensitive: false`,`/Users/42` 能匹配 `/users/:id`。
358
+ *
359
+ * 所以这里直接移植 react-router 的 `compilePath` / `matchPath`(MIT,
360
+ * 见 `react-router@6.3.0` 的 `react-router.development.js`)。
361
+ */
362
+ export interface PathMatch {
363
+ params: Record<string, string>;
364
+ pathname: string;
365
+ pathnameBase: string;
366
+ pattern: { path: string; caseSensitive: boolean; end: boolean };
367
+ }
368
+
369
+ export function matchPath(
370
+ pattern: string | { path: string; caseSensitive?: boolean; end?: boolean },
371
+ pathname: string,
372
+ ): PathMatch | null {
373
+ const compiled =
374
+ typeof pattern === "string"
375
+ ? { path: pattern, caseSensitive: false, end: true }
376
+ : { path: pattern.path, caseSensitive: pattern.caseSensitive ?? false, end: pattern.end ?? true };
377
+
378
+ const [matcher, paramNames] = compilePath(compiled.path, compiled.caseSensitive, compiled.end);
379
+ const match = pathname.match(matcher);
380
+ if (!match) {
381
+ return null;
382
+ }
383
+ const matchedPathname = match[0] as string;
384
+ let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
385
+ const captureGroups = match.slice(1);
386
+ const params: Record<string, string> = {};
387
+
388
+ paramNames.forEach((paramName, index) => {
389
+ // splat 的 `pathnameBase` 要用**未解码**的原始值算,所以先算再解码。
390
+ if (paramName === "*") {
391
+ const splatValue = captureGroups[index] || "";
392
+ pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
393
+ }
394
+ params[paramName] = safelyDecodeURIComponent(captureGroups[index] || "", paramName);
395
+ });
396
+
397
+ return { params, pathname: matchedPathname, pathnameBase, pattern: compiled };
398
+ }
399
+
400
+ /** 把路径模式编译成正则 + 参数名列表(与 react-router 同):`^/users/([^\/]+)\/?$`。 */
401
+ function compilePath(path: string, caseSensitive = false, end = true): [RegExp, string[]] {
402
+ const paramNames: string[] = [];
403
+ let regexpSource = `^${path
404
+ .replace(/\/*\*?$/, "") // 尾部的 / 与 /* 由下面单独处理
405
+ .replace(/^\/*/, "/") // 保证以 / 开头
406
+ .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // 转义正则特殊字符
407
+ .replace(/:(\w+)/g, (_match, paramName: string) => {
408
+ paramNames.push(paramName);
409
+ return "([^\\/]+)";
410
+ })}`;
411
+
412
+ if (path.endsWith("*")) {
413
+ paramNames.push("*");
414
+ // `*` / `/*` 已经匹配了开头的 /,剩下的直接 capture;
415
+ // 否则不要把这个 / 算进 params["*"]。
416
+ regexpSource += path === "*" || path === "/*" ? "(.*)$" : "(?:\\/(.+)|\\/*)$";
417
+ } else if (end) {
418
+ regexpSource += "\\/*$"; // 匹配到末尾时忽略尾斜杠
419
+ } else {
420
+ regexpSource += "(?:(?=[.~-]|%[0-9A-F]{2})|\\b|\\/|$)";
421
+ }
422
+
423
+ return [new RegExp(regexpSource, caseSensitive ? undefined : "i"), paramNames];
424
+ }
425
+
426
+ /** 解码失败的 URL 段原样返回,不抛错(与 react-router 同:那里只打 warning)。 */
427
+ function safelyDecodeURIComponent(value: string, _paramName: string): string {
428
+ try {
429
+ return decodeURIComponent(value);
430
+ } catch {
431
+ return value;
432
+ }
433
+ }
434
+
435
+ /**
436
+ * `generatePath(pattern, params)` —— 把 `:param` 与尾部 `*` 替换成实际值。
437
+ *
438
+ * 移植自 react-router 6.3:**不做 URL 编码**(中文等原样输出),
439
+ * 缺失参数抛错,`*` 允许为空。
440
+ */
441
+ export function generatePath(pattern: string, params: Record<string, string> = {}): string {
442
+ return pattern
443
+ .replace(/:(\w+)/g, (_match, key: string) => {
444
+ const value = params[key];
445
+ // 真实实现用 `params[key] != null`(同时排除 null 与 undefined),
446
+ // 这里展开写,避免依赖宽松相等。
447
+ if (value === undefined || value === null) {
448
+ throw new Error(`Missing ":${key}" param`);
449
+ }
450
+ return value;
451
+ })
452
+ .replace(/\/*\*$/, () => {
453
+ const splat = params["*"];
454
+ return splat === undefined || splat === null ? "" : String(splat).replace(/^\/*/, "/");
455
+ });
456
+ }
457
+
458
+ /** `matchRoutes(routes, pathname)` —— 没有路由表时恒 `null`。 */ export function matchRoutes(
459
+ _routes?: unknown,
460
+ _pathname?: string,
461
+ ): null {
462
+ return null;
463
+ }
464
+
465
+ /**
466
+ * `withRouter()` —— 直通 HOC。
467
+ *
468
+ * 真实版本注入 `history` / `location` / `match`;预览没有路由上下文,
469
+ * 注入空值(与 `connect` 的策略一致)。
470
+ */
471
+ export function withRouter<T>(Component: T): T {
472
+ return Component;
473
+ }
474
+
475
+ /* ------------------------------------------------------------------ *
476
+ * helmet / history 转导出
477
+ * ------------------------------------------------------------------ */
478
+
479
+ /**
480
+ * `<Helmet>` / `<HelmetProvider>` —— 渲染子节点,不改文档头。
481
+ *
482
+ * 预览的 `document.title` 由面板控制,让组件随便改会与宿主抢控制权。
483
+ */
484
+ export function Helmet({ children }: { children?: ReactNode }): ReactNode {
485
+ return children ?? null;
486
+ }
487
+
488
+ export function HelmetProvider({ children }: { children?: ReactNode }): ReactNode {
489
+ return children ?? null;
490
+ }
491
+
492
+ /**
493
+ * `createMemoryHistory()` —— 返回与 shim 的 `history` 同构的内存栈。
494
+ *
495
+ * 注意:真实 `history` 包的接口大得多(`block` / `go` / `createHref` 等)。
496
+ * 这里只提供**渲染路径上常见**的几个,未实现的显式抛错(与 `request` 同策略)。
497
+ */
498
+ export function createMemoryHistory(_options?: unknown) {
499
+ return history;
500
+ }
501
+
502
+ export const createBrowserHistory = createMemoryHistory;
503
+ export const createHashHistory = createMemoryHistory;
504
+
505
+ /** `LinkWithPrefetch as Link` —— 真实导出名是无下划线的 prefetch 版,预览里与 `Link` 同构。 */
506
+ export const LinkWithPrefetch = Link;
507
+
508
+ /* ------------------------------------------------------------------ *
509
+ * 应用挂载入口
510
+ *
511
+ * 这两个是 **应用级** API(把整棵路由树挂到 DOM),组件预览永远不会调用。
512
+ * 仍然显式导出:ESM 是**静态**导出校验,少一个名字整个模块就 `SyntaxError`,
513
+ * 整个预览白屏 —— 而降级为抛错至少能给出可读的原因。
514
+ * ------------------------------------------------------------------ */
515
+
516
+ /** `renderClient()` —— 应用级挂载,不在组件预览的职责内。 */
517
+ export function renderClient(): never {
518
+ throw new Error("预览环境不提供 umi.renderClient —— 应用级渲染请用 DevBrowser(应用预览)");
519
+ }
520
+
521
+ /** `__getRoot()` —— 拿到应用根元素,同上。 */
522
+ export function __getRoot(): never {
523
+ throw new Error("预览环境不提供 umi.__getRoot —— 应用级渲染请用 DevBrowser(应用预览)");
524
+ }
525
+
526
+ /**
527
+ * `useRequest` —— **返回空态而非抛错**。
528
+ *
529
+ * 理由(spec §4.2.1):它是 `@umijs/max` 下最高频的 API,抛错会让几乎所有页面
530
+ * 组件在预览里直接崩。空态能让组件的 `if (!data) return null` 分支自然生效。
531
+ *
532
+ * 选择 `loading: false` 而非 `true`:`true` 会让组件永远停在骨架屏,
533
+ * 而预览的目的是看结构,不是看 loading。
534
+ *
535
+ * 注意:**不伪造数据**,`data` 恒为 `undefined`。
536
+ */
537
+ let warnedUseRequestEmpty = false;
538
+
539
+ export function useRequest<R = unknown>(_service: unknown, _options?: unknown) {
540
+ if (!warnedUseRequestEmpty && typeof console !== "undefined") {
541
+ // 只打一次:每次渲染都打会把控制台刷爆。
542
+ // 让"为什么没数据"可被发现,而不是让人误以为是 bug(spec §11 R6)。
543
+ warnedUseRequestEmpty = true;
544
+ console.debug("[cfp-preview] useRequest:预览环境返回空态");
545
+ }
546
+ return {
547
+ data: undefined as R | undefined,
548
+ error: undefined,
549
+ loading: false,
550
+ run: async () => undefined as R | undefined,
551
+ runAsync: async () => undefined as R | undefined,
552
+ refresh: async () => undefined as R | undefined,
553
+ refreshAsync: async () => undefined as R | undefined,
554
+ mutate: () => undefined,
555
+ cancel: () => undefined,
556
+ };
557
+ }
558
+
559
+ /** 其它未实现的 API:显式抛错,不做静默 `undefined`。 */
560
+ export function request(): never {
561
+ throw new Error("预览环境未实现 umi.request —— 请求类逻辑请在 DevBrowser(应用预览)里验证");
562
+ }
563
+
564
+ export function useAccess(): Record<string, boolean> {
565
+ return {};
566
+ }
567
+
568
+ export function useIntl() {
569
+ return {
570
+ formatMessage: ({ id }: { id: string }) => id,
571
+ };
572
+ }
573
+
574
+ export function getLocale(): string {
575
+ return "zh-CN";
576
+ }
577
+
578
+ export function setLocale(): void {
579
+ // 预览里切语言不生效:Umi 的 locale 由运行时注入,shim 不做模拟。
580
+ }
@@ -1,9 +1,9 @@
1
1
  {
2
- "extends": "../../../../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "module": "ES2022",
5
- "moduleResolution": "Bundler",
6
- "jsx": "preserve"
7
- },
8
- "exclude": ["types"]
2
+ "extends": "../../../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "ES2022",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "preserve"
7
+ },
8
+ "exclude": ["types"]
9
9
  }