gamekit777 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.
Files changed (136) hide show
  1. package/README.md +48 -0
  2. package/client/http.ts +253 -0
  3. package/client/index.ts +211 -0
  4. package/client/node.ts +61 -0
  5. package/create-game/scaffold.ts +73 -0
  6. package/create-game/template/CLAUDE.md.tmpl +167 -0
  7. package/create-game/template/_gitignore +4 -0
  8. package/create-game/template/game.meta.json.tmpl +8 -0
  9. package/create-game/template/game.ts.tmpl +57 -0
  10. package/create-game/template/index.html.tmpl +11 -0
  11. package/create-game/template/package.json.tmpl +26 -0
  12. package/create-game/template/scripts/verify.ts.tmpl +79 -0
  13. package/create-game/template/src/assets/critical/.gitkeep +0 -0
  14. package/create-game/template/src/assets/lazy/.gitkeep +0 -0
  15. package/create-game/template/src/assets/manifest.ts +30 -0
  16. package/create-game/template/src/assets/registry.ts +23 -0
  17. package/create-game/template/src/components/App.svelte.tmpl +54 -0
  18. package/create-game/template/src/context.svelte.ts.tmpl +22 -0
  19. package/create-game/template/src/dev.ts +8 -0
  20. package/create-game/template/src/index.ts.tmpl +17 -0
  21. package/create-game/template/src/rules/const.ts +9 -0
  22. package/create-game/template/src/rules/restore.ts.tmpl +39 -0
  23. package/create-game/template/src/rules/schema.ts.tmpl +19 -0
  24. package/create-game/template/src/rules/simulate.ts.tmpl +9 -0
  25. package/create-game/template/src/rules/types.ts.tmpl +13 -0
  26. package/create-game/template/src/state/game.svelte.ts.tmpl +45 -0
  27. package/create-game/template/src/styles/animations.css +4 -0
  28. package/create-game/template/src/styles/global.css +18 -0
  29. package/create-game/template/src/view/bridge.svelte.ts +33 -0
  30. package/create-game/template/src/view/mount.svelte.ts.tmpl +86 -0
  31. package/create-game/template/src/view/present.ts.tmpl +36 -0
  32. package/create-game/template/test/e2e.test.ts.tmpl +81 -0
  33. package/create-game/template/tsconfig.json +22 -0
  34. package/create-game/template/vite.config.ts +4 -0
  35. package/create-game/template/vitest.config.ts +10 -0
  36. package/dev-host/DevShell.svelte +348 -0
  37. package/dev-host/Field.svelte +25 -0
  38. package/dev-host/SchemaFields.svelte +14 -0
  39. package/dev-host/SchemaFieldsPure.svelte +98 -0
  40. package/dev-host/context.svelte.ts +9 -0
  41. package/dev-host/fonts.ts +16 -0
  42. package/dev-host/index.ts +8 -0
  43. package/dev-host/local.ts +69 -0
  44. package/dev-host/platform-server.ts +78 -0
  45. package/dev-host/platform.svelte.ts +277 -0
  46. package/dev-host/remote.ts +178 -0
  47. package/dev-host/run.ts +46 -0
  48. package/dev-host/table-server.ts +194 -0
  49. package/dev-host/theme.css +344 -0
  50. package/lut/book.ts +97 -0
  51. package/lut/csv.ts +37 -0
  52. package/lut/format.ts +106 -0
  53. package/lut/index.ts +4 -0
  54. package/lut/verify.ts +243 -0
  55. package/package.json +58 -0
  56. package/protocol/bets.ts +74 -0
  57. package/protocol/errors.ts +90 -0
  58. package/protocol/games.ts +158 -0
  59. package/protocol/index.ts +9 -0
  60. package/protocol/money.ts +51 -0
  61. package/protocol/rounds.ts +60 -0
  62. package/protocol/seeds.ts +36 -0
  63. package/protocol/session.ts +18 -0
  64. package/protocol/tables.ts +120 -0
  65. package/protocol/verify.ts +33 -0
  66. package/publish/build.ts +168 -0
  67. package/publish/index.ts +6 -0
  68. package/publish/parallel.ts +56 -0
  69. package/publish/sample-worker.ts +31 -0
  70. package/publish/sample.ts +131 -0
  71. package/publish/spec.ts +8 -0
  72. package/publish/table.ts +244 -0
  73. package/publish/upload-core.ts +176 -0
  74. package/publish/upload.ts +30 -0
  75. package/runtime/assets.ts +122 -0
  76. package/runtime/fonts.ts +21 -0
  77. package/runtime/index.ts +3 -0
  78. package/runtime/types.ts +42 -0
  79. package/sdk/contract.ts +51 -0
  80. package/sdk/hash.ts +184 -0
  81. package/sdk/host.ts +96 -0
  82. package/sdk/index.ts +9 -0
  83. package/sdk/rng.ts +109 -0
  84. package/sdk/sample.ts +68 -0
  85. package/sdk/schema.ts +90 -0
  86. package/sdk/spec.ts +255 -0
  87. package/sdk/stage.ts +20 -0
  88. package/sdk/store.ts +67 -0
  89. package/studio/app/App.svelte +113 -0
  90. package/studio/app/lib/api.ts +59 -0
  91. package/studio/app/lib/bus.svelte.ts +44 -0
  92. package/studio/app/lib/upload.ts +34 -0
  93. package/studio/app/main.ts +5 -0
  94. package/studio/app/panels/CasePanel.svelte +83 -0
  95. package/studio/app/panels/LogPanel.svelte +19 -0
  96. package/studio/app/panels/PreviewPanel.svelte +33 -0
  97. package/studio/app/panels/PublishPanel.svelte +111 -0
  98. package/studio/app/panels/TablePanel.svelte +64 -0
  99. package/studio/app/panels/TuningPanel.svelte +140 -0
  100. package/studio/app/virtual.d.ts +6 -0
  101. package/studio/bin.ts +68 -0
  102. package/studio/cli.ts +43 -0
  103. package/studio/preview/bridge.ts +63 -0
  104. package/studio/preview/entry.ts +89 -0
  105. package/studio/preview/virtual.d.ts +6 -0
  106. package/studio/src/api.ts +157 -0
  107. package/studio/src/build.ts +166 -0
  108. package/studio/src/bus.ts +98 -0
  109. package/studio/src/canon.ts +18 -0
  110. package/studio/src/cases.ts +255 -0
  111. package/studio/src/config.ts +39 -0
  112. package/studio/src/engine.ts +218 -0
  113. package/studio/src/fingerprint.ts +39 -0
  114. package/studio/src/game-vite.ts +91 -0
  115. package/studio/src/game.ts +173 -0
  116. package/studio/src/jobs.ts +59 -0
  117. package/studio/src/mcp.ts +357 -0
  118. package/studio/src/probe.ts +359 -0
  119. package/studio/src/scaffold.ts +85 -0
  120. package/studio/src/solver.ts +139 -0
  121. package/studio/src/stats.ts +134 -0
  122. package/studio/src/studio-plugin.ts +76 -0
  123. package/studio/src/tasks.ts +52 -0
  124. package/studio/src/worker/pool.ts +84 -0
  125. package/studio/src/worker/rpc.ts +178 -0
  126. package/vite-config/index.js +207 -0
  127. package/vite-config/index.ts +87 -0
  128. package/vite-config/manifest.ts +144 -0
  129. package/vite-config/meta.ts +41 -0
  130. package/vite-config/namespace-css.ts +74 -0
  131. package/weights/index.ts +11 -0
  132. package/weights/linalg.ts +118 -0
  133. package/weights/report.ts +78 -0
  134. package/weights/solve.ts +236 -0
  135. package/weights/types.ts +73 -0
  136. package/weights/volatility.ts +40 -0
