@lunatest/react 0.1.0 → 0.1.1

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.
@@ -1,14 +1,16 @@
1
1
  import React, { createContext, useMemo, useState } from "react";
2
2
  import { createLunaProvider } from "./luna-provider.js";
3
+ import { createProviderOptionsKey } from "./provider-options.js";
3
4
  export const LunaTestContext = createContext(null);
4
5
  export function LunaTestProvider(props) {
5
6
  const [scenarioId, setScenarioId] = useState(props.initialScenarioId);
7
+ const optionsKey = createProviderOptionsKey(props.options);
6
8
  const provider = useMemo(() => {
7
9
  if (props.provider) {
8
10
  return props.provider;
9
11
  }
10
12
  return createLunaProvider(props.options ?? {});
11
- }, [props.provider, props.options]);
13
+ }, [props.provider, optionsKey, props.options?.callHandler]);
12
14
  const value = useMemo(() => ({
13
15
  provider,
14
16
  scenarioId,
@@ -1,15 +1,32 @@
1
- import type { LuaConfig } from "@lunatest/core";
1
+ import { type LunaWalletAssetState, type LunaWalletPermission } from "@lunatest/contracts";
2
+ import type { LuaConfig } from "@lunatest/core/browser";
3
+ import { type PresetRegistry, type ProjectPresetSources } from "@lunatest/core/browser";
2
4
  import { type LunaRuntimeInterceptConfig } from "@lunatest/runtime-intercept";
3
5
  export type LunaBootstrapOptions = {
6
+ enable?: boolean;
4
7
  source?: string | URL;
5
8
  nodeEnv?: string;
6
9
  mountDevtools?: boolean;
7
10
  devtoolsTargetId?: string;
11
+ presetRegistry?: PresetRegistry;
12
+ projectPresetSources?: ProjectPresetSources;
13
+ protocolPresetId?: string;
14
+ protocolPresetParams?: Record<string, unknown>;
15
+ walletPresetId?: string;
16
+ walletPresetParams?: Record<string, unknown>;
17
+ walletFallbackMode?: "off" | "manual-toggle";
18
+ walletPreset?: {
19
+ address: string;
20
+ chainId?: string;
21
+ permissions?: Array<LunaWalletPermission | string>;
22
+ assets?: Partial<LunaWalletAssetState>;
23
+ };
8
24
  configOverride?: Partial<LunaRuntimeInterceptConfig>;
9
25
  };
10
26
  export type LunaBootstrapResult = {
11
27
  enabled: boolean;
28
+ configLoaded: boolean;
12
29
  unmountDevtools?: () => void;
13
- config: LuaConfig;
30
+ config?: LuaConfig;
14
31
  };
15
32
  export declare function bootstrapLunaRuntime(options?: LunaBootstrapOptions): Promise<LunaBootstrapResult>;
package/dist/bootstrap.js CHANGED
@@ -1,9 +1,11 @@
1
- import { loadLunaConfig } from "@lunatest/core";
2
- import { applyInterceptState, enableLunaRuntimeIntercept, setRouteMocks, } from "@lunatest/runtime-intercept";
1
+ import { createLunaWalletAssetState, normalizeWalletPermissions, } from "@lunatest/contracts";
2
+ import { loadLunaConfig, createPresetRegistry, materializeProtocolPreset, materializeWalletPreset, } from "@lunatest/core/browser";
3
+ import { applyInterceptState, disableLunaRuntimeIntercept, enableLunaRuntimeIntercept, resolveEnabled, setWalletSession, setRouteMocks, } from "@lunatest/runtime-intercept";
3
4
  import { mountLunaDevtools } from "./devtools/mount.js";
4
5
  import { resolveNodeEnv } from "./node-env.js";
5
- function toRuntimeConfig(config, configOverride) {
6
+ function toRuntimeConfig(config, enable, configOverride) {
6
7
  return {
8
+ enable,
7
9
  ...configOverride,
8
10
  intercept: {
9
11
  ...configOverride?.intercept,
@@ -15,34 +17,85 @@ function toRuntimeConfig(config, configOverride) {
15
17
  },
16
18
  };
17
19
  }
20
+ function resolveBootstrapEnabled(options, nodeEnv) {
21
+ if (typeof options.enable === "boolean") {
22
+ return options.enable;
23
+ }
24
+ return resolveEnabled({
25
+ enable: options.configOverride?.enable,
26
+ }, nodeEnv);
27
+ }
18
28
  export async function bootstrapLunaRuntime(options = {}) {
19
- const config = await loadLunaConfig(options.source ?? "./lunatest.lua");
20
29
  const nodeEnv = resolveNodeEnv(options.nodeEnv);
21
- const runtimeConfig = toRuntimeConfig(config, options.configOverride);
30
+ const bootstrapEnabled = resolveBootstrapEnabled(options, nodeEnv);
31
+ if (!bootstrapEnabled) {
32
+ return {
33
+ enabled: false,
34
+ configLoaded: false,
35
+ };
36
+ }
37
+ const config = await loadLunaConfig(options.source ?? "./lunatest.lua");
38
+ const presetRegistry = options.presetRegistry ??
39
+ createPresetRegistry({
40
+ projectSources: options.projectPresetSources,
41
+ });
42
+ const runtimeConfig = toRuntimeConfig(config, options.enable ?? options.configOverride?.enable, options.configOverride);
22
43
  const enabled = enableLunaRuntimeIntercept(runtimeConfig, nodeEnv);
23
44
  if (!enabled) {
24
45
  return {
25
46
  enabled: false,
26
47
  config,
48
+ configLoaded: true,
27
49
  };
28
50
  }
29
- const routeMocks = options.configOverride?.intercept?.routes ?? config.intercept?.routes ?? [];
30
- setRouteMocks(routeMocks);
31
- if (config.given) {
32
- applyInterceptState(config.given);
51
+ try {
52
+ const routeMocks = options.configOverride?.intercept?.routes ?? config.intercept?.routes ?? [];
53
+ setRouteMocks(routeMocks);
54
+ if (config.given) {
55
+ applyInterceptState(config.given);
56
+ }
57
+ if (config.intercept?.state) {
58
+ applyInterceptState(config.intercept.state);
59
+ }
60
+ if (options.protocolPresetId) {
61
+ const materialized = await materializeProtocolPreset(options.protocolPresetId, options.protocolPresetParams, presetRegistry);
62
+ setRouteMocks(materialized.routeMocks);
63
+ applyInterceptState(materialized.interceptState);
64
+ setWalletSession(materialized.walletSession);
65
+ }
66
+ if (options.walletPresetId) {
67
+ const materialized = await materializeWalletPreset(options.walletPresetId, options.walletPresetParams, presetRegistry);
68
+ setWalletSession(materialized.walletSession);
69
+ }
70
+ if (options.walletPreset) {
71
+ setWalletSession({
72
+ enabled: false,
73
+ connected: false,
74
+ chainId: options.walletPreset.chainId ?? "0x1",
75
+ accounts: [options.walletPreset.address],
76
+ permissions: normalizeWalletPermissions(options.walletPreset.permissions),
77
+ assets: createLunaWalletAssetState(options.walletPreset.assets),
78
+ });
79
+ }
80
+ const unmountDevtools = options.mountDevtools === false
81
+ ? undefined
82
+ : mountLunaDevtools({
83
+ targetId: options.devtoolsTargetId,
84
+ nodeEnv,
85
+ panelProps: {
86
+ presetRegistry,
87
+ walletFallbackMode: options.walletFallbackMode ?? "off",
88
+ },
89
+ }) ?? undefined;
90
+ return {
91
+ enabled: true,
92
+ configLoaded: true,
93
+ unmountDevtools,
94
+ config,
95
+ };
33
96
  }
34
- if (config.intercept?.state) {
35
- applyInterceptState(config.intercept.state);
97
+ catch (error) {
98
+ disableLunaRuntimeIntercept();
99
+ throw error;
36
100
  }
37
- const unmountDevtools = options.mountDevtools === false
38
- ? undefined
39
- : mountLunaDevtools({
40
- targetId: options.devtoolsTargetId,
41
- nodeEnv,
42
- }) ?? undefined;
43
- return {
44
- enabled: true,
45
- unmountDevtools,
46
- config,
47
- };
48
101
  }
@@ -0,0 +1,5 @@
1
+ export type { LunaBootstrapOptions, LunaBootstrapResult } from "./bootstrap.js";
2
+ export { bootstrapLunaRuntime } from "./bootstrap.js";
3
+ export { enableLunaIntercept } from "./intercept.js";
4
+ export { LunaDevtoolsPanel } from "./devtools/LunaDevtoolsPanel.js";
5
+ export { mountLunaDevtools } from "./devtools/mount.js";
@@ -0,0 +1,4 @@
1
+ export { bootstrapLunaRuntime } from "./bootstrap.js";
2
+ export { enableLunaIntercept } from "./intercept.js";
3
+ export { LunaDevtoolsPanel } from "./devtools/LunaDevtoolsPanel.js";
4
+ export { mountLunaDevtools } from "./devtools/mount.js";
@@ -1,3 +1,5 @@
1
+ import { type PresetRegistry } from "@lunatest/core/browser";
2
+ import type { PresetDiagnostic } from "@lunatest/contracts";
1
3
  import { type RouteMock } from "@lunatest/runtime-intercept";
2
4
  type LunaDevtoolsPanelProps = {
3
5
  title?: string;
@@ -15,6 +17,9 @@ type LunaDevtoolsPanelProps = {
15
17
  };
16
18
  onSetRouteMocks?: (routes: RouteMock[]) => void | Promise<void>;
17
19
  onPatchState?: (patch: Record<string, unknown>) => void | Promise<void>;
20
+ initialPresetDiagnostics?: PresetDiagnostic[];
21
+ presetRegistry?: PresetRegistry;
22
+ walletFallbackMode?: "off" | "manual-toggle";
18
23
  };
19
24
  export declare function LunaDevtoolsPanel(props: LunaDevtoolsPanelProps): import("react/jsx-runtime").JSX.Element;
20
25
  export {};
@@ -1,11 +1,39 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
3
- import { executeLuaScenario, loadLunaConfig } from "@lunatest/core";
4
- import { applyInterceptState, getInterceptState, setRouteMocks, } from "@lunatest/runtime-intercept";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { createPresetRegistry, executeLuaScenario, getPresetDiagnostics, listProtocolPresets, listWalletPresets, loadLunaConfig, materializeProtocolPreset, materializeWalletPreset, } from "@lunatest/core/browser";
4
+ import { applyInterceptState, connectWalletSession, disconnectWalletSession, getInterceptState, getWalletSession, setWalletSession, setRouteMocks, } from "@lunatest/runtime-intercept";
5
5
  function toPrettyJson(input) {
6
6
  return JSON.stringify(input, null, 2);
7
7
  }
8
+ function buildDefaultParamValues(schema) {
9
+ return Object.fromEntries(schema.map((item) => [item.key, item.default === undefined ? "" : String(item.default)]));
10
+ }
11
+ function parsePresetValue(descriptor, value) {
12
+ if (descriptor.type === "number" || descriptor.type === "chainId") {
13
+ return Number(value);
14
+ }
15
+ if (descriptor.type === "boolean") {
16
+ return value === "true";
17
+ }
18
+ return value;
19
+ }
20
+ function resolveWalletSelectionId(walletPresets, candidateId) {
21
+ const exact = walletPresets.find((item) => item.qualifiedId === candidateId);
22
+ if (exact) {
23
+ return exact.qualifiedId;
24
+ }
25
+ const builtin = walletPresets.find((item) => item.qualifiedId === `builtin/${candidateId}`);
26
+ if (builtin) {
27
+ return builtin.qualifiedId;
28
+ }
29
+ const project = walletPresets.find((item) => item.qualifiedId === `project/${candidateId}`);
30
+ if (project) {
31
+ return project.qualifiedId;
32
+ }
33
+ return candidateId;
34
+ }
8
35
  export function LunaDevtoolsPanel(props) {
36
+ const presetRegistry = useMemo(() => props.presetRegistry ?? createPresetRegistry(), [props.presetRegistry]);
9
37
  const initialLuaScript = useMemo(() => props.initialLua ??
10
38
  `scenario {
11
39
  name = "swap-warning",
@@ -33,6 +61,114 @@ export function LunaDevtoolsPanel(props) {
33
61
  const [status, setStatus] = useState("idle");
34
62
  const [error, setError] = useState(null);
35
63
  const [diff, setDiff] = useState(null);
64
+ const [protocolPresets, setProtocolPresets] = useState([]);
65
+ const [walletPresets, setWalletPresets] = useState([]);
66
+ const [selectedProtocolPresetId, setSelectedProtocolPresetId] = useState("");
67
+ const [selectedWalletPresetId, setSelectedWalletPresetId] = useState("");
68
+ const [selectedBuiltinScenarioId, setSelectedBuiltinScenarioId] = useState("");
69
+ const [presetParamValues, setPresetParamValues] = useState({});
70
+ const [presetParamsJson, setPresetParamsJson] = useState("{}");
71
+ const [materializedPreview, setMaterializedPreview] = useState("");
72
+ const [presetDiagnostics, setPresetDiagnostics] = useState(props.initialPresetDiagnostics ?? []);
73
+ const [walletSession, setWalletSessionState] = useState(() => {
74
+ try {
75
+ return getWalletSession();
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ });
81
+ const refreshWalletSession = () => {
82
+ try {
83
+ setWalletSessionState(getWalletSession());
84
+ }
85
+ catch {
86
+ setWalletSessionState(null);
87
+ }
88
+ };
89
+ const refreshPresetDiagnostics = async () => {
90
+ try {
91
+ const diagnostics = await getPresetDiagnostics(presetRegistry);
92
+ const projectDiagnostics = diagnostics.filter((item) => item.source === "project");
93
+ setPresetDiagnostics(projectDiagnostics.length > 0 ? projectDiagnostics : diagnostics);
94
+ }
95
+ catch (cause) {
96
+ setError(cause instanceof Error ? cause.message : String(cause));
97
+ }
98
+ };
99
+ useEffect(() => {
100
+ let cancelled = false;
101
+ async function loadBuiltIns() {
102
+ try {
103
+ const [protocols, wallets] = await Promise.all([
104
+ listProtocolPresets(presetRegistry),
105
+ listWalletPresets(presetRegistry),
106
+ ]);
107
+ if (cancelled) {
108
+ return;
109
+ }
110
+ setProtocolPresets(protocols);
111
+ setWalletPresets(wallets);
112
+ setSelectedProtocolPresetId((current) => current || protocols[0]?.qualifiedId || "");
113
+ setSelectedWalletPresetId((current) => current || wallets[0]?.qualifiedId || "");
114
+ await refreshPresetDiagnostics();
115
+ }
116
+ catch (cause) {
117
+ if (!cancelled) {
118
+ setError(cause instanceof Error ? cause.message : String(cause));
119
+ }
120
+ }
121
+ }
122
+ void loadBuiltIns();
123
+ return () => {
124
+ cancelled = true;
125
+ };
126
+ }, [presetRegistry]);
127
+ const selectedProtocolPreset = useMemo(() => protocolPresets.find((item) => item.qualifiedId === selectedProtocolPresetId) ?? null, [protocolPresets, selectedProtocolPresetId]);
128
+ const selectedWalletPreset = useMemo(() => walletPresets.find((item) => item.qualifiedId === selectedWalletPresetId) ?? null, [walletPresets, selectedWalletPresetId]);
129
+ const recommendedDescriptors = useMemo(() => {
130
+ const descriptors = new Map();
131
+ if (selectedProtocolPreset) {
132
+ const keys = new Set(selectedProtocolPreset.recommendedControls);
133
+ for (const descriptor of selectedProtocolPreset.paramsSchema) {
134
+ if (keys.has(descriptor.key)) {
135
+ descriptors.set(descriptor.key, descriptor);
136
+ }
137
+ }
138
+ }
139
+ if (selectedWalletPreset?.paramsSchema) {
140
+ const keys = new Set(selectedWalletPreset.recommendedControls ?? []);
141
+ for (const descriptor of selectedWalletPreset.paramsSchema) {
142
+ if (keys.has(descriptor.key) && !descriptors.has(descriptor.key)) {
143
+ descriptors.set(descriptor.key, descriptor);
144
+ }
145
+ }
146
+ }
147
+ return Array.from(descriptors.values());
148
+ }, [selectedProtocolPreset, selectedWalletPreset]);
149
+ useEffect(() => {
150
+ if (!selectedProtocolPreset) {
151
+ return;
152
+ }
153
+ setPresetParamValues(buildDefaultParamValues(selectedProtocolPreset.paramsSchema));
154
+ setSelectedBuiltinScenarioId(selectedProtocolPreset.builtinScenarios[0]?.id ?? "");
155
+ setSelectedWalletPresetId(resolveWalletSelectionId(walletPresets, selectedProtocolPreset.defaultWalletPreset.id));
156
+ }, [selectedProtocolPreset, walletPresets]);
157
+ const buildPresetParams = () => {
158
+ const params = {};
159
+ for (const descriptor of recommendedDescriptors) {
160
+ const current = presetParamValues[descriptor.key];
161
+ if (current === undefined || current === "") {
162
+ continue;
163
+ }
164
+ params[descriptor.key] = parsePresetValue(descriptor, current);
165
+ }
166
+ const advanced = JSON.parse(presetParamsJson);
167
+ return {
168
+ ...params,
169
+ ...advanced,
170
+ };
171
+ };
36
172
  const handleRun = async () => {
37
173
  setError(null);
38
174
  setDiff(null);
@@ -76,6 +212,8 @@ export function LunaDevtoolsPanel(props) {
76
212
  if (executed.diff) {
77
213
  setDiff(executed.diff);
78
214
  }
215
+ refreshWalletSession();
216
+ await refreshPresetDiagnostics();
79
217
  }
80
218
  catch (cause) {
81
219
  setStatus("failed");
@@ -95,6 +233,8 @@ export function LunaDevtoolsPanel(props) {
95
233
  setRouteMocks(parsed);
96
234
  }
97
235
  setStatus("routes-updated");
236
+ refreshWalletSession();
237
+ await refreshPresetDiagnostics();
98
238
  }
99
239
  catch (cause) {
100
240
  setStatus("failed");
@@ -114,6 +254,7 @@ export function LunaDevtoolsPanel(props) {
114
254
  applyInterceptState(parsed);
115
255
  }
116
256
  setStatus("state-patched");
257
+ refreshWalletSession();
117
258
  }
118
259
  catch (cause) {
119
260
  setStatus("failed");
@@ -127,6 +268,60 @@ export function LunaDevtoolsPanel(props) {
127
268
  setStatus("reset");
128
269
  setError(null);
129
270
  setDiff(null);
271
+ refreshWalletSession();
272
+ void refreshPresetDiagnostics();
273
+ };
274
+ const handleApplyProtocolPreset = async () => {
275
+ if (!selectedProtocolPresetId) {
276
+ return;
277
+ }
278
+ setError(null);
279
+ setStatus("applying-protocol-preset");
280
+ try {
281
+ const params = buildPresetParams();
282
+ const materialized = await materializeProtocolPreset(selectedProtocolPresetId, params, presetRegistry);
283
+ let nextWalletSession = materialized.walletSession;
284
+ if (selectedWalletPresetId && selectedWalletPresetId !== materialized.walletPresetId) {
285
+ const walletMaterialized = await materializeWalletPreset(selectedWalletPresetId, params, presetRegistry);
286
+ nextWalletSession = walletMaterialized.walletSession;
287
+ }
288
+ setWalletSession(nextWalletSession);
289
+ setRouteMocks(materialized.routeMocks);
290
+ applyInterceptState(materialized.interceptState);
291
+ setMaterializedPreview(toPrettyJson(materialized));
292
+ setSelectedWalletPresetId(materialized.walletPresetId);
293
+ const scenario = materialized.builtinScenarios.find((item) => item.id === selectedBuiltinScenarioId) ??
294
+ materialized.builtinScenarios[0];
295
+ if (scenario) {
296
+ setLuaScript(scenario.lua);
297
+ }
298
+ refreshWalletSession();
299
+ await refreshPresetDiagnostics();
300
+ setStatus("protocol-preset-applied");
301
+ }
302
+ catch (cause) {
303
+ setStatus("failed");
304
+ setError(cause instanceof Error ? cause.message : String(cause));
305
+ }
306
+ };
307
+ const handleApplyWalletPreset = async () => {
308
+ if (!selectedWalletPresetId) {
309
+ return;
310
+ }
311
+ setError(null);
312
+ setStatus("applying-wallet-preset");
313
+ try {
314
+ const materialized = await materializeWalletPreset(selectedWalletPresetId, buildPresetParams(), presetRegistry);
315
+ setWalletSession(materialized.walletSession);
316
+ setMaterializedPreview(toPrettyJson(materialized));
317
+ refreshWalletSession();
318
+ await refreshPresetDiagnostics();
319
+ setStatus("wallet-preset-applied");
320
+ }
321
+ catch (cause) {
322
+ setStatus("failed");
323
+ setError(cause instanceof Error ? cause.message : String(cause));
324
+ }
130
325
  };
131
326
  return (_jsxs("aside", { "data-lunatest-devtools": true, style: {
132
327
  position: "fixed",
@@ -144,5 +339,46 @@ export function LunaDevtoolsPanel(props) {
144
339
  fontSize: 12,
145
340
  color: "#0f172a",
146
341
  padding: 12,
147
- }, children: [_jsx("h3", { style: { margin: 0, marginBottom: 8 }, children: props.title ?? "LunaTest Devtools" }), _jsx("p", { style: { margin: 0, marginBottom: 10, color: "#334155" }, children: "Lua DSL\uACFC \uC778\uD130\uC149\uD2B8 \uC0C1\uD0DC\uB97C \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uBC14\uB85C \uC218\uC815\uD569\uB2C8\uB2E4." }), _jsx("label", { htmlFor: "lunatest-lua-script", children: "Lua Scenario" }), _jsx("textarea", { id: "lunatest-lua-script", value: luaScript, onChange: (event) => setLuaScript(event.currentTarget.value), style: { width: "100%", minHeight: 140, marginBottom: 8 } }), _jsx("button", { type: "button", onClick: handleRun, children: "Run Scenario" }), _jsx("hr", {}), _jsx("label", { htmlFor: "lunatest-routes", children: "Route Mocks (JSON array)" }), _jsx("textarea", { id: "lunatest-routes", value: routeJson, onChange: (event) => setRouteJson(event.currentTarget.value), style: { width: "100%", minHeight: 110, marginBottom: 8 } }), _jsx("button", { type: "button", onClick: handleApplyRoutes, children: "Apply Routes" }), _jsx("hr", {}), _jsx("label", { htmlFor: "lunatest-state", children: "Intercept State Patch (JSON object)" }), _jsx("textarea", { id: "lunatest-state", value: stateJson, onChange: (event) => setStateJson(event.currentTarget.value), style: { width: "100%", minHeight: 110, marginBottom: 8 } }), _jsx("button", { type: "button", onClick: handlePatchState, children: "Patch State" }), _jsx("button", { type: "button", onClick: handleReset, style: { marginLeft: 8 }, children: "Reset" }), _jsxs("p", { style: { marginTop: 10, marginBottom: 0 }, children: ["status: ", status] }), error ? (_jsxs("p", { style: { marginTop: 4, marginBottom: 0, color: "#b91c1c" }, children: ["error: ", error] })) : null, diff ? (_jsxs("pre", { style: { marginTop: 8, marginBottom: 0, color: "#7f1d1d", whiteSpace: "pre-wrap" }, children: ["diff: ", diff] })) : null] }));
342
+ }, children: [_jsx("h3", { style: { margin: 0, marginBottom: 8 }, children: props.title ?? "LunaTest Devtools" }), _jsx("p", { style: { margin: 0, marginBottom: 10, color: "#334155" }, children: "Lua DSL\uACFC \uC778\uD130\uC149\uD2B8 \uC0C1\uD0DC\uB97C \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uBC14\uB85C \uC218\uC815\uD569\uB2C8\uB2E4." }), _jsx("label", { htmlFor: "lunatest-lua-script", children: "Lua Scenario" }), _jsx("textarea", { id: "lunatest-lua-script", value: luaScript, onChange: (event) => setLuaScript(event.currentTarget.value), style: { width: "100%", minHeight: 140, marginBottom: 8 } }), _jsx("button", { type: "button", onClick: handleRun, children: "Run Scenario" }), _jsx("hr", {}), _jsxs("h4", { style: { margin: 0, marginBottom: 8 }, children: ["Diagnostics (", presetDiagnostics.length, ")"] }), presetDiagnostics.length === 0 ? (_jsx("p", { style: { margin: 0, marginBottom: 10, color: "#334155" }, children: "No preset diagnostics." })) : (_jsx("div", { style: { marginBottom: 12 }, children: presetDiagnostics.map((diagnostic) => (_jsxs("div", { style: {
343
+ border: "1px solid #cbd5e1",
344
+ borderRadius: 8,
345
+ padding: 8,
346
+ marginBottom: 8,
347
+ }, children: [_jsxs("p", { style: { margin: 0, marginBottom: 4 }, children: ["[", diagnostic.source, "] ", diagnostic.code] }), _jsx("p", { style: { margin: 0, marginBottom: 4 }, children: diagnostic.message }), diagnostic.qualifiedId ? (_jsxs("p", { style: { margin: 0, marginBottom: 4 }, children: ["preset: ", diagnostic.qualifiedId] })) : null, diagnostic.hint ? _jsxs("p", { style: { margin: 0 }, children: ["hint: ", diagnostic.hint] }) : null] }, `${diagnostic.code}:${diagnostic.qualifiedId ?? "none"}`))) })), _jsx("hr", {}), _jsx("h4", { style: { margin: 0, marginBottom: 8 }, children: "Protocol Preset" }), _jsx("label", { htmlFor: "lunatest-protocol-preset", children: "Protocol Preset" }), _jsx("select", { id: "lunatest-protocol-preset", value: selectedProtocolPresetId, onChange: (event) => setSelectedProtocolPresetId(event.currentTarget.value), style: { width: "100%", marginBottom: 8 }, children: protocolPresets.map((preset) => (_jsxs("option", { value: preset.qualifiedId, children: ["[", preset.source, "] ", preset.label] }, preset.qualifiedId))) }), _jsx("label", { htmlFor: "lunatest-wallet-preset", children: "Wallet Preset" }), _jsx("select", { id: "lunatest-wallet-preset", value: selectedWalletPresetId, onChange: (event) => setSelectedWalletPresetId(event.currentTarget.value), style: { width: "100%", marginBottom: 8 }, children: walletPresets.map((preset) => (_jsxs("option", { value: preset.qualifiedId, children: ["[", preset.source, "] ", preset.label] }, preset.qualifiedId))) }), recommendedDescriptors.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("h4", { style: { margin: 0, marginBottom: 8 }, children: "Recommended Controls" }), recommendedDescriptors.map((descriptor) => (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("label", { htmlFor: `preset-control-${descriptor.key}`, children: descriptor.label }), _jsx("input", { id: `preset-control-${descriptor.key}`, value: presetParamValues[descriptor.key] ?? "", onChange: (event) => setPresetParamValues((current) => ({
348
+ ...current,
349
+ [descriptor.key]: event.currentTarget.value,
350
+ })), style: { width: "100%" } })] }, descriptor.key)))] })) : null, _jsx("label", { htmlFor: "lunatest-preset-params", children: "Advanced Preset Params (JSON)" }), _jsx("textarea", { id: "lunatest-preset-params", value: presetParamsJson, onChange: (event) => setPresetParamsJson(event.currentTarget.value), style: { width: "100%", minHeight: 90, marginBottom: 8 } }), selectedProtocolPreset?.builtinScenarios?.length ? (_jsxs(_Fragment, { children: [_jsx("label", { htmlFor: "lunatest-builtin-scenario", children: "Built-in Scenario" }), _jsx("select", { id: "lunatest-builtin-scenario", value: selectedBuiltinScenarioId, onChange: (event) => setSelectedBuiltinScenarioId(event.currentTarget.value), style: { width: "100%", marginBottom: 8 }, children: selectedProtocolPreset.builtinScenarios.map((scenario) => (_jsx("option", { value: scenario.id, children: scenario.label }, scenario.id))) })] })) : null, _jsx("button", { type: "button", onClick: handleApplyProtocolPreset, children: "Apply Protocol Preset" }), _jsx("button", { type: "button", style: { marginLeft: 8 }, onClick: handleApplyWalletPreset, children: "Apply Wallet Preset" }), materializedPreview ? (_jsxs(_Fragment, { children: [_jsx("p", { style: { marginTop: 10, marginBottom: 4 }, children: "materialized:" }), _jsx("pre", { style: { marginTop: 0, whiteSpace: "pre-wrap" }, children: materializedPreview })] })) : null, props.walletFallbackMode === "manual-toggle" ? (_jsxs(_Fragment, { children: [_jsx("hr", {}), _jsx("h4", { style: { margin: 0, marginBottom: 8 }, children: "Luna Wallet" }), _jsx("p", { style: { margin: 0, marginBottom: 8, color: "#334155" }, children: "\uC9C0\uAC11\uC774 \uC5C6\uC5B4\uB3C4 wallet RPC\uB97C Luna session\uC73C\uB85C \uD558\uC774\uC7AC\uD0B9\uD569\uB2C8\uB2E4." }), _jsxs("p", { style: { margin: 0, marginBottom: 8 }, children: ["mode: ", walletSession?.enabled ? "on" : "off", " / connected: ", walletSession?.connected ? "yes" : "no"] }), _jsxs("p", { style: { margin: 0, marginBottom: 8 }, children: ["account: ", walletSession?.accounts?.[0] ?? "n/a"] }), _jsxs("p", { style: { margin: 0, marginBottom: 8 }, children: ["chain: ", walletSession?.chainId ?? "n/a"] }), _jsxs("p", { style: { margin: 0, marginBottom: 8 }, children: ["permissions: ", walletSession?.permissions?.map((item) => item.parentCapability).join(", ") || "none"] }), _jsx("button", { type: "button", onClick: () => {
351
+ try {
352
+ if (walletSession?.enabled) {
353
+ disconnectWalletSession();
354
+ }
355
+ else {
356
+ connectWalletSession();
357
+ }
358
+ refreshWalletSession();
359
+ setStatus("wallet-updated");
360
+ setError(null);
361
+ }
362
+ catch (cause) {
363
+ setStatus("failed");
364
+ setError(cause instanceof Error ? cause.message : String(cause));
365
+ }
366
+ }, children: walletSession?.enabled ? "Disable Luna Wallet" : "Enable Luna Wallet" }), _jsx("button", { type: "button", style: { marginLeft: 8 }, onClick: () => {
367
+ try {
368
+ setWalletSession({
369
+ enabled: walletSession?.enabled ?? false,
370
+ connected: walletSession?.connected ?? false,
371
+ chainId: "0xaa36a7",
372
+ accounts: ["0x1111111111111111111111111111111111111111"],
373
+ permissions: walletSession?.permissions ?? [],
374
+ });
375
+ refreshWalletSession();
376
+ setStatus("wallet-updated");
377
+ setError(null);
378
+ }
379
+ catch (cause) {
380
+ setStatus("failed");
381
+ setError(cause instanceof Error ? cause.message : String(cause));
382
+ }
383
+ }, children: "Reset Session" })] })) : null, _jsx("hr", {}), _jsx("label", { htmlFor: "lunatest-routes", children: "Route Mocks (JSON array)" }), _jsx("textarea", { id: "lunatest-routes", value: routeJson, onChange: (event) => setRouteJson(event.currentTarget.value), style: { width: "100%", minHeight: 110, marginBottom: 8 } }), _jsx("button", { type: "button", onClick: handleApplyRoutes, children: "Apply Routes" }), _jsx("hr", {}), _jsx("label", { htmlFor: "lunatest-state", children: "Intercept State Patch (JSON object)" }), _jsx("textarea", { id: "lunatest-state", value: stateJson, onChange: (event) => setStateJson(event.currentTarget.value), style: { width: "100%", minHeight: 110, marginBottom: 8 } }), _jsx("button", { type: "button", onClick: handlePatchState, children: "Patch State" }), _jsx("button", { type: "button", onClick: handleReset, style: { marginLeft: 8 }, children: "Reset" }), _jsxs("p", { style: { marginTop: 10, marginBottom: 0 }, children: ["status: ", status] }), error ? (_jsxs("p", { style: { marginTop: 4, marginBottom: 0, color: "#b91c1c" }, children: ["error: ", error] })) : null, diff ? (_jsxs("pre", { style: { marginTop: 8, marginBottom: 0, color: "#7f1d1d", whiteSpace: "pre-wrap" }, children: ["diff: ", diff] })) : null] }));
148
384
  }
@@ -5,6 +5,7 @@ import { resolveNodeEnv } from "../node-env.js";
5
5
  const DEFAULT_TARGET_ID = "lunatest-devtools-root";
6
6
  let activeRoot = null;
7
7
  let activeTarget = null;
8
+ let activeTargetOwned = false;
8
9
  export function mountLunaDevtools(options = {}) {
9
10
  if (typeof document === "undefined") {
10
11
  return null;
@@ -26,16 +27,18 @@ export function mountLunaDevtools(options = {}) {
26
27
  }
27
28
  activeRoot = createRoot(target);
28
29
  activeTarget = target;
30
+ activeTargetOwned = !existing;
29
31
  activeRoot.render(React.createElement(LunaDevtoolsPanel, options.panelProps));
30
32
  return () => {
31
33
  if (!activeRoot || !activeTarget) {
32
34
  return;
33
35
  }
34
36
  activeRoot.unmount();
35
- if (activeTarget.id === DEFAULT_TARGET_ID) {
37
+ if (activeTargetOwned) {
36
38
  activeTarget.remove();
37
39
  }
38
40
  activeRoot = null;
39
41
  activeTarget = null;
42
+ activeTargetOwned = false;
40
43
  };
41
44
  }
@@ -1,5 +1,7 @@
1
1
  import { useMemo } from "react";
2
2
  import { createLunaProvider } from "../luna-provider.js";
3
+ import { createProviderOptionsKey } from "../provider-options.js";
3
4
  export function useLunaProvider(options) {
4
- return useMemo(() => createLunaProvider(options), [options]);
5
+ const optionsKey = createProviderOptionsKey(options);
6
+ return useMemo(() => createLunaProvider(options), [optionsKey, options.callHandler]);
5
7
  }
@@ -0,0 +1,2 @@
1
+ import type { LunaProviderOptions } from "@lunatest/core";
2
+ export declare function createProviderOptionsKey(options: LunaProviderOptions | undefined): string;
@@ -0,0 +1,29 @@
1
+ function stableStringify(value) {
2
+ if (value === null || value === undefined) {
3
+ return String(value);
4
+ }
5
+ if (typeof value !== "object") {
6
+ return JSON.stringify(value);
7
+ }
8
+ if (Array.isArray(value)) {
9
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
10
+ }
11
+ const entries = Object.entries(value)
12
+ .filter((entry) => entry[1] !== undefined)
13
+ .sort(([left], [right]) => left.localeCompare(right));
14
+ return `{${entries
15
+ .map(([key, nested]) => `${JSON.stringify(key)}:${stableStringify(nested)}`)
16
+ .join(",")}}`;
17
+ }
18
+ export function createProviderOptionsKey(options) {
19
+ if (!options) {
20
+ return "undefined";
21
+ }
22
+ const serializable = {
23
+ chainId: options.chainId,
24
+ accounts: options.accounts,
25
+ balances: options.balances,
26
+ wallet: options.wallet,
27
+ };
28
+ return stableStringify(serializable);
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunatest/react",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,6 +11,11 @@
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
13
  },
14
+ "./browser": {
15
+ "types": "./dist/browser.d.ts",
16
+ "import": "./dist/browser.js",
17
+ "default": "./dist/browser.js"
18
+ },
14
19
  "./package.json": "./package.json"
15
20
  },
16
21
  "publishConfig": {
@@ -21,8 +26,9 @@
21
26
  "dist"
22
27
  ],
23
28
  "dependencies": {
24
- "@lunatest/core": "0.1.0",
25
- "@lunatest/runtime-intercept": "0.1.0"
29
+ "@lunatest/core": "0.1.1",
30
+ "@lunatest/runtime-intercept": "0.1.0",
31
+ "@lunatest/contracts": "0.1.0"
26
32
  },
27
33
  "peerDependencies": {
28
34
  "react": "^18.3.1 || ^19.0.0",
@@ -34,9 +40,14 @@
34
40
  "@types/react": "^18.3.12",
35
41
  "@types/react-dom": "^18.3.1"
36
42
  },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/songforthemute/lunatest",
46
+ "directory": "packages/react"
47
+ },
37
48
  "scripts": {
38
49
  "build": "tsc -p tsconfig.json",
39
50
  "test": "vitest run",
40
- "lint": "tsc -p tsconfig.json --noEmit"
51
+ "lint": "tsc -p tsconfig.lint.json --pretty false"
41
52
  }
42
53
  }