@lunatest/core 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.
Files changed (36) hide show
  1. package/dist/browser.d.ts +6 -0
  2. package/dist/browser.js +5 -0
  3. package/dist/config/lua-config.js +2 -43
  4. package/dist/config/read-source.d.ts +3 -0
  5. package/dist/config/read-source.js +56 -0
  6. package/dist/coverage/catalog.d.ts +14 -0
  7. package/dist/coverage/catalog.js +91 -0
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.js +3 -0
  10. package/dist/mocks/provider.js +1 -3
  11. package/dist/presets/__tests__/registry.test.ts +213 -0
  12. package/dist/presets/loader.d.ts +6 -0
  13. package/dist/presets/loader.js +40 -0
  14. package/dist/presets/loader.ts +58 -0
  15. package/dist/presets/project-sources.node.d.ts +2 -0
  16. package/dist/presets/project-sources.node.js +44 -0
  17. package/dist/presets/project-sources.node.ts +57 -0
  18. package/dist/presets/protocol/aave.lua +82 -0
  19. package/dist/presets/protocol/curve.lua +82 -0
  20. package/dist/presets/protocol/uniswap_v2.lua +92 -0
  21. package/dist/presets/protocol/uniswap_v3.lua +144 -0
  22. package/dist/presets/registry.d.ts +59 -0
  23. package/dist/presets/registry.js +483 -0
  24. package/dist/presets/registry.ts +784 -0
  25. package/dist/presets/wallet/demo_sepolia.lua +66 -0
  26. package/dist/presets/wallet/empty_wallet.lua +54 -0
  27. package/dist/provider/luna-provider.d.ts +3 -0
  28. package/dist/provider/luna-provider.js +66 -5
  29. package/dist/runner/assert.js +23 -1
  30. package/dist/runtime/bridge.js +10 -2
  31. package/dist/runtime/engine.js +6 -2
  32. package/dist/runtime/scenario-runtime.d.ts +8 -0
  33. package/dist/runtime/scenario-runtime.js +9 -0
  34. package/dist/scenario/index.d.ts +7 -1
  35. package/dist/scenario/index.js +8 -0
  36. package/package.json +13 -3
@@ -0,0 +1,6 @@
1
+ export declare const sdkName = "@lunatest/core/browser";
2
+ export { loadLunaConfig } from "./config/lua-config.js";
3
+ export { createPresetRegistry, getProtocolPreset, getPresetDiagnostics, getWalletPreset, listProtocolPresets, listWalletPresets, validateProtocolPresetSource, validateWalletPresetSource, materializeProtocolPreset, materializeWalletPreset, } from "./presets/registry.js";
4
+ export type { PresetRegistry, PresetRegistryOptions, ProjectPresetSources } from "./presets/registry.js";
5
+ export { applyInterceptState, createScenarioRuntime, LuaConfigSchema, setRouteMocks, type LuaConfig, type RouteMock, type ScenarioRuntime, } from "./runtime/scenario-runtime.js";
6
+ export { executeLuaScenario, type ExecuteLuaScenarioInput, type ExecuteLuaScenarioResult, } from "./runner/execute-scenario.js";
@@ -0,0 +1,5 @@
1
+ export const sdkName = "@lunatest/core/browser";
2
+ export { loadLunaConfig } from "./config/lua-config.js";
3
+ export { createPresetRegistry, getProtocolPreset, getPresetDiagnostics, getWalletPreset, listProtocolPresets, listWalletPresets, validateProtocolPresetSource, validateWalletPresetSource, materializeProtocolPreset, materializeWalletPreset, } from "./presets/registry.js";
4
+ export { applyInterceptState, createScenarioRuntime, LuaConfigSchema, setRouteMocks, } from "./runtime/scenario-runtime.js";
5
+ export { executeLuaScenario, } from "./runner/execute-scenario.js";
@@ -1,50 +1,9 @@
1
1
  import { createRuntime } from "../runtime/engine.js";
2
2
  import { LuaConfigSchema } from "../runtime/scenario-runtime.js";
3
3
  import { isRecord } from "@lunatest/contracts";