@@ -0,0 +1,45 @@
1
+ /* 单局状态。
2
+
3
+ 设置来自平台(只读投影),结果来自 round(),这里只放「演到哪一步了」。
4
+ 做成可实例化的类而不是模块级单例:同页挂两个实例时各自一套状态。 */
5
+ import type { ViewSettings } from 'gamekit777/sdk';
6
+ import type { __PASCAL__Config, __PASCAL__Outcome } from '../rules/types.ts';
7
+ import { schema } from '../rules/schema.ts';
8
+
9
+ export type Phase = 'idle' | 'playing' | 'over';
10
+
11
+ const DEFAULT_CONFIG: __PASCAL__Config = { side: schema.side.default };
12
+ const DEFAULT_VIEW: ViewSettings = {
13
+ sound: true, volume: 1, turbo: false, reducedMotion: false,
14
+ skin: 'default', locale: 'zh-CN', autoActive: false
15
+ };
16
+
17
+ export class Game {
18
+ #config = $state<__PASCAL__Config>(DEFAULT_CONFIG);
19
+ #view = $state<ViewSettings>(DEFAULT_VIEW);
20
+
21
+ /* 影响结果的参数,来自 host.config */
22
+ get side(): __PASCAL__Config['side'] { return this.#config.side; }
23
+
24
+ /* 不影响结果的表现参数,来自 host.view */
25
+ get turbo(): boolean { return this.#view.turbo; }
26
+ get sound(): boolean { return this.#view.sound; }
27
+ get reducedMotion(): boolean { return this.#view.reducedMotion; }
28
+ get autoActive(): boolean { return this.#view.autoActive; }
29
+
30
+ phase = $state<Phase>('idle');
31
+ /** 作废令牌:开新局时 +1,旧的演出协程醒来发现对不上号就退出 */
32
+ rid = $state(0);
33
+ /** 演出过程中翻动的那一面 */
34
+ face = $state<'heads' | 'tails'>('heads');
35
+ last = $state<__PASCAL__Outcome | null>(null);
36
+ lastPayout = $state(0);
37
+
38
+ live = $derived(this.phase === 'playing');
39
+
40
+ /* 只有 view/mount 的订阅回调会调这两个 */
41
+ applyConfig(c: __PASCAL__Config): void { this.#config = c; }
42
+ applyView(v: ViewSettings): void { this.#view = v; }
43
+
44
+ invalidate(): void { this.rid++; this.phase = 'idle'; }
45
+ }
@@ -0,0 +1,4 @@
1
+ /* 名字起得通用没关系:namespace-css 插件会在构建时把它们改成 <name>-<游戏 id>,
2
+ 同页两个游戏才不会互相覆盖 */
3
+ @keyframes flash{from{opacity:.5}to{opacity:0}}
4
+ @keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.1)}}
@@ -0,0 +1,18 @@
1
+ /* 游戏自己的设计变量和 reset。
2
+
3
+ 构建时 @gamekit/vite-config 的 namespace-css 插件会把这里的 :root / html / body
4
+ 收敛到游戏根元素 .gk-<id>,@keyframes 也会改名。原因是同页可能挂着两个游戏,
5
+ 而 @keyframes 是全局命名空间、后定义覆盖先定义——撞名不报错,只是动画播成别人的。
6
+
7
+ 页面外框(body 的底色、外边距)不放这里:那是平台的地盘。 */
8
+ :root{
9
+ --bg:#0E1116; --panel:#161B23; --line:#232B36;
10
+ --ink:#E8EDF4; --dim:#8A97A8;
11
+ --win:#31D07A; --lose:#F0554A; --mark:#FFC93C;
12
+ --num:ui-monospace,SFMono-Regular,Menlo,monospace;
13
+ --txt:system-ui,-apple-system,"Noto Sans SC",sans-serif;
14
+ }
15
+ *,*::before,*::after{box-sizing:border-box}
16
+ button,input{font:inherit;color:inherit}
17
+ button{cursor:pointer}
18
+ button:disabled{cursor:not-allowed}
@@ -0,0 +1,33 @@
1
+ /* 平台的 store ↔ Svelte runes 的桥。
2
+
3
+ 平台可能是 React / Vue / 原生,所以契约层用的是框架中立的 Readable
4
+ (subscribe 立即同步回调一次、返回退订函数)。这里把它接到 runes 上。 */
5
+ import type { Readable } from 'gamekit777/sdk';
6
+
7
+ /** 外部 store → runes。必须在 $effect.root 或组件内调用 */
8
+ export function fromStore<T>(src: Readable<T>): { readonly value: T } {
9
+ let v = $state(src.get());
10
+ // subscribe 的返回值正好是 cleanup,$effect 会在销毁时调它
11
+ $effect(() => src.subscribe(n => { v = n; }));
12
+ return { get value() { return v; } };
13
+ }
14
+
15
+ /** runes → 外部 store。read 里读 $state/$derived,变化自动推给订阅者 */
16
+ export function toStore<T>(read: () => T): Readable<T> & { dispose(): void } {
17
+ const subs = new Set<(v: T) => void>();
18
+ let first = true;
19
+ const stop = $effect.root(() => {
20
+ $effect(() => {
21
+ const v = read();
22
+ // 首帧的值由 get() 供应,这里再推一次就重复了
23
+ if (first) { first = false; return; }
24
+ for (const f of [...subs]) f(v);
25
+ });
26
+ });
27
+ return {
28
+ // 在 effect 外读 $state 是合法的,只是不建立依赖——get() 正是这种用法
29
+ get: read,
30
+ subscribe(run) { run(read()); subs.add(run); return () => { subs.delete(run); }; },
31
+ dispose: stop
32
+ };
33
+ }
@@ -0,0 +1,86 @@
1
+ /* 把游戏挂起来,并把平台的状态接进来。
2
+
3
+ 放在 .svelte.ts 里是因为要用 $effect.root——status 是从 runes 派生出来
4
+ 推给平台的。 */
5
+ import { mount as svelteMount, unmount } from 'svelte';
6
+ import { ensureFonts, loadAssets, prefetchLazy, type LoadProgress } from 'gamekit777/runtime';
7
+ import type { GameInstance, GameStatus, Host, PresentOptions, Readable, RoundResult } from 'gamekit777/sdk';
8
+ import App from '../components/App.svelte';
9
+ import buildMeta from '../../game.meta.json' with { type: 'json' };
10
+ import { assets } from '../assets/registry.ts';
11
+ import { loadManifest } from '../assets/manifest.ts';
12
+ import { createInstance } from '../context.svelte.ts';
13
+ import type { __PASCAL__Config, __PASCAL__Outcome } from '../rules/types.ts';
14
+ import { present } from './present.ts';
15
+ import { toStore } from './bridge.svelte.ts';
16
+
17
+ export interface MountOptions {
18
+ /** 资源加载进度。平台拿它渲染自己的加载条 */
19
+ onProgress?(p: LoadProgress): void;
20
+ signal?: AbortSignal;
21
+ }
22
+
23
+ export async function mountGame(
24
+ host: Host<__PASCAL__Config>, el: HTMLElement, opts: MountOptions = {}
25
+ ): Promise<GameInstance<__PASCAL__Outcome>> {
26
+ /* 每次 mount 造一棵全新的状态树,同页挂两个实例是安全的 */
27
+ const inst = createInstance(host);
28
+ const { game } = inst;
29
+
30
+ /* CSS 变量和命名空间挂在容器上,不写 document.documentElement:
31
+ 同页两个游戏时写全局会互相覆盖 */
32
+ el.classList.add(`gk-${buildMeta.id}`);
33
+
34
+ const teardown: Array<() => void> = [];
35
+ teardown.push(host.config.subscribe(c => game.applyConfig(c)));
36
+ teardown.push(host.view.subscribe(v => game.applyView(v)));
37
+
38
+ const manifest = await loadManifest(opts.signal);
39
+ const urls = Object.fromEntries(assets.keys.map(k => [k, assets.url(k)]));
40
+ let progress = 0;
41
+ await Promise.all([
42
+ ensureFonts(buildMeta.fonts ?? []),
43
+ loadAssets({ manifest, urls, signal: opts.signal,
44
+ onProgress: p => { progress = p.ratio; opts.onProgress?.(p); } })
45
+ ]);
46
+ progress = 1;
47
+
48
+ const status = toStore<GameStatus>(() => ({
49
+ phase: game.phase === 'playing' ? 'playing' : game.phase === 'over' ? 'settling' : 'idle',
50
+ busy: game.live,
51
+ canBet: !game.live,
52
+ canSkip: game.live,
53
+ /* 和游戏画布内的控件用同一个判断,两边不会打架 */
54
+ canEditConfig: !game.live && !game.autoActive,
55
+ progress,
56
+ lastPayout: game.lastPayout
57
+ }));
58
+ teardown.push(status.dispose);
59
+
60
+ const app = svelteMount(App, { target: el, props: { instance: inst } });
61
+ prefetchLazy(manifest, urls);
62
+ host.emit({ type: 'ready' });
63
+
64
+ const play = async (r: RoundResult<__PASCAL__Outcome>, o?: PresentOptions): Promise<void> => {
65
+ host.emit({ type: 'present:start' });
66
+ await present(game, r.outcome, r.payout, o?.rate ?? 1);
67
+ host.emit({ type: 'present:end', payout: r.payout });
68
+ };
69
+
70
+ return {
71
+ status: status as Readable<GameStatus>,
72
+ present: play,
73
+ /** 回看:有了种子,回看就是拿同一份输入重算一遍再演 */
74
+ preview: play,
75
+ skip() { if (game.live) game.rid++; },
76
+ reset() { game.invalidate(); game.last = null; },
77
+ resize() {},
78
+ destroy() {
79
+ game.invalidate();
80
+ while (teardown.length) teardown.pop()!();
81
+ void unmount(app);
82
+ el.classList.remove(`gk-${buildMeta.id}`);
83
+ el.replaceChildren();
84
+ }
85
+ };
86
+ }
@@ -0,0 +1,36 @@
1
+ /* 演出:把已经算好的结果播出来。
2
+
3
+ 判定一行都不在这里——outcome 是传进来的。turbo / reducedMotion 只改这段的
4
+ 时长,改不了结果,因为它们根本不在 round() 的入参里。 */
5
+ import type { Game } from '../state/game.svelte';
6
+ import type { __PASCAL__Outcome } from '../rules/types.ts';
7
+
8
+ const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
9
+
10
+ export async function present(
11
+ game: Game, o: __PASCAL__Outcome, payout: number, rate = 1
12
+ ): Promise<void> {
13
+ const id = ++game.rid;
14
+ game.phase = 'playing';
15
+ game.last = null;
16
+ game.lastPayout = 0;
17
+
18
+ const instant = game.reducedMotion || rate === Infinity;
19
+ const dur = instant ? 0 : (game.turbo ? 320 : 800) / rate;
20
+
21
+ /* 翻动几下再定格。翻动次数是装饰性的,不占用可复现的随机数 */
22
+ if (dur > 0) {
23
+ const steps = Math.max(2, Math.round(dur / 90));
24
+ for (let i = 0; i < steps; i++) {
25
+ if (game.rid !== id) return;
26
+ game.face = i % 2 === 0 ? 'tails' : 'heads';
27
+ await sleep(dur / steps);
28
+ }
29
+ if (game.rid !== id) return;
30
+ }
31
+
32
+ game.face = o.landed;
33
+ game.last = o;
34
+ game.lastPayout = payout;
35
+ game.phase = 'over';
36
+ }
@@ -0,0 +1,81 @@
1
+ /* 端到端:参考平台 → 本地表模型算钱 → restore 还原 → 游戏演出。
2
+
3
+ 跑在 happy-dom 里,整条链路都真的执行,而不只是「构建通过」。
4
+ 平台来自 gamekit777/dev-host,是泛型的——所有游戏共用同一份。
5
+ 服务端也是真平台那条路:浏览器里就地建表、按权重抽行、restore 还原。 */
6
+ import { test, expect, afterEach } from 'vitest';
7
+ import { tick } from 'svelte';
8
+ import { createPlatform, localServer } from 'gamekit777/dev-host';
9
+ import { defineGame } from 'gamekit777/sdk';
10
+ import game from '../src/index.ts';
11
+ import type { __PASCAL__Config as C, __PASCAL__Outcome as O } from '../src/rules/types.ts';
12
+
13
+ const live: { destroy(): void }[] = [];
14
+ afterEach(() => { while (live.length) { try { live.pop()!.destroy(); } catch { /* 已经拆过了 */ } } });
15
+
16
+ const boot = async () => {
17
+ const p = createPlatform<C, O>(game);
18
+ await p.init();
19
+ const el = document.createElement('div');
20
+ document.body.appendChild(el);
21
+ const inst = await game.mount(p.host(), el);
22
+ live.push(inst);
23
+ inst.status.subscribe(s => p.setGameStatus(s));
24
+ p.bindGame({ present: r => inst.present(r), status: () => p.game });
25
+ return { p, el, inst };
26
+ };
27
+
28
+ test('挂载 → 下注 → 演完 → 余额按 payout 变化', async () => {
29
+ const { p, el } = await boot();
30
+ expect(p.game.phase).toBe('idle');
31
+ expect(el.querySelector('.coin')).toBeTruthy();
32
+
33
+ p.setView({ reducedMotion: true });
34
+ p.setBet(1000);
35
+
36
+ const before = p.balance;
37
+ expect(await p.playOne()).toBeTruthy();
38
+ expect(p.balance).toBe(before - 1000 + (p.game.lastPayout ?? 0));
39
+ });
40
+
41
+ test('平台改配置 → 游戏画布跟着变', async () => {
42
+ const { p, el } = await boot();
43
+ p.requestConfig({ side: 'tails' });
44
+ await tick();
45
+ expect(el.querySelector('.head b')?.textContent).toBe('反');
46
+ });
47
+
48
+ test('表模型:还原出的赔付必须等于平台记账,差一分也要拦住', async () => {
49
+ const srv = localServer<C, O>({ spec: game, sims: 500 });
50
+ const before = (await srv.init()).balance;
51
+ const r = await srv.place({ config: { side: 'heads' }, bet: 1000, clientSeed: 'c' });
52
+ expect(r.ok).toBe(true);
53
+ if (!r.ok) return;
54
+ expect(r.round.result!.payout).toBe((1000 / 100) * r.round.row!.payoutCenti);
55
+ expect(r.round.wallet.balance).toBe(before - 1000 + r.round.result!.payout);
56
+
57
+ const tampered = defineGame<C, O>({ ...game, restore: (i) => { const x = game.restore(i); return { ...x, payout: x.payout + 1 }; } });
58
+ const bad = localServer<C, O>({ spec: tampered, sims: 500 });
59
+ await bad.init();
60
+ const v = await bad.place({ config: { side: 'heads' }, bet: 1000, clientSeed: 'c' });
61
+ expect(v.ok).toBe(false);
62
+ if (!v.ok) expect(v.error).toMatch(/不符/);
63
+ });
64
+
65
+ test('同页两个实例互不干扰', async () => {
66
+ const a = await boot();
67
+ const b = await boot();
68
+ a.p.setView({ reducedMotion: true });
69
+ b.p.setView({ reducedMotion: true });
70
+
71
+ a.p.requestConfig({ side: 'heads' });
72
+ b.p.requestConfig({ side: 'tails' });
73
+ await tick();
74
+ expect(a.el.querySelector('.head b')?.textContent).toBe('正');
75
+ expect(b.el.querySelector('.head b')?.textContent).toBe('反');
76
+
77
+ a.p.setBet(1000);
78
+ await a.p.playOne();
79
+ expect(b.p.balance).toBe(100_000); // b 的钱包没被动过
80
+ expect(b.p.game.phase).toBe('idle');
81
+ });
@@ -0,0 +1,22 @@
1
+ {
2
+ "extends": "@tsconfig/svelte/tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "moduleResolution": "bundler",
8
+ "strict": true,
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "noImplicitOverride": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "noEmit": true,
14
+ "isolatedModules": true,
15
+ "verbatimModuleSyntax": true,
16
+ "skipLibCheck": true,
17
+ "forceConsistentCasingInFileNames": true,
18
+ "allowImportingTsExtensions": true,
19
+ "types": ["vite/client"]
20
+ },
21
+ "include": ["src", "test", "scripts", "*.ts"]
22
+ }
@@ -0,0 +1,4 @@
1
+ import { defineGameConfig } from 'gamekit777/vite-config';
2
+ import meta from './game.meta.json' with { type: 'json' };
3
+
4
+ export default defineGameConfig({ meta });
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import { svelte } from '@sveltejs/vite-plugin-svelte';
3
+
4
+ /* 测试必须走 Vite 管线:assets/registry.ts 用了 import.meta.glob(Vite 特性),
5
+ 组件和 .svelte.ts 里的 runes 也要 Svelte 编译器处理。 */
6
+ export default defineConfig({
7
+ plugins: [svelte({ hot: false })],
8
+ test: { environment: 'happy-dom', include: ['test/**/*.test.ts'], testTimeout: 30_000 },
9
+ resolve: { conditions: ['browser'] }
10
+ });