@dev-to/react-loader 0.1.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/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @dev-to/react-loader
2
+
3
+ 在宿主页面中动态加载 **Vite Dev Server** 上的 React 组件,并保持 React Fast Refresh/HMR 能力。
4
+
5
+ 该包默认与 `@dev-to/react-plugin`(Vite 侧)配套使用。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pnpm add @dev-to/react-loader
11
+ ```
12
+
13
+ ## 前置条件(Vite 侧)
14
+
15
+ 确保你的 Vite 项目启用了 `@dev-to/react-plugin`:
16
+
17
+ ```ts
18
+ import { devToReactPlugin } from '@dev-to/react-plugin'
19
+
20
+ devToReactPlugin({ MyCard: 'src/MyCard.tsx' })
21
+ ```
22
+
23
+ ## 使用
24
+
25
+ ```tsx
26
+ import { ReactLoader } from '@dev-to/react-loader'
27
+
28
+ export function Demo() {
29
+ return (
30
+ <ReactLoader
31
+ origin="http://localhost:5173"
32
+ name="MyCard"
33
+ componentProps={{ title: 'Hello' }}
34
+ />
35
+ )
36
+ }
37
+ ```
38
+
39
+ ### 直接 URL 模式
40
+
41
+ 如果你已经拿到完整入口 URL,也可以绕过内部解析:
42
+
43
+ ```tsx
44
+ <ReactLoader url="http://localhost:5173/@fs/abs/path/to/MyCard.tsx" componentProps={{}} />
45
+ ```
46
+
47
+ ## 默认桥接端点
48
+
49
+ - Contract:`/__dev_to_react__/contract.js`
50
+ - Init:`/__dev_to_react__/init.js`
51
+
52
+ 这些端点常量由 `@dev-to/react-shared` 统一定义,保证与 `@dev-to/react-plugin` 保持一致。
53
+
54
+ 可通过 `contractEndpoint` 自定义 contract 路径;init 路径由 contract 返回值决定(未提供时回退到默认值)。
55
+
56
+ ## 导出内容
57
+
58
+ - React 组件:`ReactLoader`
59
+ - 底层能力:`loadBridgeContract` / `ensureBridgeInit` / `resolveReactEntry` / `resolveReactEntryForLoader`
@@ -0,0 +1,80 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type DevToReactBridgeContract } from '@dev-to/react-shared';
3
+ /**
4
+ * --- DevTo React Bridge Protocol Types & Utils ---
5
+ */
6
+ export declare const DEFAULT_CONTRACT_ENDPOINT: "/__dev_to_react__/contract.js";
7
+ export declare const DEFAULT_INIT_ENDPOINT: "/__dev_to_react__/init.js";
8
+ /** 加载远程 dev server 的桥接合约(无副作用) */
9
+ export declare function loadBridgeContract(origin: string, contractEndpoint?: string): Promise<DevToReactBridgeContract>;
10
+ /** 初始化远程 dev server 的 HMR/Refresh 运行时 */
11
+ export declare function ensureBridgeInit(origin: string, contract: DevToReactBridgeContract): Promise<void>;
12
+ /**
13
+ * --- Entry Resolution Logic (Core Engine) ---
14
+ */
15
+ export interface ReactEntryResolutionResult {
16
+ success: true;
17
+ entryUrl: string;
18
+ }
19
+ export interface ReactEntryResolutionError {
20
+ success: false;
21
+ error: {
22
+ message: string;
23
+ type: 'COMPONENT_NOT_FOUND' | 'CONTRACT_LOAD_FAILED';
24
+ componentName?: string;
25
+ errorReason?: string;
26
+ setupGuide?: {
27
+ title: string;
28
+ steps: string[];
29
+ };
30
+ };
31
+ }
32
+ export type ReactEntryResolution = ReactEntryResolutionResult | ReactEntryResolutionError;
33
+ /**
34
+ * Core engine: Resolve dev server entry URL for a given component name from contract.
35
+ * This is business-agnostic and only handles the matching logic.
36
+ */
37
+ export declare function resolveReactEntry(origin: string, componentName: string, contractEndpoint?: string): Promise<ReactEntryResolution>;
38
+ /**
39
+ * Engine helper for host/business layer:
40
+ * Resolve entry and convert failures into a loader-friendly error object.
41
+ */
42
+ export declare function resolveReactEntryForLoader(origin: string, componentName: string, contractEndpoint?: string): Promise<{
43
+ success: true;
44
+ entryUrl: string;
45
+ } | {
46
+ success: false;
47
+ loaderError: ReactLoaderError;
48
+ }>;
49
+ export interface ReactLoaderError {
50
+ message: string;
51
+ origin?: string;
52
+ componentName?: string;
53
+ type?: string;
54
+ errorReason?: string;
55
+ statusCode?: number | string;
56
+ setupGuide?: {
57
+ title: string;
58
+ steps: string[];
59
+ };
60
+ }
61
+ export interface ReactLoaderProps<P extends Record<string, unknown> = Record<string, unknown>> {
62
+ /** Dev server origin (e.g., http://localhost:5173). Required if url is not provided. */
63
+ origin?: string;
64
+ /** Component name to resolve. Required if url is not provided. */
65
+ name?: string;
66
+ /** Direct entry URL (bypass internal resolution) */
67
+ url?: string;
68
+ contractEndpoint?: string;
69
+ componentProps: P;
70
+ loading?: ReactNode;
71
+ /** @deprecated Resolution error is now handled internally via origin/name props */
72
+ externalError?: ReactLoaderError | null;
73
+ /** 内部预留:允许覆盖默认错误渲染 */
74
+ renderError?: (error: Error | ReactLoaderError) => ReactNode;
75
+ }
76
+ /**
77
+ * React 开发态 ESM 组件加载器(宿主内联复用)
78
+ */
79
+ export declare function ReactLoader<P extends Record<string, unknown> = Record<string, unknown>>(props: ReactLoaderProps<P>): import("react/jsx-runtime").JSX.Element;
80
+ //# sourceMappingURL=ReactLoader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReactLoader.d.ts","sourceRoot":"","sources":["../src/ReactLoader.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { DEFAULT_CONTRACT_ENDPOINT, DEFAULT_INIT_ENDPOINT, ReactLoader, ensureBridgeInit, loadBridgeContract, resolveReactEntry, resolveReactEntryForLoader, } from './ReactLoader.js';
2
+ export type { ReactEntryResolution, ReactEntryResolutionError, ReactEntryResolutionResult, ReactLoaderError, ReactLoaderProps, } from './ReactLoader.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,0BAA0B,GAC3B,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EACV,oBAAoB,EACpB,yBAAyB,EACzB,0BAA0B,EAC1B,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,kBAAkB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,860 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { DEV_TO_REACT_BASE_PATH, DEV_TO_REACT_CONTRACT_PATH, DEV_TO_REACT_DEBUG_HTML_PATH, DEV_TO_REACT_INIT_PATH, DEV_TO_REACT_NAMESPACE } from "@dev-to/react-shared";
4
+ function isRecord(value) {
5
+ return 'object' == typeof value && null !== value;
6
+ }
7
+ function unwrapDefault(value) {
8
+ if (!isRecord(value)) return value;
9
+ const defaultValue = value['default'];
10
+ return null != defaultValue ? defaultValue : value;
11
+ }
12
+ function toError(value) {
13
+ return value instanceof Error ? value : new Error(String(value));
14
+ }
15
+ function resolveRemoteRuntime(runtimeModule) {
16
+ const runtimeRecord = isRecord(runtimeModule) ? runtimeModule : {};
17
+ const reactCandidate = runtimeRecord['default'] ?? runtimeRecord['React'] ?? runtimeModule;
18
+ const domCandidate = runtimeRecord['ReactDOMClient'];
19
+ if (!isRecord(reactCandidate) || 'function' != typeof reactCandidate['createElement'] || !isRecord(domCandidate) || 'function' != typeof domCandidate['createRoot']) throw new Error('Invalid React runtime module from dev server.');
20
+ return {
21
+ React: reactCandidate,
22
+ ReactDOMClient: domCandidate
23
+ };
24
+ }
25
+ const DEFAULT_CONTRACT_ENDPOINT = DEV_TO_REACT_CONTRACT_PATH;
26
+ const DEFAULT_INIT_ENDPOINT = DEV_TO_REACT_INIT_PATH;
27
+ function resolveEndpointUrl(origin, endpoint) {
28
+ if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) return endpoint;
29
+ if (endpoint.startsWith('/')) return `${origin}${endpoint}`;
30
+ return `${origin}/${endpoint}`;
31
+ }
32
+ function isBridgeContract(value) {
33
+ if (!isRecord(value)) return false;
34
+ const paths = value['paths'];
35
+ const events = value['events'];
36
+ if (!isRecord(paths) || 'string' != typeof paths['reactRuntime']) return false;
37
+ if (!isRecord(events) || 'string' != typeof events['fullReload']) return false;
38
+ return true;
39
+ }
40
+ function resolveContract(initModule) {
41
+ const moduleRecord = isRecord(initModule) ? initModule : {};
42
+ const contractCandidate = moduleRecord['DEV_TO_REACT_CONTRACT'] ?? moduleRecord['default'] ?? null;
43
+ if (!contractCandidate) throw new Error('Dev server contract not found. Please ensure `@dev-to/react-plugin` (devToReactPlugin) is enabled in the Vite dev server.');
44
+ if (!isBridgeContract(contractCandidate)) throw new Error('Invalid dev server contract.');
45
+ return contractCandidate;
46
+ }
47
+ const nativeImport = new Function('url', 'return import(url)');
48
+ const contractCache = new Map();
49
+ const initCache = new Map();
50
+ async function loadBridgeContract(origin, contractEndpoint) {
51
+ const endpoint = contractEndpoint || DEFAULT_CONTRACT_ENDPOINT;
52
+ const initUrl = resolveEndpointUrl(origin, endpoint);
53
+ if (contractCache.has(initUrl)) return contractCache.get(initUrl);
54
+ const p = nativeImport(initUrl).then((m)=>resolveContract(m)).catch(async (e)=>{
55
+ contractCache.delete(initUrl);
56
+ const err = e instanceof Error ? e : new Error(String(e));
57
+ if (err.message.includes('Failed to fetch')) try {
58
+ const resp = await fetch(initUrl, {
59
+ method: 'HEAD'
60
+ }).catch(()=>null);
61
+ if (resp) err.statusCode = resp.status;
62
+ } catch {}
63
+ throw err;
64
+ });
65
+ contractCache.set(initUrl, p);
66
+ return p;
67
+ }
68
+ async function ensureBridgeInit(origin, contract) {
69
+ const initPath = contract?.paths?.initClient || DEFAULT_INIT_ENDPOINT;
70
+ const initUrl = resolveEndpointUrl(origin, initPath);
71
+ if (initCache.has(initUrl)) return initCache.get(initUrl);
72
+ const p = nativeImport(initUrl).then(()=>void 0).catch((e)=>{
73
+ initCache.delete(initUrl);
74
+ throw e instanceof Error ? e : new Error(String(e));
75
+ });
76
+ initCache.set(initUrl, p);
77
+ return p;
78
+ }
79
+ async function resolveReactEntry(origin, componentName, contractEndpoint) {
80
+ try {
81
+ const contract = await loadBridgeContract(origin, contractEndpoint);
82
+ const dev = contract?.dev || {};
83
+ const componentMap = dev.componentMap || {};
84
+ const entry = componentMap[componentName] || componentMap['*'];
85
+ if (!entry) {
86
+ const errorMessage = `Component "${componentName}" not found in devComponentMap`;
87
+ const isWildcardMode = !!componentMap['*'];
88
+ const errorReason = isWildcardMode ? `The component "${componentName}" is not configured, and the wildcard fallback is also missing or invalid.` : `The component "${componentName}" is not configured in devComponentMap. Please add it to your Vite configuration.`;
89
+ return {
90
+ success: false,
91
+ error: {
92
+ message: errorMessage,
93
+ type: 'COMPONENT_NOT_FOUND',
94
+ componentName,
95
+ errorReason,
96
+ setupGuide: {
97
+ title: 'Setup Guide',
98
+ steps: [
99
+ "import { devToReactPlugin } from '@dev-to/react-plugin'",
100
+ '',
101
+ "// Option 1: Shorthand (Default)",
102
+ `devToReactPlugin('${componentName}')`,
103
+ '',
104
+ "// Option 2: Explicit Mapping",
105
+ "devToReactPlugin({",
106
+ ` '${componentName}': '/', // Default entry`,
107
+ " 'Other': 'src/Card.tsx' // Specific file",
108
+ "})"
109
+ ]
110
+ }
111
+ }
112
+ };
113
+ }
114
+ const entryUrl = entry.startsWith('http://') || entry.startsWith('https://') ? entry : `${origin}${entry.startsWith('/') ? '' : '/'}${entry}`;
115
+ return {
116
+ success: true,
117
+ entryUrl
118
+ };
119
+ } catch (e) {
120
+ const errorMessage = e instanceof Error ? e.message : String(e);
121
+ return {
122
+ success: false,
123
+ error: {
124
+ message: errorMessage,
125
+ type: 'CONTRACT_LOAD_FAILED'
126
+ }
127
+ };
128
+ }
129
+ }
130
+ async function resolveReactEntryForLoader(origin, componentName, contractEndpoint) {
131
+ const r = await resolveReactEntry(origin, componentName, contractEndpoint);
132
+ if (r.success) return r;
133
+ const e = r.error;
134
+ return {
135
+ success: false,
136
+ loaderError: {
137
+ message: e.message,
138
+ origin,
139
+ componentName: e.componentName || componentName,
140
+ type: e.type,
141
+ errorReason: e.errorReason,
142
+ setupGuide: e.setupGuide
143
+ }
144
+ };
145
+ }
146
+ function safeLocationHref() {
147
+ if ("u" < typeof window) return '';
148
+ try {
149
+ return window.location.href;
150
+ } catch {
151
+ return '';
152
+ }
153
+ }
154
+ function safeOrigin() {
155
+ if ("u" < typeof window) return '';
156
+ try {
157
+ return window.location.origin;
158
+ } catch {
159
+ return '';
160
+ }
161
+ }
162
+ function getOriginFromFinalJsUrl(finalJsUrl) {
163
+ let origin = '';
164
+ try {
165
+ const urlParam = finalJsUrl.match(/url=([^&]+)/)?.[1] || '';
166
+ const decoded = urlParam ? decodeURIComponent(urlParam) : finalJsUrl;
167
+ const parsed = decoded.startsWith('http') ? new URL(decoded) : new URL(decoded, safeLocationHref() || 'http://localhost');
168
+ origin = parsed.origin;
169
+ } catch {
170
+ origin = safeOrigin();
171
+ }
172
+ return origin;
173
+ }
174
+ function useReactLoader(options) {
175
+ const { url: directUrl, origin, name, componentProps, contractEndpoint } = options;
176
+ const containerRef = useRef(null);
177
+ const rootRef = useRef(null);
178
+ const runtimeRef = useRef(null);
179
+ const cardRef = useRef(null);
180
+ const fullReloadListenerRef = useRef(null);
181
+ const propsRef = useRef(componentProps);
182
+ propsRef.current = componentProps;
183
+ const [isReady, setIsReady] = useState(false);
184
+ const [error, setError] = useState(null);
185
+ const [version, setVersion] = useState(0);
186
+ const [resolvedUrl, setResolvedUrl] = useState(directUrl);
187
+ useEffect(()=>{
188
+ if (directUrl) {
189
+ setResolvedUrl(directUrl);
190
+ setError(null);
191
+ return;
192
+ }
193
+ if (!origin || !name) {
194
+ if (!directUrl && (origin || name)) setError(new Error('Missing dev server origin or component name for resolution.'));
195
+ return;
196
+ }
197
+ let cancelled = false;
198
+ setIsReady(false);
199
+ setError(null);
200
+ resolveReactEntryForLoader(origin, name, contractEndpoint).then((res)=>{
201
+ if (cancelled) return;
202
+ if (true === res.success) setResolvedUrl(res.entryUrl);
203
+ else setError(res.loaderError);
204
+ });
205
+ return ()=>{
206
+ cancelled = true;
207
+ };
208
+ }, [
209
+ directUrl,
210
+ origin,
211
+ name,
212
+ contractEndpoint
213
+ ]);
214
+ const ensureFullReloadListener = useCallback((eventName)=>{
215
+ if (!eventName) return;
216
+ if ("u" < typeof window) return;
217
+ const { current } = fullReloadListenerRef;
218
+ if (current?.eventName === eventName) return;
219
+ if (current) window.removeEventListener(current.eventName, current.handler);
220
+ const handler = ()=>{
221
+ setVersion((v)=>v + 1);
222
+ };
223
+ window.addEventListener(eventName, handler);
224
+ fullReloadListenerRef.current = {
225
+ eventName,
226
+ handler
227
+ };
228
+ }, []);
229
+ useEffect(()=>()=>{
230
+ if ("u" < typeof window) return;
231
+ const { current } = fullReloadListenerRef;
232
+ if (current) window.removeEventListener(current.eventName, current.handler);
233
+ }, []);
234
+ const renderViteRoot = useCallback(()=>{
235
+ const runtime = runtimeRef.current;
236
+ const Card = cardRef.current;
237
+ if (!runtime || !Card) return;
238
+ if (!containerRef.current) return;
239
+ if (!rootRef.current) rootRef.current = runtime.ReactDOMClient.createRoot(containerRef.current);
240
+ rootRef.current.render(runtime.React.createElement(Card, propsRef.current));
241
+ }, []);
242
+ const loadViteComponent = useCallback(async (entryUrl, exportName, currentVersion)=>{
243
+ if (!entryUrl) throw new Error('Not found entry url.');
244
+ const originFromUrl = getOriginFromFinalJsUrl(entryUrl);
245
+ const connector = entryUrl.includes('?') ? '&' : '?';
246
+ const jsUrlWithParam = currentVersion > 0 ? `${entryUrl}${connector}v=${currentVersion}` : entryUrl;
247
+ try {
248
+ const contract = await loadBridgeContract(originFromUrl, contractEndpoint);
249
+ await ensureBridgeInit(originFromUrl, contract);
250
+ ensureFullReloadListener(contract.events.fullReload);
251
+ const runtimeModule = await nativeImport(`${originFromUrl}${contract.paths.reactRuntime}`);
252
+ const runtime = resolveRemoteRuntime(runtimeModule);
253
+ const moduleNs = await nativeImport(jsUrlWithParam);
254
+ const moduleRecord = isRecord(moduleNs) ? moduleNs : {};
255
+ const exportCandidate = exportName ? moduleRecord[exportName] : void 0;
256
+ const candidate = exportName ? exportCandidate ?? moduleRecord['default'] ?? moduleNs : moduleRecord['default'] ?? moduleNs;
257
+ const Card = unwrapDefault(candidate);
258
+ if (Card) return {
259
+ Card: Card,
260
+ runtime
261
+ };
262
+ throw new Error('Vite Dev Component Load Fail: Component not found in module.');
263
+ } catch (e) {
264
+ const err = toError(e);
265
+ if (err.message.includes('Failed to fetch')) try {
266
+ const resp = await fetch(jsUrlWithParam, {
267
+ method: 'HEAD'
268
+ }).catch(()=>null);
269
+ if (resp) err.statusCode = resp.status;
270
+ } catch {}
271
+ throw err;
272
+ }
273
+ }, [
274
+ contractEndpoint,
275
+ ensureFullReloadListener
276
+ ]);
277
+ useEffect(()=>{
278
+ if (!resolvedUrl) return;
279
+ let cancelled = false;
280
+ setIsReady(false);
281
+ if (error instanceof Error) setError(null);
282
+ loadViteComponent(resolvedUrl, name, version).then(({ Card, runtime })=>{
283
+ if (cancelled) return;
284
+ runtimeRef.current = runtime;
285
+ cardRef.current = Card;
286
+ setIsReady(true);
287
+ renderViteRoot();
288
+ }).catch((err)=>{
289
+ if (cancelled) return;
290
+ setError(err instanceof Error ? err : new Error(String(err)));
291
+ setIsReady(false);
292
+ });
293
+ return ()=>{
294
+ cancelled = true;
295
+ };
296
+ }, [
297
+ loadViteComponent,
298
+ renderViteRoot,
299
+ resolvedUrl,
300
+ name,
301
+ version,
302
+ error
303
+ ]);
304
+ useEffect(()=>{
305
+ if (isReady) renderViteRoot();
306
+ });
307
+ useEffect(()=>()=>{
308
+ try {
309
+ rootRef.current?.unmount?.();
310
+ } catch {}
311
+ rootRef.current = null;
312
+ }, []);
313
+ useEffect(()=>{
314
+ if (error) {
315
+ try {
316
+ rootRef.current?.unmount?.();
317
+ } catch {}
318
+ rootRef.current = null;
319
+ }
320
+ }, [
321
+ error
322
+ ]);
323
+ return {
324
+ containerRef,
325
+ isReady,
326
+ error,
327
+ resolvedUrl
328
+ };
329
+ }
330
+ function InlineViteErrorView(props) {
331
+ const { errorMessage, currentOrigin, componentName, setupGuide, statusCode } = props;
332
+ const isComponentNotFound = errorMessage.includes('not found in devComponentMap');
333
+ const isFetchError = errorMessage.includes('Failed to fetch dynamically imported module');
334
+ const isInternalBridgeFile = errorMessage.includes(DEV_TO_REACT_BASE_PATH) || errorMessage.includes(DEV_TO_REACT_NAMESPACE);
335
+ const isContractError = errorMessage.includes('Vite dev contract not found') || errorMessage.includes('Failed to load Vite bridge contract') || errorMessage.includes('Failed to load contract from') || isFetchError && isInternalBridgeFile;
336
+ const isModuleFetchError = isFetchError && !isInternalBridgeFile;
337
+ let title = 'Vite Server Error';
338
+ let typeLabel = statusCode?.toString() || '500';
339
+ if (isComponentNotFound) {
340
+ title = 'Component Not Configured';
341
+ typeLabel = '412';
342
+ } else if (isModuleFetchError) {
343
+ title = 'Vite Module Not Found';
344
+ typeLabel = statusCode?.toString() || '404';
345
+ } else if (isContractError) {
346
+ title = 'Vite Server Connection Failed';
347
+ typeLabel = statusCode?.toString() || '503';
348
+ }
349
+ const theme = isContractError ? 'urgent' : 'warning';
350
+ const palette = 'urgent' === theme ? {
351
+ bg: 'linear-gradient(135deg, #fef2f2 0%, #fff5f5 100%)',
352
+ border: '#fecaca',
353
+ title: '#dc2626',
354
+ accent: '#dc2626',
355
+ pillBg: 'rgba(220, 38, 38, 0.05)',
356
+ pillBorder: 'rgba(220, 38, 38, 0.2)',
357
+ tipsBg: '#fffbeb',
358
+ tipsBorder: '#fde68a',
359
+ tipsText: '#92400e'
360
+ } : {
361
+ bg: 'linear-gradient(135deg, #fffbeb 0%, #fffdf2 100%)',
362
+ border: '#fde68a',
363
+ title: '#d97706',
364
+ accent: '#d97706',
365
+ pillBg: 'rgba(217, 119, 6, 0.05)',
366
+ pillBorder: 'rgba(217, 119, 6, 0.2)',
367
+ tipsBg: '#fffbeb',
368
+ tipsBorder: '#fde68a',
369
+ tipsText: '#92400e'
370
+ };
371
+ const styles = {
372
+ container: {
373
+ padding: 12,
374
+ borderRadius: 8,
375
+ background: palette.bg,
376
+ border: `1px solid ${palette.border}`,
377
+ boxShadow: '0 1px 4px rgba(0,0,0,0.06)'
378
+ },
379
+ header: {
380
+ display: 'flex',
381
+ alignItems: 'center',
382
+ gap: 8,
383
+ marginBottom: 10,
384
+ paddingBottom: 8,
385
+ borderBottom: `1px solid ${palette.border}66`
386
+ },
387
+ icon: {
388
+ fontSize: 18,
389
+ fontFamily: '"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif'
390
+ },
391
+ title: {
392
+ fontSize: 12,
393
+ fontWeight: 700,
394
+ color: palette.title,
395
+ display: 'flex',
396
+ alignItems: 'center',
397
+ gap: 6
398
+ },
399
+ typeTag: {
400
+ fontSize: 11,
401
+ fontWeight: 700,
402
+ padding: '1px 6px',
403
+ borderRadius: 999,
404
+ border: `1px solid ${palette.pillBorder}`,
405
+ background: palette.pillBg,
406
+ color: palette.accent,
407
+ lineHeight: 1
408
+ },
409
+ content: {
410
+ display: 'flex',
411
+ flexDirection: 'column',
412
+ gap: 8
413
+ },
414
+ message: {
415
+ fontSize: 9,
416
+ color: '#991b1b',
417
+ background: '#fee2e2',
418
+ padding: 5,
419
+ borderRadius: 6,
420
+ borderLeft: `1px solid ${palette.accent}`,
421
+ whiteSpace: 'pre-wrap',
422
+ fontFamily: 'monospace',
423
+ wordBreak: 'break-word'
424
+ },
425
+ info: {
426
+ background: '#fff',
427
+ padding: '8px 10px',
428
+ borderRadius: 6,
429
+ border: `1px solid ${palette.border}`,
430
+ display: 'flex',
431
+ flexDirection: 'column',
432
+ gap: 6
433
+ },
434
+ infoRow: {
435
+ display: 'flex',
436
+ gap: 8,
437
+ alignItems: 'center'
438
+ },
439
+ label: {
440
+ fontSize: 11,
441
+ color: '#64748b',
442
+ fontWeight: 600,
443
+ width: 110,
444
+ textAlign: 'right'
445
+ },
446
+ value: {
447
+ fontSize: 11,
448
+ color: '#1e293b',
449
+ background: '#f8fafc',
450
+ padding: '3px 6px',
451
+ borderRadius: 4,
452
+ fontFamily: 'monospace',
453
+ border: '1px solid #e2e8f0',
454
+ wordBreak: 'break-word',
455
+ flex: 1
456
+ },
457
+ tips: {
458
+ background: palette.tipsBg,
459
+ padding: '8px 10px',
460
+ borderRadius: 6,
461
+ border: `1px solid ${palette.tipsBorder}`
462
+ },
463
+ tipsTitle: {
464
+ fontSize: 12,
465
+ fontWeight: 800,
466
+ color: palette.tipsText,
467
+ marginBottom: 6,
468
+ textAlign: 'center'
469
+ },
470
+ list: {
471
+ padding: 0,
472
+ listStyle: 'none',
473
+ display: 'flex',
474
+ flexDirection: 'column',
475
+ gap: 6
476
+ },
477
+ li: {
478
+ fontSize: 11,
479
+ color: '#78350f'
480
+ },
481
+ code: {
482
+ fontFamily: 'monospace',
483
+ fontSize: 10
484
+ },
485
+ link: {
486
+ color: '#2563eb'
487
+ },
488
+ codeBlock: {
489
+ padding: '10px 12px',
490
+ background: '#1e293b',
491
+ borderRadius: 6,
492
+ border: '1px solid #334155',
493
+ overflowX: 'auto',
494
+ fontSize: 11,
495
+ color: '#e2e8f0',
496
+ fontFamily: 'monospace'
497
+ },
498
+ codeComment: {
499
+ color: '#94a3b8',
500
+ fontStyle: 'italic'
501
+ }
502
+ };
503
+ const debugPanelUrl = `${currentOrigin}${DEV_TO_REACT_DEBUG_HTML_PATH}`;
504
+ const componentNameLabel = componentName || '';
505
+ return /*#__PURE__*/ jsxs("div", {
506
+ className: "vdev-container",
507
+ style: styles.container,
508
+ children: [
509
+ /*#__PURE__*/ jsxs("div", {
510
+ className: "vdev-header",
511
+ style: styles.header,
512
+ children: [
513
+ /*#__PURE__*/ jsx("div", {
514
+ className: "vdev-icon",
515
+ style: styles.icon,
516
+ children: '\u26A0\uFE0F'
517
+ }),
518
+ /*#__PURE__*/ jsxs("div", {
519
+ className: "vdev-title",
520
+ style: styles.title,
521
+ children: [
522
+ /*#__PURE__*/ jsx("span", {
523
+ className: "vdev-type-tag",
524
+ style: styles.typeTag,
525
+ children: typeLabel
526
+ }),
527
+ title
528
+ ]
529
+ })
530
+ ]
531
+ }),
532
+ /*#__PURE__*/ jsxs("div", {
533
+ className: "vdev-content",
534
+ style: styles.content,
535
+ children: [
536
+ /*#__PURE__*/ jsx("div", {
537
+ className: "vdev-message",
538
+ style: styles.message,
539
+ children: errorMessage
540
+ }),
541
+ /*#__PURE__*/ jsxs("div", {
542
+ className: "vdev-info",
543
+ style: styles.info,
544
+ children: [
545
+ /*#__PURE__*/ jsxs("div", {
546
+ className: "vdev-info-row",
547
+ style: styles.infoRow,
548
+ children: [
549
+ /*#__PURE__*/ jsx("span", {
550
+ className: "vdev-label",
551
+ style: styles.label,
552
+ children: "Server Address:"
553
+ }),
554
+ /*#__PURE__*/ jsx("code", {
555
+ className: "vdev-value",
556
+ style: styles.value,
557
+ children: currentOrigin
558
+ })
559
+ ]
560
+ }),
561
+ componentNameLabel ? /*#__PURE__*/ jsxs("div", {
562
+ className: "vdev-info-row",
563
+ style: styles.infoRow,
564
+ children: [
565
+ /*#__PURE__*/ jsx("span", {
566
+ className: "vdev-label",
567
+ style: styles.label,
568
+ children: "Component Name:"
569
+ }),
570
+ /*#__PURE__*/ jsx("code", {
571
+ className: "vdev-value",
572
+ style: styles.value,
573
+ children: componentNameLabel
574
+ })
575
+ ]
576
+ }) : null
577
+ ]
578
+ }),
579
+ /*#__PURE__*/ jsxs("div", {
580
+ className: "vdev-tips",
581
+ style: styles.tips,
582
+ children: [
583
+ /*#__PURE__*/ jsx("div", {
584
+ className: "vdev-tips-title",
585
+ style: styles.tipsTitle,
586
+ children: "Next Steps:"
587
+ }),
588
+ setupGuide ? /*#__PURE__*/ jsx("pre", {
589
+ className: "vdev-code-block",
590
+ style: styles.codeBlock,
591
+ children: setupGuide.steps.map((step, idx)=>{
592
+ const stepKey = `step-${idx}`;
593
+ const commentIdx = step.indexOf('//');
594
+ if (-1 === commentIdx) return /*#__PURE__*/ jsx("div", {
595
+ children: step || ' '
596
+ }, stepKey);
597
+ const codePart = step.substring(0, commentIdx);
598
+ const commentPart = step.substring(commentIdx);
599
+ return /*#__PURE__*/ jsxs("div", {
600
+ children: [
601
+ /*#__PURE__*/ jsx("span", {
602
+ children: codePart
603
+ }),
604
+ /*#__PURE__*/ jsx("span", {
605
+ className: "vdev-code-comment",
606
+ style: styles.codeComment,
607
+ children: commentPart
608
+ })
609
+ ]
610
+ }, stepKey);
611
+ })
612
+ }) : /*#__PURE__*/ jsxs("ul", {
613
+ className: "vdev-list",
614
+ style: styles.list,
615
+ children: [
616
+ isModuleFetchError ? /*#__PURE__*/ jsxs(Fragment, {
617
+ children: [
618
+ /*#__PURE__*/ jsxs("li", {
619
+ className: "vdev-li",
620
+ style: styles.li,
621
+ children: [
622
+ /*#__PURE__*/ jsx("b", {
623
+ children: "Verify Path"
624
+ }),
625
+ ": check the entry mapping for",
626
+ ' ',
627
+ /*#__PURE__*/ jsx("code", {
628
+ className: "vdev-code",
629
+ style: styles.code,
630
+ children: componentNameLabel || 'this component'
631
+ }),
632
+ ' ',
633
+ "in",
634
+ ' ',
635
+ /*#__PURE__*/ jsx("code", {
636
+ className: "vdev-code",
637
+ style: styles.code,
638
+ children: "vite.config.ts"
639
+ }),
640
+ "."
641
+ ]
642
+ }),
643
+ /*#__PURE__*/ jsxs("li", {
644
+ className: "vdev-li",
645
+ style: styles.li,
646
+ children: [
647
+ /*#__PURE__*/ jsx("b", {
648
+ children: "Check Terminal"
649
+ }),
650
+ ": look at the terminal where",
651
+ ' ',
652
+ /*#__PURE__*/ jsx("code", {
653
+ className: "vdev-code",
654
+ style: styles.code,
655
+ children: currentOrigin
656
+ }),
657
+ ' ',
658
+ "is running for build errors."
659
+ ]
660
+ }),
661
+ /*#__PURE__*/ jsxs("li", {
662
+ className: "vdev-li",
663
+ style: styles.li,
664
+ children: [
665
+ /*#__PURE__*/ jsx("b", {
666
+ children: "Check Export"
667
+ }),
668
+ ": ensure the entry module exports a React component (default export recommended)."
669
+ ]
670
+ })
671
+ ]
672
+ }) : null,
673
+ isComponentNotFound ? /*#__PURE__*/ jsxs(Fragment, {
674
+ children: [
675
+ /*#__PURE__*/ jsxs("li", {
676
+ className: "vdev-li",
677
+ style: styles.li,
678
+ children: [
679
+ /*#__PURE__*/ jsx("b", {
680
+ children: "Update Config"
681
+ }),
682
+ ": map",
683
+ ' ',
684
+ /*#__PURE__*/ jsx("code", {
685
+ className: "vdev-code",
686
+ style: styles.code,
687
+ children: componentNameLabel
688
+ }),
689
+ ' ',
690
+ "in",
691
+ ' ',
692
+ /*#__PURE__*/ jsx("code", {
693
+ className: "vdev-code",
694
+ style: styles.code,
695
+ children: "devComponentMap"
696
+ }),
697
+ "."
698
+ ]
699
+ }),
700
+ /*#__PURE__*/ jsxs("li", {
701
+ className: "vdev-li",
702
+ style: styles.li,
703
+ children: [
704
+ /*#__PURE__*/ jsx("b", {
705
+ children: "Wildcard Fallback"
706
+ }),
707
+ ": add",
708
+ ' ',
709
+ /*#__PURE__*/ jsx("code", {
710
+ className: "vdev-code",
711
+ style: styles.code,
712
+ children: "'*': '/'"
713
+ }),
714
+ ' ',
715
+ "to map all components to the default entry."
716
+ ]
717
+ })
718
+ ]
719
+ }) : null,
720
+ isContractError ? /*#__PURE__*/ jsxs(Fragment, {
721
+ children: [
722
+ /*#__PURE__*/ jsxs("li", {
723
+ className: "vdev-li",
724
+ style: styles.li,
725
+ children: [
726
+ /*#__PURE__*/ jsx("b", {
727
+ children: "Check Server"
728
+ }),
729
+ ": ensure your Vite server is running at",
730
+ ' ',
731
+ /*#__PURE__*/ jsx("code", {
732
+ className: "vdev-code",
733
+ style: styles.code,
734
+ children: currentOrigin
735
+ }),
736
+ "."
737
+ ]
738
+ }),
739
+ /*#__PURE__*/ jsxs("li", {
740
+ className: "vdev-li",
741
+ style: styles.li,
742
+ children: [
743
+ /*#__PURE__*/ jsx("b", {
744
+ children: "Verify Origin"
745
+ }),
746
+ ": confirm localStorage key",
747
+ ' ',
748
+ /*#__PURE__*/ jsx("code", {
749
+ className: "vdev-code",
750
+ style: styles.code,
751
+ children: "VITE_DEV_SERVER_ORIGIN"
752
+ }),
753
+ ' ',
754
+ "is set to",
755
+ ' ',
756
+ /*#__PURE__*/ jsx("code", {
757
+ className: "vdev-code",
758
+ style: styles.code,
759
+ children: currentOrigin
760
+ }),
761
+ "."
762
+ ]
763
+ }),
764
+ /*#__PURE__*/ jsxs("li", {
765
+ className: "vdev-li",
766
+ style: styles.li,
767
+ children: [
768
+ /*#__PURE__*/ jsx("b", {
769
+ children: "Open Debug Panel"
770
+ }),
771
+ ":",
772
+ ' ',
773
+ /*#__PURE__*/ jsx("a", {
774
+ className: "vdev-link",
775
+ style: styles.link,
776
+ href: debugPanelUrl,
777
+ target: "_blank",
778
+ rel: "noreferrer",
779
+ children: debugPanelUrl
780
+ })
781
+ ]
782
+ })
783
+ ]
784
+ }) : null,
785
+ isModuleFetchError || isComponentNotFound || isContractError ? null : /*#__PURE__*/ jsxs("li", {
786
+ className: "vdev-li",
787
+ style: styles.li,
788
+ children: [
789
+ "Open Debug Panel:",
790
+ ' ',
791
+ /*#__PURE__*/ jsx("a", {
792
+ className: "vdev-link",
793
+ style: styles.link,
794
+ href: debugPanelUrl,
795
+ target: "_blank",
796
+ rel: "noreferrer",
797
+ children: debugPanelUrl
798
+ })
799
+ ]
800
+ })
801
+ ]
802
+ })
803
+ ]
804
+ })
805
+ ]
806
+ })
807
+ ]
808
+ });
809
+ }
810
+ function ReactLoader(props) {
811
+ const { url, origin, name, componentProps, contractEndpoint, loading, renderError, externalError } = props;
812
+ const state = useReactLoader({
813
+ url,
814
+ origin,
815
+ name,
816
+ componentProps,
817
+ contractEndpoint
818
+ });
819
+ const defaultLoading = /*#__PURE__*/ jsx("div", {
820
+ children: "Loading..."
821
+ });
822
+ const renderErrorNode = useCallback((err)=>{
823
+ if (renderError) return renderError(err);
824
+ const isLoaderErrorObject = !(err instanceof Error);
825
+ const errorMessage = err.message || String(err);
826
+ const currentOrigin = (isLoaderErrorObject ? err.origin : void 0) || origin || (url ? getOriginFromFinalJsUrl(url) : '');
827
+ const componentName = (isLoaderErrorObject ? err.componentName : void 0) || name;
828
+ const setupGuide = isLoaderErrorObject ? err.setupGuide : void 0;
829
+ const statusCode = isLoaderErrorObject ? err.statusCode : err.statusCode;
830
+ return /*#__PURE__*/ jsx(InlineViteErrorView, {
831
+ errorMessage: errorMessage,
832
+ currentOrigin: currentOrigin,
833
+ componentName: componentName,
834
+ setupGuide: setupGuide,
835
+ statusCode: statusCode
836
+ });
837
+ }, [
838
+ renderError,
839
+ name,
840
+ origin,
841
+ url
842
+ ]);
843
+ if (externalError) return /*#__PURE__*/ jsx(Fragment, {
844
+ children: renderErrorNode(externalError)
845
+ });
846
+ if (state.error) return /*#__PURE__*/ jsx(Fragment, {
847
+ children: renderErrorNode(state.error)
848
+ });
849
+ return /*#__PURE__*/ jsxs(Fragment, {
850
+ children: [
851
+ /*#__PURE__*/ jsx("div", {
852
+ ref: state.containerRef,
853
+ className: "vdev-loader-container",
854
+ "data-is": "ReactLoader"
855
+ }),
856
+ state.isReady ? null : loading ?? defaultLoading
857
+ ]
858
+ });
859
+ }
860
+ export { DEFAULT_CONTRACT_ENDPOINT, DEFAULT_INIT_ENDPOINT, ReactLoader, ensureBridgeInit, loadBridgeContract, resolveReactEntry, resolveReactEntryForLoader };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@dev-to/react-loader",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "peerDependencies": {
18
+ "react": ">=18.0.0",
19
+ "react-dom": ">=18.0.0",
20
+ "typescript": ">=5.0.0"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/YangYongAn/dev-to.git",
25
+ "directory": "packages/react-loader"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "@dev-to/react-shared": "0.1.0"
32
+ },
33
+ "scripts": {
34
+ "build": "rslib build",
35
+ "dev": "rslib build --watch",
36
+ "lint": "pnpm -w lint",
37
+ "test": "echo 'test @dev-to/react-loader'"
38
+ }
39
+ }