4
- function seemsInlineLua(input) {
5
- return input.includes("\n") || input.includes("scenario {") || input.includes("scenario{");
6
- }
7
- async function readSource(source) {
8
- if (source instanceof URL) {
9
- if (source.protocol === "http:" || source.protocol === "https:") {
10
- if (typeof fetch !== "function") {
11
- throw new Error(`Fetch API is unavailable for URL source: ${source.toString()}`);
12
- }
13
- const response = await fetch(source);
14
- if (!response.ok) {
15
- throw new Error(`Failed to load Lua config: ${source.toString()} (${response.status})`);
16
- }
17
- return response.text();
18
- }
19
- if (source.protocol !== "file:") {
20
- throw new Error(`Unsupported Lua config URL protocol: ${source.protocol}`);
21
- }
22
- const [{ fileURLToPath }, { readFile }] = await Promise.all([
23
- import("node:url"),
24
- import("node:fs/promises"),
25
- ]);
26
- return readFile(fileURLToPath(source), "utf8");
27
- }
28
- if (seemsInlineLua(source)) {
29
- return source;
30
- }
31
- if (typeof document !== "undefined" && typeof fetch === "function") {
32
- const response = await fetch(source);
33
- if (!response.ok) {
34
- throw new Error(`Failed to load Lua config: ${source} (${response.status})`);
35
- }
36
- return response.text();
37
- }
38
- try {
39
- const { readFile } = await import("node:fs/promises");
40
- return await readFile(source, "utf8");
41
- }
42
- catch {
43
- return source;
44
- }
45
- }
4
+ import { readLuaSource } from "./read-source.js";
46
5
  export async function loadLunaConfig(source) {
47
- const code = await readSource(source);
6
+ const code = await readLuaSource(source);
48
7
  const runtime = await createRuntime();
49
8
  let captured;
50
9
  runtime.register("scenario", (table) => {
@@ -0,0 +1,3 @@
1
+ type LuaSource = string | URL;
2
+ export declare function readLuaSource(source: LuaSource): Promise<string>;
3
+ export {};
@@ -0,0 +1,56 @@
1
+ function seemsInlineLua(input) {
2
+ return input.includes("\n") || input.includes("scenario {") || input.includes("scenario{");
3
+ }
4
+ function canUseBrowserFetch() {
5
+ return typeof document !== "undefined" && typeof fetch === "function";
6
+ }
7
+ function getBuiltinModule(specifier) {
8
+ if (typeof process === "undefined" ||
9
+ typeof process.getBuiltinModule !== "function") {
10
+ return null;
11
+ }
12
+ return process.getBuiltinModule(specifier);
13
+ }
14
+ export async function readLuaSource(source) {
15
+ if (source instanceof URL) {
16
+ if (source.protocol === "http:" || source.protocol === "https:") {
17
+ if (typeof fetch !== "function") {
18
+ throw new Error(`Fetch API is unavailable for URL source: ${source.toString()}`);
19
+ }
20
+ const response = await fetch(source);
21
+ if (!response.ok) {
22
+ throw new Error(`Failed to load Lua source: ${source.toString()} (${response.status})`);
23
+ }
24
+ return response.text();
25
+ }
26
+ if (source.protocol !== "file:") {
27
+ throw new Error(`Unsupported Lua source URL protocol: ${source.protocol}`);
28
+ }
29
+ const urlModule = getBuiltinModule("url");
30
+ const fsModule = getBuiltinModule("fs/promises");
31
+ if (!urlModule || !fsModule) {
32
+ throw new Error(`File URL source is unavailable in this runtime: ${source.toString()}`);
33
+ }
34
+ return fsModule.readFile(urlModule.fileURLToPath(source), "utf8");
35
+ }
36
+ if (seemsInlineLua(source)) {
37
+ return source;
38
+ }
39
+ if (canUseBrowserFetch()) {
40
+ const response = await fetch(source);
41
+ if (!response.ok) {
42
+ throw new Error(`Failed to load Lua source: ${source} (${response.status})`);
43
+ }
44
+ return response.text();
45
+ }
46
+ const fsModule = getBuiltinModule("fs/promises");
47
+ if (fsModule) {
48
+ try {
49
+ return await fsModule.readFile(source, "utf8");
50
+ }
51
+ catch {
52
+ return source;
53
+ }
54
+ }
55
+ return source;
56
+ }
@@ -0,0 +1,14 @@
1
+ import type { CoverageCatalog, CoverageMetadata, CoverageSnapshot } from "@lunatest/contracts";
2
+ type CoverageCarrier = {
3
+ when?: Record<string, unknown>;
4
+ then_ui?: Record<string, unknown>;
5
+ then_state?: Record<string, unknown>;
6
+ not_present?: string[];
7
+ coverage?: CoverageMetadata;
8
+ };
9
+ export declare function resolveCoverageMetadata(input: CoverageCarrier): CoverageCatalog;
10
+ export declare function buildCoverageSnapshot(input: {
11
+ items: CoverageCarrier[];
12
+ coverageCatalog?: Partial<CoverageCatalog>;
13
+ }): CoverageSnapshot;
14
+ export {};
@@ -0,0 +1,91 @@
1
+ function normalizeList(values) {
2
+ if (!values) {
3
+ return [];
4
+ }
5
+ return Array.from(new Set(values
6
+ .map((value) => value.trim())
7
+ .filter((value) => value.length > 0))).sort();
8
+ }
9
+ function emptyCatalog() {
10
+ return {
11
+ features: [],
12
+ states: [],
13
+ components: [],
14
+ };
15
+ }
16
+ function topLevelKeys(value) {
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
18
+ return [];
19
+ }
20
+ return Object.keys(value);
21
+ }
22
+ function inferCoverageMetadata(input) {
23
+ const inferredStates = new Set([
24
+ ...topLevelKeys(input.then_ui),
25
+ ...topLevelKeys(input.then_state),
26
+ ...(input.not_present ?? []),
27
+ ]);
28
+ return {
29
+ features: input.when && typeof input.when.action === "string"
30
+ ? [input.when.action]
31
+ : [],
32
+ states: Array.from(inferredStates).sort(),
33
+ components: topLevelKeys(input.then_ui).sort(),
34
+ };
35
+ }
36
+ export function resolveCoverageMetadata(input) {
37
+ const inferred = inferCoverageMetadata(input);
38
+ const declared = input.coverage ?? {};
39
+ return {
40
+ features: declared.features !== undefined
41
+ ? normalizeList(declared.features)
42
+ : inferred.features,
43
+ states: declared.states !== undefined
44
+ ? normalizeList(declared.states)
45
+ : inferred.states,
46
+ components: declared.components !== undefined
47
+ ? normalizeList(declared.components)
48
+ : inferred.components,
49
+ };
50
+ }
51
+ function mergeCatalogs(base, next) {
52
+ return {
53
+ features: normalizeList([...base.features, ...next.features]),
54
+ states: normalizeList([...base.states, ...next.states]),
55
+ components: normalizeList([...base.components, ...next.components]),
56
+ };
57
+ }
58
+ function subtractCatalog(known, coveredTargets) {
59
+ const coveredFeatures = new Set(coveredTargets.features);
60
+ const coveredStates = new Set(coveredTargets.states);
61
+ const coveredComponents = new Set(coveredTargets.components);
62
+ return {
63
+ features: known.features.filter((feature) => !coveredFeatures.has(feature)),
64
+ states: known.states.filter((state) => !coveredStates.has(state)),
65
+ components: known.components.filter((component) => !coveredComponents.has(component)),
66
+ };
67
+ }
68
+ export function buildCoverageSnapshot(input) {
69
+ const knownFromCatalog = {
70
+ features: normalizeList(input.coverageCatalog?.features),
71
+ states: normalizeList(input.coverageCatalog?.states),
72
+ components: normalizeList(input.coverageCatalog?.components),
73
+ };
74
+ const coveredTargets = input.items.reduce((acc, item) => {
75
+ return mergeCatalogs(acc, resolveCoverageMetadata(item));
76
+ }, emptyCatalog());
77
+ const known = mergeCatalogs(knownFromCatalog, coveredTargets);
78
+ const missing = subtractCatalog(known, coveredTargets);
79
+ const total = known.features.length + known.states.length + known.components.length;
80
+ const covered = coveredTargets.features.length +
81
+ coveredTargets.states.length +
82
+ coveredTargets.components.length;
83
+ return {
84
+ total,
85
+ covered,
86
+ ratio: total === 0 ? 1 : Number((covered / total).toFixed(4)),
87
+ known,
88
+ coveredTargets,
89
+ missing,
90
+ };
91
+ }
package/dist/index.d.ts CHANGED
@@ -2,5 +2,9 @@ export declare const sdkName = "@lunatest/core";
2
2
  export { LunaProvider } from "./provider/luna-provider.js";
3
3
  export type { LunaProviderOptions } from "./provider/luna-provider.js";
4
4
  export { loadLunaConfig } from "./config/lua-config.js";
5
+ export { buildCoverageSnapshot, resolveCoverageMetadata, } from "./coverage/catalog.js";
6
+ export { createPresetRegistry, getProtocolPreset, getPresetDiagnostics, getWalletPreset, listProtocolPresets, listWalletPresets, validateProtocolPresetSource, validateWalletPresetSource, materializeProtocolPreset, materializeWalletPreset, } from "./presets/registry.js";
7
+ export type { PresetRegistry, PresetRegistryOptions, ProjectPresetSources } from "./presets/registry.js";
8
+ export { loadProjectPresetSources } from "./presets/project-sources.node.js";
5
9
  export { applyInterceptState, createScenarioRuntime, LuaConfigSchema, setRouteMocks, type LuaConfig, type RouteMock, type ScenarioRuntime, } from "./runtime/scenario-runtime.js";
6
10
  export { executeLuaScenario, type ExecuteLuaScenarioInput, type ExecuteLuaScenarioResult, } from "./runner/execute-scenario.js";
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  export const sdkName = "@lunatest/core";
2
2
  export { LunaProvider } from "./provider/luna-provider.js";
3
3
  export { loadLunaConfig } from "./config/lua-config.js";
4
+ export { buildCoverageSnapshot, resolveCoverageMetadata, } from "./coverage/catalog.js";
5
+ export { createPresetRegistry, getProtocolPreset, getPresetDiagnostics, getWalletPreset, listProtocolPresets, listWalletPresets, validateProtocolPresetSource, validateWalletPresetSource, materializeProtocolPreset, materializeWalletPreset, } from "./presets/registry.js";
6
+ export { loadProjectPresetSources } from "./presets/project-sources.node.js";
4
7
  export { applyInterceptState, createScenarioRuntime, LuaConfigSchema, setRouteMocks, } from "./runtime/scenario-runtime.js";
5
8
  export { executeLuaScenario, } from "./runner/execute-scenario.js";
@@ -1,3 +1,4 @@
1
+ import { normalizeAddress } from "@lunatest/contracts";
1
2
  function parseTokenAmountToWeiHex(value) {
2
3
  const [integerPartRaw, fractionalPartRaw = ""] = value.split(".");
3
4
  const integerPart = integerPartRaw === "" ? "0" : integerPartRaw;
@@ -6,9 +7,6 @@ function parseTokenAmountToWeiHex(value) {
6
7
  const fractionalWei = BigInt(fractionalPart || "0");
7
8
  return `0x${(integerWei + fractionalWei).toString(16)}`;
8
9
  }
9
- function normalizeAddress(value) {
10
- return value.toLowerCase();
11
- }
12
10
  function cloneState(state) {
13
11
  return {
14
12
  timeMs: state.timeMs,
@@ -0,0 +1,213 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ createPresetRegistry,
5
+ getPresetDiagnostics,
6
+ getProtocolPreset,
7
+ getWalletPreset,
8
+ listProtocolPresets,
9
+ listWalletPresets,
10
+ materializeProtocolPreset,
11
+ materializeWalletPreset,
12
+ } from "../registry";
13
+ import { loadProjectPresetSources } from "../project-sources.node";
14
+ import { writeFile, mkdir } from "node:fs/promises";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+
18
+ describe("preset registry", () => {
19
+ it("lists built-in protocol and wallet presets", async () => {
20
+ const [protocols, wallets] = await Promise.all([
21
+ listProtocolPresets(),
22
+ listWalletPresets(),
23
+ ]);
24
+
25
+ expect(protocols.map((item) => item.qualifiedId)).toEqual(
26
+ expect.arrayContaining([
27
+ "builtin/uniswap_v2",
28
+ "builtin/uniswap_v3",
29
+ "builtin/curve",
30
+ "builtin/aave",
31
+ ]),
32
+ );
33
+ expect(wallets.map((item) => item.qualifiedId)).toEqual(
34
+ expect.arrayContaining(["builtin/empty_wallet", "builtin/demo_sepolia"]),
35
+ );
36
+ });
37
+
38
+ it("loads protocol and wallet preset metadata by id", async () => {
39
+ await expect(getProtocolPreset("uniswap_v3")).resolves.toMatchObject({
40
+ id: "uniswap_v3",
41
+ qualifiedId: "builtin/uniswap_v3",
42
+ components: {
43
+ quoter: "v2",
44
+ },
45
+ });
46
+
47
+ await expect(getWalletPreset("demo_sepolia")).resolves.toMatchObject({
48
+ id: "demo_sepolia",
49
+ qualifiedId: "builtin/demo_sepolia",
50
+ kind: "wallet",
51
+ });
52
+ });
53
+
54
+ it("materializes wallet preset deterministically", async () => {
55
+ const first = await materializeWalletPreset("demo_sepolia", {
56
+ address: "0x1111111111111111111111111111111111111111",
57
+ chainId: 11155111,
58
+ });
59
+ const second = await materializeWalletPreset("demo_sepolia", {
60
+ address: "0x1111111111111111111111111111111111111111",
61
+ chainId: 11155111,
62
+ });
63
+
64
+ expect(first).toEqual(second);
65
+ });
66
+
67
+ it("materializes uniswap v3 with component override", async () => {
68
+ const v1 = await materializeProtocolPreset("uniswap_v3", {
69
+ chainId: 11155111,
70
+ quoter: "v1",
71
+ });
72
+ const v2 = await materializeProtocolPreset("uniswap_v3", {
73
+ chainId: 11155111,
74
+ quoter: "v2",
75
+ });
76
+
77
+ expect(v1.interceptState).not.toEqual(v2.interceptState);
78
+ expect(v1.walletPresetId).toBe("builtin/demo_sepolia");
79
+ });
80
+
81
+ it("rejects unsupported protocol chain", async () => {
82
+ await expect(
83
+ materializeProtocolPreset("curve", {
84
+ chainId: 11155111,
85
+ }),
86
+ ).rejects.toThrow(/does not support chain/);
87
+ });
88
+
89
+ it("discovers project-local preset sources with namespace separation", async () => {
90
+ const tempRoot = await fsMkdtemp();
91
+ await mkdir(path.join(tempRoot, "lunatest/presets/protocol"), { recursive: true });
92
+ await mkdir(path.join(tempRoot, "lunatest/presets/wallet"), { recursive: true });
93
+
94
+ await writeFile(
95
+ path.join(tempRoot, "lunatest/presets/protocol/team_swap.lua"),
96
+ `return {
97
+ manifest = {
98
+ id = "team_swap",
99
+ label = "Team Swap",
100
+ kind = "dex",
101
+ supportedChains = { 11155111 },
102
+ protocol = "teamdex",
103
+ version = "v1",
104
+ components = { quoter = "local" },
105
+ defaultWalletPreset = { id = "project/team_wallet" },
106
+ defaultInterceptState = {},
107
+ defaultRouteMocks = {},
108
+ builtinScenarios = {},
109
+ paramsSchema = {},
110
+ recommendedControls = {},
111
+ },
112
+ materialize = function()
113
+ return {
114
+ walletPreset = { id = "project/team_wallet" },
115
+ interceptState = { protocol = { id = "team_swap" } },
116
+ }
117
+ end,
118
+ }`,
119
+ );
120
+
121
+ await writeFile(
122
+ path.join(tempRoot, "lunatest/presets/wallet/team_wallet.lua"),
123
+ `return {
124
+ manifest = {
125
+ id = "team_wallet",
126
+ label = "Team Wallet",
127
+ kind = "wallet",
128
+ supportedChains = { 11155111 },
129
+ defaultSession = {
130
+ chainId = "0xaa36a7",
131
+ accounts = { "0x1111111111111111111111111111111111111111" },
132
+ permissions = {},
133
+ assets = { nativeBalance = "1", tokens = {} },
134
+ },
135
+ },
136
+ materialize = function()
137
+ return {}
138
+ end,
139
+ }`,
140
+ );
141
+
142
+ const projectSources = await loadProjectPresetSources(tempRoot);
143
+ const registry = createPresetRegistry({ projectSources });
144
+ const protocols = await listProtocolPresets(registry);
145
+ const wallets = await listWalletPresets(registry);
146
+
147
+ expect(protocols.map((item) => item.qualifiedId)).toEqual(
148
+ expect.arrayContaining(["project/team_swap", "builtin/uniswap_v3"]),
149
+ );
150
+ expect(wallets.map((item) => item.qualifiedId)).toEqual(
151
+ expect.arrayContaining(["project/team_wallet", "builtin/demo_sepolia"]),
152
+ );
153
+ });
154
+
155
+ it("collects diagnostics for malformed local presets and keeps catalog healthy", async () => {
156
+ const tempRoot = await fsMkdtemp();
157
+ await mkdir(path.join(tempRoot, "lunatest/presets/protocol"), { recursive: true });
158
+
159
+ await writeFile(
160
+ path.join(tempRoot, "lunatest/presets/protocol/bad_swap.lua"),
161
+ `return {
162
+ manifest = {
163
+ id = "bad_swap",
164
+ label = "Bad Swap",
165
+ kind = "dex",
166
+ supportedChains = { 11155111 },
167
+ protocol = "teamdex",
168
+ version = "v1",
169
+ components = { quoter = "local" },
170
+ defaultWalletPreset = { id = "missing_wallet" },
171
+ defaultInterceptState = {},
172
+ defaultRouteMocks = {},
173
+ builtinScenarios = {},
174
+ paramsSchema = {
175
+ { key = "tokenIn", label = "Token In", type = "address" },
176
+ },
177
+ recommendedControls = { "tokenOut" },
178
+ },
179
+ materialize = function()
180
+ return {}
181
+ end,
182
+ }`,
183
+ );
184
+
185
+ const registry = createPresetRegistry({
186
+ projectSources: await loadProjectPresetSources(tempRoot),
187
+ });
188
+
189
+ const protocols = await listProtocolPresets(registry);
190
+ const diagnostics = await getPresetDiagnostics(registry);
191
+
192
+ expect(protocols.map((item) => item.qualifiedId)).not.toContain("project/bad_swap");
193
+ expect(diagnostics).toEqual(
194
+ expect.arrayContaining([
195
+ expect.objectContaining({
196
+ code: "preset_recommended_control_unknown",
197
+ qualifiedId: "project/bad_swap",
198
+ source: "project",
199
+ }),
200
+ expect.objectContaining({
201
+ code: "preset_wallet_reference_missing",
202
+ qualifiedId: "project/bad_swap",
203
+ source: "project",
204
+ }),
205
+ ]),
206
+ );
207
+ });
208
+ });
209
+
210
+ async function fsMkdtemp(): Promise<string> {
211
+ const { mkdtemp } = await import("node:fs/promises");
212
+ return mkdtemp(path.join(os.tmpdir(), "lunatest-preset-registry-"));
213
+ }
@@ -0,0 +1,6 @@
1
+ type LuaPresetModule = {
2
+ manifest: unknown;
3
+ materialize: (params?: Record<string, unknown>) => Promise<unknown>;
4
+ };
5
+ export declare function loadLuaPresetModule(source: string | URL): Promise<LuaPresetModule>;
6
+ export {};
@@ -0,0 +1,40 @@
1
+ import { asRecord } from "@lunatest/contracts";
2
+ import { readLuaSource } from "../config/read-source.js";
3
+ import { createRuntime } from "../runtime/engine.js";
4
+ function createPresetBootstrap(code) {
5
+ return `
6
+ local __preset_module = (function()
7
+ ${code}
8
+ end)()
9
+ __lunatest_preset_manifest = __preset_module.manifest
10
+ __lunatest_preset_materialize = __preset_module.materialize
11
+ `;
12
+ }
13
+ export async function loadLuaPresetModule(source) {
14
+ const runtime = await createRuntime();
15
+ const code = await readLuaSource(source);
16
+ await runtime.eval(createPresetBootstrap(code));
17
+ const snapshot = await runtime.getState(["__lunatest_preset_manifest"]);
18
+ const manifest = snapshot.__lunatest_preset_manifest;
19
+ if (!asRecord(manifest)) {
20
+ throw new Error("Lua preset must expose manifest table");
21
+ }
22
+ return {
23
+ manifest,
24
+ async materialize(params = {}) {
25
+ const result = await runtime.call("__lunatest_preset_materialize", params);
26
+ if (!asRecord(result)) {
27
+ throw new Error("Lua preset materialize() must return a table");
28
+ }
29
+ const normalizedResult = result;
30
+ if (Array.isArray(result.routeMocks)) {
31
+ const routeMocks = result.routeMocks.filter((route) => asRecord(route) !== null);
32
+ return {
33
+ ...normalizedResult,
34
+ routeMocks,
35
+ };
36
+ }
37
+ return normalizedResult;
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,58 @@
1
+ import { asRecord, type RouteMock } from "@lunatest/contracts";
2
+
3
+ import { readLuaSource } from "../config/read-source.js";
4
+ import { createRuntime } from "../runtime/engine.js";
5
+
6
+ type LuaPresetModule = {
7
+ manifest: unknown;
8
+ materialize: (params?: Record<string, unknown>) => Promise<unknown>;
9
+ };
10
+
11
+ function createPresetBootstrap(code: string): string {
12
+ return `
13
+ local __preset_module = (function()
14
+ ${code}
15
+ end)()
16
+ __lunatest_preset_manifest = __preset_module.manifest
17
+ __lunatest_preset_materialize = __preset_module.materialize
18
+ `;
19
+ }
20
+
21
+ export async function loadLuaPresetModule(source: string | URL): Promise<LuaPresetModule> {
22
+ const runtime = await createRuntime();
23
+ const code = await readLuaSource(source);
24
+
25
+ await runtime.eval(createPresetBootstrap(code));
26
+
27
+ const snapshot = await runtime.getState(["__lunatest_preset_manifest"]);
28
+ const manifest = snapshot.__lunatest_preset_manifest;
29
+
30
+ if (!asRecord(manifest)) {
31
+ throw new Error("Lua preset must expose manifest table");
32
+ }
33
+
34
+ return {
35
+ manifest,
36
+ async materialize(params: Record<string, unknown> = {}) {
37
+ const result = await runtime.call("__lunatest_preset_materialize", params);
38
+
39
+ if (!asRecord(result)) {
40
+ throw new Error("Lua preset materialize() must return a table");
41
+ }
42
+
43
+ const normalizedResult = result as Record<string, unknown>;
44
+
45
+ if (Array.isArray((result as { routeMocks?: unknown }).routeMocks)) {
46
+ const routeMocks = (result as { routeMocks: unknown[] }).routeMocks.filter(
47
+ (route): route is RouteMock => asRecord(route) !== null,
48
+ );
49
+ return {
50
+ ...normalizedResult,
51
+ routeMocks,
52
+ };
53
+ }
54
+
55
+ return normalizedResult;
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectPresetSources } from "./registry.js";
2
+ export declare function loadProjectPresetSources(projectRoot: string): Promise<ProjectPresetSources>;
@@ -0,0 +1,44 @@
1
+ async function readPresetDir(root, bucket, baseDir = root) {
2
+ const { readdir } = await import("node:fs/promises");
3
+ const path = await import("node:path");
4
+ const entries = await readdir(root, { withFileTypes: true });
5
+ for (const entry of entries) {
6
+ const absolutePath = path.join(root, entry.name);
7
+ if (entry.isDirectory()) {
8
+ await readPresetDir(absolutePath, bucket, baseDir);
9
+ continue;
10
+ }
11
+ if (!entry.isFile() || !entry.name.endsWith(".lua")) {
12
+ continue;
13
+ }
14
+ const relativePath = path.relative(baseDir, absolutePath).replace(/\\/g, "/");
15
+ const id = relativePath.replace(/\.lua$/u, "");
16
+ bucket[id] = absolutePath;
17
+ }
18
+ }
19
+ export async function loadProjectPresetSources(projectRoot) {
20
+ const path = await import("node:path");
21
+ const fs = await import("node:fs/promises");
22
+ const protocolRoot = path.join(projectRoot, "lunatest", "presets", "protocol");
23
+ const walletRoot = path.join(projectRoot, "lunatest", "presets", "wallet");
24
+ const protocol = {};
25
+ const wallet = {};
26
+ try {
27
+ await fs.access(protocolRoot);
28
+ await readPresetDir(protocolRoot, protocol);
29
+ }
30
+ catch {
31
+ // ignore missing local protocol preset directory
32
+ }
33
+ try {
34
+ await fs.access(walletRoot);
35
+ await readPresetDir(walletRoot, wallet);
36
+ }
37
+ catch {
38
+ // ignore missing local wallet preset directory
39
+ }
40
+ return {
41
+ protocol,
42
+ wallet,
43
+ };
44
+ }