miaoda-game-devkit 0.2.12 → 0.2.14

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 CHANGED
@@ -8,11 +8,17 @@
8
8
  Vitest 配置和自动收集运行错误的 Testing Library setup,分别从 `miaoda-game-devkit/react`、
9
9
  `miaoda-game-devkit/react/testing` 和 `miaoda-game-devkit/react/vitest-config` 导入;
10
10
 
11
+ - React Vitest 在项目声明 Phaser 3 时使用其 browser bundle,并提供仅供模块能力探测的
12
+ 最小 Canvas 2D 边界,使 Phaser 3 代码不会阻断普通 JSDOM 测试;Storage 使用 JSDOM
13
+ 按 origin 提供的原生实现,Node 25+ worker 会关闭冲突的 Node Web Storage,
14
+ 并在每个用例后清理;该 Canvas 适配不执行真实绘制,
15
+ 也不提供 Canvas/WebGL 像素证明;
16
+
11
17
  - 支持 DOM 鼠标、键盘和触摸输入的确定性 Phaser 无头测试宿主;
12
18
  - 不依赖渲染器的运行时、交互区域和文字健康检查;
13
- - 由模板平台维护的通用 Vitest 契约测试;
14
- - 统一的 `miaoda-game-lint` 命令,聚合 TypeScript、Biome、Tailwind、
15
- Oxlint 和 Phaser 通用契约检查。
19
+ - 只在 devkit 自身 CI 中运行、不会发布到业务项目的通用 Vitest 契约测试;
20
+ - 显式隔离的 `miaoda-react-game-lint` `miaoda-phaser-game-lint` 命令,
21
+ 共享 TypeScript、Biome、Tailwind Oxlint 检查,同时分别校验 React 与 Phaser 4 配置边界。
16
22
  - 统一的 `defineGameVitestConfig` 配置工厂,固定 Phaser 无头测试依赖的基础配置,
17
23
  同时允许每个游戏声明 Scene 审计范围,并配置额外 alias、setup 文件和超时。
18
24
  - 与 Vite `defineConfig` 同签名的 `defineGameViteConfig` 配置工厂,统一维护生产 Scene
@@ -42,21 +48,25 @@ HEADLESS host,并在 `onTestFinished` 校验 Ledger。`vitest.config.ts` 默
42
48
  基线、失败、证据不足、Scene 未访问和 registry 不一致。旧项目仍可提供显式
43
49
  `gameplayAudit.contracts` 清单,但新游戏通常不需要。
44
50
 
45
- 模板直接调用开发工具包提供的命令行程序:
51
+ 两套模板直接调用各自的命令行程序:
46
52
 
47
53
  ```json
48
54
  {
49
55
  "scripts": {
50
- "lint": "miaoda-game-lint"
56
+ "lint": "miaoda-react-game-lint"
51
57
  }
52
58
  }
53
59
  ```
54
60
 
61
+ Phaser 4 模板对应使用 `miaoda-phaser-game-lint`。命令类型不通过依赖猜测,因此 React
62
+ 模板即使预装 Phaser 3 及相关插件,也不会解析 Phaser 或运行 Phaser 4 检查。旧的
63
+ `miaoda-game-lint` 不再发布,避免在同时包含 React 与 Phaser 依赖的项目中产生歧义。
64
+
55
65
  开发工具包自行维护工具版本,并从自身依赖中解析可执行文件,使用方不需要重复声明
56
66
  Biome、Oxlint 或 tsgo。仓库中保留稳定的命令入口,因此 `dist/` 尚未生成时,pnpm
57
67
  也能正确建立命令链接;工作区开发状态下由 Nx 在模板检查前构建开发工具包,发布包则
58
- 直接携带运行时 `dist/`。开发工具包内部的 `*.test.*` 测试不会进入 npm 包;随包运行的
59
- 通用契约测试以 `*.contract.mjs` 形式发布。
68
+ 直接携带运行时 `dist/`。开发工具包内部的 `*.test.*` 与构建后的 `*.contract.mjs` 都不会
69
+ 进入 npm 包;这些契约只由 devkit 自己的测试目标执行。
60
70
 
61
71
  monorepo 中的模板可以放置只包含 `extends: ["miaoda-game-devkit/biome"]` 的薄配置,
62
72
  用于让 Biome 从模板 package.json 判断依赖声明。规则本身仍由本开发工具包维护;生成后的
@@ -64,7 +74,7 @@ monorepo 中的模板可以放置只包含 `extends: ["miaoda-game-devkit/biome"
64
74
 
65
75
  模板的 `tsconfig.json` 应继承 `miaoda-game-devkit/tsconfig-base`,只保留源码范围和路径
66
76
  别名等项目差异。TypeScript 严格模式、bundler 模块解析和无输出检查等平台基线由开发
67
- 工具包集中维护,`miaoda-game-lint` 会直接检查模板的这个配置。
77
+ 工具包集中维护,两套模板专用 lint CLI 会直接检查模板的这个配置。
68
78
 
69
79
  Phaser4 模板的 `vite.config.ts` 应从 `miaoda-game-devkit/vite` 导入 `defineGameViteConfig`,调用方式
70
80
  与 Vite 的 `defineConfig` 相同,支持配置对象、Promise、同步配置函数和异步配置函数:
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('../dist/cli/phaser-lint.js');
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('../dist/cli/react-lint.js');
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli/lint.ts
5
+ var import_node_child_process = require("child_process");
6
+ var import_node_fs = require("fs");
7
+ var import_node_module = require("module");
8
+ var import_node_os = require("os");
9
+ var import_node_path = require("path");
10
+ var devkitRoot = (0, import_node_path.resolve)(__dirname, "../..");
11
+ var projectRoot = process.cwd();
12
+ var requireFromDevkit = (0, import_node_module.createRequire)((0, import_node_path.join)(devkitRoot, "package.json"));
13
+ var projectBiomeConfig = (0, import_node_path.join)(projectRoot, "biome.json");
14
+ var biomeConfig = (0, import_node_fs.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path.join)(devkitRoot, "biome-config.json");
15
+ function resolveBin(packageName, binName) {
16
+ const packageJsonPath = requireFromDevkit.resolve(`${packageName}/package.json`);
17
+ const packageJson = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
18
+ const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
19
+ if (!relativeBin) {
20
+ throw new Error(`${packageName} does not expose the ${binName} executable`);
21
+ }
22
+ return (0, import_node_path.resolve)((0, import_node_path.dirname)(packageJsonPath), relativeBin);
23
+ }
24
+ function run(name, packageName, binName, args) {
25
+ return new Promise((resolveResult) => {
26
+ const child = (0, import_node_child_process.spawn)(process.execPath, [resolveBin(packageName, binName), ...args], {
27
+ cwd: projectRoot,
28
+ env: process.env,
29
+ stdio: "inherit"
30
+ });
31
+ child.once("error", (error) => {
32
+ console.error(`[${name}] ${error.message}`);
33
+ resolveResult({ name, ok: false });
34
+ });
35
+ child.once("close", (code, signal) => {
36
+ if (signal) {
37
+ console.error(`[${name}] terminated by ${signal}`);
38
+ }
39
+ resolveResult({ name, ok: code === 0 && signal === null });
40
+ });
41
+ });
42
+ }
43
+ function readProjectSource(path, checkName) {
44
+ const absolutePath = (0, import_node_path.join)(projectRoot, path);
45
+ if (!(0, import_node_fs.existsSync)(absolutePath)) {
46
+ console.error(`[${checkName}] \u7F3A\u5C11\u6A21\u677F\u5FC5\u9700\u7684 ${path}\u3002\u8BF7\u6062\u590D\u6A21\u677F\u539F\u6587\u4EF6\u540E\u91CD\u65B0\u8FD0\u884C pnpm lint\u3002`);
47
+ return void 0;
48
+ }
49
+ return (0, import_node_fs.readFileSync)(absolutePath, "utf8");
50
+ }
51
+ function readProjectManifest() {
52
+ const manifestPath = (0, import_node_path.join)(projectRoot, "package.json");
53
+ if (!(0, import_node_fs.existsSync)(manifestPath)) {
54
+ console.error("[phaser-version] \u7F3A\u5C11\u9879\u76EE package.json\u3002");
55
+ return void 0;
56
+ }
57
+ try {
58
+ return JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
59
+ } catch (error) {
60
+ console.error(`[phaser-version] \u65E0\u6CD5\u89E3\u6790 package.json\uFF1A${String(error)}`);
61
+ return void 0;
62
+ }
63
+ }
64
+ function checkPhaser4Dependency() {
65
+ const name = "phaser-version";
66
+ const manifest = readProjectManifest();
67
+ const version = manifest?.dependencies?.phaser ?? manifest?.devDependencies?.phaser;
68
+ const declaresPhaser4 = typeof version === "string" && /(?:^|[\s<>=~^|])4(?:\.|\s|$)/.test(version);
69
+ if (!declaresPhaser4) {
70
+ console.error(
71
+ `[${name}] Phaser \u6A21\u677F\u5FC5\u987B\u663E\u5F0F\u58F0\u660E Phaser 4\uFF1B\u5F53\u524D\u58F0\u660E\u4E3A ${JSON.stringify(version ?? null)}\u3002`
72
+ );
73
+ return { name, ok: false };
74
+ }
75
+ return { name, ok: true };
76
+ }
77
+ function checkPhaserViteConfig() {
78
+ const name = "phaser-vite-config";
79
+ const source = readProjectSource("vite.config.ts", name);
80
+ if (!source) return { name, ok: false };
81
+ const hasDevkitImport = /from\s+["']miaoda-game-devkit\/vite["']/.test(source);
82
+ const hasConfigCall = /defineGameViteConfig\s*\(/.test(source);
83
+ if (!hasDevkitImport || !hasConfigCall) {
84
+ console.error(
85
+ `[${name}] vite.config.ts \u672A\u4F7F\u7528 Phaser \u6A21\u677F\u89C4\u5B9A\u7684\u914D\u7F6E\u5165\u53E3\u3002
86
+ \u8BF7\u4ECE miaoda-game-devkit/vite \u5BFC\u5165 defineGameViteConfig\uFF0C\u5E76\u7528 defineGameViteConfig({ ... }) \u5305\u4F4F\u5F53\u524D\u7684\u914D\u7F6E\u5BF9\u8C61\u3002
87
+ \u53EA\u66FF\u6362\u914D\u7F6E\u51FD\u6570\uFF0C\u4FDD\u7559\u73B0\u6709 plugins\u3001resolve \u7B49\u5BF9\u8C61\u5185\u5BB9\u4E0D\u53D8\u3002`
88
+ );
89
+ return { name, ok: false };
90
+ }
91
+ return { name, ok: true };
92
+ }
93
+ function checkVitestConfig(target) {
94
+ const name = `${target}-vitest-config`;
95
+ const source = readProjectSource("vitest.config.ts", name);
96
+ if (!source) return { name, ok: false };
97
+ const expected = target === "react" ? {
98
+ entry: "miaoda-game-devkit/react/vitest-config",
99
+ factory: "defineReactGameVitestConfig"
100
+ } : {
101
+ entry: "miaoda-game-devkit/vitest-config",
102
+ factory: "defineGameVitestConfig"
103
+ };
104
+ const hasDevkitImport = new RegExp(
105
+ `from\\s+["']${expected.entry.replaceAll("/", "\\/")}["']`
106
+ ).test(source);
107
+ const hasConfigCall = new RegExp(`${expected.factory}\\s*\\(`).test(source);
108
+ if (!hasDevkitImport || !hasConfigCall) {
109
+ console.error(
110
+ `[${name}] vitest.config.ts \u5FC5\u987B\u4ECE ${expected.entry} \u5BFC\u5165\u5E76\u8C03\u7528 ${expected.factory}\u3002`
111
+ );
112
+ return { name, ok: false };
113
+ }
114
+ return { name, ok: true };
115
+ }
116
+ async function runAllChecks(target) {
117
+ const targetChecks = target === "react" ? [checkVitestConfig("react")] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
118
+ const [tsgo, biome] = await Promise.all([
119
+ run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
120
+ run("biome", "@biomejs/biome", "biome", [
121
+ "lint",
122
+ "--config-path",
123
+ biomeConfig,
124
+ "src"
125
+ ])
126
+ ]);
127
+ const tailwindOutput = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
128
+ const tailwind = await run("tailwind", "tailwindcss", "tailwindcss", [
129
+ "-i",
130
+ (0, import_node_path.join)(projectRoot, "src/index.css"),
131
+ "-o",
132
+ tailwindOutput
133
+ ]);
134
+ (0, import_node_fs.rmSync)(tailwindOutput, { force: true });
135
+ const oxlint = await run("oxlint", "oxlint", "oxlint", [
136
+ "-c",
137
+ (0, import_node_path.join)(devkitRoot, "oxlint-config.json"),
138
+ "src"
139
+ ]);
140
+ return [...targetChecks, tsgo, biome, tailwind, oxlint];
141
+ }
142
+ async function runLintCli(target) {
143
+ try {
144
+ const results = await runAllChecks(target);
145
+ const failed = results.filter((result) => !result.ok);
146
+ console.log("");
147
+ if (failed.length === 0) {
148
+ console.log("RESULT: ALL CHECKS PASSED - Finished with 0 errors. Found no issues.");
149
+ return;
150
+ }
151
+ console.error(
152
+ `RESULT: FAILED - Found errors in: ${failed.map(({ name }) => name).join(", ")}`
153
+ );
154
+ console.error("Fix all lint errors, then re-run 'pnpm lint'.");
155
+ process.exitCode = 1;
156
+ } catch (error) {
157
+ console.error(error);
158
+ process.exitCode = 1;
159
+ }
160
+ }
161
+
162
+ // src/cli/phaser-lint.ts
163
+ void runLintCli("phaser");
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli/lint.ts
5
+ var import_node_child_process = require("child_process");
6
+ var import_node_fs = require("fs");
7
+ var import_node_module = require("module");
8
+ var import_node_os = require("os");
9
+ var import_node_path = require("path");
10
+ var devkitRoot = (0, import_node_path.resolve)(__dirname, "../..");
11
+ var projectRoot = process.cwd();
12
+ var requireFromDevkit = (0, import_node_module.createRequire)((0, import_node_path.join)(devkitRoot, "package.json"));
13
+ var projectBiomeConfig = (0, import_node_path.join)(projectRoot, "biome.json");
14
+ var biomeConfig = (0, import_node_fs.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path.join)(devkitRoot, "biome-config.json");
15
+ function resolveBin(packageName, binName) {
16
+ const packageJsonPath = requireFromDevkit.resolve(`${packageName}/package.json`);
17
+ const packageJson = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
18
+ const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
19
+ if (!relativeBin) {
20
+ throw new Error(`${packageName} does not expose the ${binName} executable`);
21
+ }
22
+ return (0, import_node_path.resolve)((0, import_node_path.dirname)(packageJsonPath), relativeBin);
23
+ }
24
+ function run(name, packageName, binName, args) {
25
+ return new Promise((resolveResult) => {
26
+ const child = (0, import_node_child_process.spawn)(process.execPath, [resolveBin(packageName, binName), ...args], {
27
+ cwd: projectRoot,
28
+ env: process.env,
29
+ stdio: "inherit"
30
+ });
31
+ child.once("error", (error) => {
32
+ console.error(`[${name}] ${error.message}`);
33
+ resolveResult({ name, ok: false });
34
+ });
35
+ child.once("close", (code, signal) => {
36
+ if (signal) {
37
+ console.error(`[${name}] terminated by ${signal}`);
38
+ }
39
+ resolveResult({ name, ok: code === 0 && signal === null });
40
+ });
41
+ });
42
+ }
43
+ function readProjectSource(path, checkName) {
44
+ const absolutePath = (0, import_node_path.join)(projectRoot, path);
45
+ if (!(0, import_node_fs.existsSync)(absolutePath)) {
46
+ console.error(`[${checkName}] \u7F3A\u5C11\u6A21\u677F\u5FC5\u9700\u7684 ${path}\u3002\u8BF7\u6062\u590D\u6A21\u677F\u539F\u6587\u4EF6\u540E\u91CD\u65B0\u8FD0\u884C pnpm lint\u3002`);
47
+ return void 0;
48
+ }
49
+ return (0, import_node_fs.readFileSync)(absolutePath, "utf8");
50
+ }
51
+ function readProjectManifest() {
52
+ const manifestPath = (0, import_node_path.join)(projectRoot, "package.json");
53
+ if (!(0, import_node_fs.existsSync)(manifestPath)) {
54
+ console.error("[phaser-version] \u7F3A\u5C11\u9879\u76EE package.json\u3002");
55
+ return void 0;
56
+ }
57
+ try {
58
+ return JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
59
+ } catch (error) {
60
+ console.error(`[phaser-version] \u65E0\u6CD5\u89E3\u6790 package.json\uFF1A${String(error)}`);
61
+ return void 0;
62
+ }
63
+ }
64
+ function checkPhaser4Dependency() {
65
+ const name = "phaser-version";
66
+ const manifest = readProjectManifest();
67
+ const version = manifest?.dependencies?.phaser ?? manifest?.devDependencies?.phaser;
68
+ const declaresPhaser4 = typeof version === "string" && /(?:^|[\s<>=~^|])4(?:\.|\s|$)/.test(version);
69
+ if (!declaresPhaser4) {
70
+ console.error(
71
+ `[${name}] Phaser \u6A21\u677F\u5FC5\u987B\u663E\u5F0F\u58F0\u660E Phaser 4\uFF1B\u5F53\u524D\u58F0\u660E\u4E3A ${JSON.stringify(version ?? null)}\u3002`
72
+ );
73
+ return { name, ok: false };
74
+ }
75
+ return { name, ok: true };
76
+ }
77
+ function checkPhaserViteConfig() {
78
+ const name = "phaser-vite-config";
79
+ const source = readProjectSource("vite.config.ts", name);
80
+ if (!source) return { name, ok: false };
81
+ const hasDevkitImport = /from\s+["']miaoda-game-devkit\/vite["']/.test(source);
82
+ const hasConfigCall = /defineGameViteConfig\s*\(/.test(source);
83
+ if (!hasDevkitImport || !hasConfigCall) {
84
+ console.error(
85
+ `[${name}] vite.config.ts \u672A\u4F7F\u7528 Phaser \u6A21\u677F\u89C4\u5B9A\u7684\u914D\u7F6E\u5165\u53E3\u3002
86
+ \u8BF7\u4ECE miaoda-game-devkit/vite \u5BFC\u5165 defineGameViteConfig\uFF0C\u5E76\u7528 defineGameViteConfig({ ... }) \u5305\u4F4F\u5F53\u524D\u7684\u914D\u7F6E\u5BF9\u8C61\u3002
87
+ \u53EA\u66FF\u6362\u914D\u7F6E\u51FD\u6570\uFF0C\u4FDD\u7559\u73B0\u6709 plugins\u3001resolve \u7B49\u5BF9\u8C61\u5185\u5BB9\u4E0D\u53D8\u3002`
88
+ );
89
+ return { name, ok: false };
90
+ }
91
+ return { name, ok: true };
92
+ }
93
+ function checkVitestConfig(target) {
94
+ const name = `${target}-vitest-config`;
95
+ const source = readProjectSource("vitest.config.ts", name);
96
+ if (!source) return { name, ok: false };
97
+ const expected = target === "react" ? {
98
+ entry: "miaoda-game-devkit/react/vitest-config",
99
+ factory: "defineReactGameVitestConfig"
100
+ } : {
101
+ entry: "miaoda-game-devkit/vitest-config",
102
+ factory: "defineGameVitestConfig"
103
+ };
104
+ const hasDevkitImport = new RegExp(
105
+ `from\\s+["']${expected.entry.replaceAll("/", "\\/")}["']`
106
+ ).test(source);
107
+ const hasConfigCall = new RegExp(`${expected.factory}\\s*\\(`).test(source);
108
+ if (!hasDevkitImport || !hasConfigCall) {
109
+ console.error(
110
+ `[${name}] vitest.config.ts \u5FC5\u987B\u4ECE ${expected.entry} \u5BFC\u5165\u5E76\u8C03\u7528 ${expected.factory}\u3002`
111
+ );
112
+ return { name, ok: false };
113
+ }
114
+ return { name, ok: true };
115
+ }
116
+ async function runAllChecks(target) {
117
+ const targetChecks = target === "react" ? [checkVitestConfig("react")] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
118
+ const [tsgo, biome] = await Promise.all([
119
+ run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
120
+ run("biome", "@biomejs/biome", "biome", [
121
+ "lint",
122
+ "--config-path",
123
+ biomeConfig,
124
+ "src"
125
+ ])
126
+ ]);
127
+ const tailwindOutput = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
128
+ const tailwind = await run("tailwind", "tailwindcss", "tailwindcss", [
129
+ "-i",
130
+ (0, import_node_path.join)(projectRoot, "src/index.css"),
131
+ "-o",
132
+ tailwindOutput
133
+ ]);
134
+ (0, import_node_fs.rmSync)(tailwindOutput, { force: true });
135
+ const oxlint = await run("oxlint", "oxlint", "oxlint", [
136
+ "-c",
137
+ (0, import_node_path.join)(devkitRoot, "oxlint-config.json"),
138
+ "src"
139
+ ]);
140
+ return [...targetChecks, tsgo, biome, tailwind, oxlint];
141
+ }
142
+ async function runLintCli(target) {
143
+ try {
144
+ const results = await runAllChecks(target);
145
+ const failed = results.filter((result) => !result.ok);
146
+ console.log("");
147
+ if (failed.length === 0) {
148
+ console.log("RESULT: ALL CHECKS PASSED - Finished with 0 errors. Found no issues.");
149
+ return;
150
+ }
151
+ console.error(
152
+ `RESULT: FAILED - Found errors in: ${failed.map(({ name }) => name).join(", ")}`
153
+ );
154
+ console.error("Fix all lint errors, then re-run 'pnpm lint'.");
155
+ process.exitCode = 1;
156
+ } catch (error) {
157
+ console.error(error);
158
+ process.exitCode = 1;
159
+ }
160
+ }
161
+
162
+ // src/cli/react-lint.ts
163
+ void runLintCli("react");
@@ -1,5 +1,7 @@
1
1
  // src/lint/setup.ts
2
2
  import { afterEach } from "vitest";
3
+
4
+ // src/testing/jsdom-canvas.ts
3
5
  var contexts = /* @__PURE__ */ new WeakMap();
4
6
  function createCanvasContext(canvas) {
5
7
  const imageData = (width = 1, height = 1) => ({
@@ -14,9 +16,7 @@ function createCanvasContext(canvas) {
14
16
  };
15
17
  const context = new Proxy(state, {
16
18
  get(target, property) {
17
- if (property in target) {
18
- return target[property];
19
- }
19
+ if (property in target) return target[property];
20
20
  if (property === "createImageData" || property === "getImageData") {
21
21
  return (...args) => imageData(args.at(-2), args.at(-1));
22
22
  }
@@ -48,18 +48,25 @@ function createCanvasContext(canvas) {
48
48
  });
49
49
  return context;
50
50
  }
51
- HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
52
- if (contextId !== "2d") {
53
- return null;
54
- }
55
- const existing = contexts.get(this);
56
- if (existing) {
57
- return existing;
58
- }
59
- const context = createCanvasContext(this);
60
- contexts.set(this, context);
61
- return context;
62
- };
51
+ function installJSDOMCanvasContext() {
52
+ HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
53
+ if (contextId !== "2d") return null;
54
+ const existing = contexts.get(this);
55
+ if (existing) return existing;
56
+ const context = createCanvasContext(this);
57
+ contexts.set(this, context);
58
+ return context;
59
+ };
60
+ }
61
+
62
+ // src/testing/jsdom-storage.ts
63
+ function clearJSDOMStorage() {
64
+ window.localStorage.clear();
65
+ window.sessionStorage.clear();
66
+ }
67
+
68
+ // src/lint/setup.ts
69
+ installJSDOMCanvasContext();
63
70
  var HeadlessImage = class {
64
71
  onerror = null;
65
72
  onload = null;
@@ -91,32 +98,7 @@ var HeadlessImage = class {
91
98
  }
92
99
  };
93
100
  globalThis.Image = HeadlessImage;
94
- var storageValues = /* @__PURE__ */ new Map();
95
- var headlessStorage = {
96
- get length() {
97
- return storageValues.size;
98
- },
99
- clear() {
100
- storageValues.clear();
101
- },
102
- getItem(key) {
103
- return storageValues.get(key) ?? null;
104
- },
105
- key(index) {
106
- return [...storageValues.keys()][index] ?? null;
107
- },
108
- removeItem(key) {
109
- storageValues.delete(key);
110
- },
111
- setItem(key, value) {
112
- storageValues.set(key, value);
113
- }
114
- };
115
- Object.defineProperty(globalThis, "localStorage", {
116
- configurable: true,
117
- value: headlessStorage
118
- });
119
101
  afterEach(() => {
120
102
  document.body.replaceChildren();
121
- headlessStorage.clear();
103
+ clearJSDOMStorage();
122
104
  });
@@ -23,18 +23,51 @@ __export(react_vitest_config_exports, {
23
23
  defineReactGameVitestConfig: () => defineReactGameVitestConfig
24
24
  });
25
25
  module.exports = __toCommonJS(react_vitest_config_exports);
26
+ var import_node_fs = require("fs");
26
27
  var import_node_path = require("path");
27
28
  var import_config = require("vitest/config");
29
+
30
+ // src/testing/vitest-node-args.ts
31
+ function getJSDOMWorkerExecArgv() {
32
+ const nodeMajor = Number.parseInt(process.versions.node, 10);
33
+ return nodeMajor >= 25 ? ["--no-experimental-webstorage"] : [];
34
+ }
35
+
36
+ // src/react-vitest-config.ts
37
+ function resolvePhaser3BrowserEntry(projectRoot) {
38
+ const manifestPath = (0, import_node_path.resolve)(projectRoot, "node_modules/phaser/package.json");
39
+ if (!(0, import_node_fs.existsSync)(manifestPath)) return void 0;
40
+ try {
41
+ const manifest = JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
42
+ if (!manifest.version?.startsWith("3.")) return void 0;
43
+ const browserEntry = (0, import_node_path.resolve)(
44
+ (0, import_node_path.dirname)(manifestPath),
45
+ manifest.browser ?? "dist/phaser.js"
46
+ );
47
+ return (0, import_node_fs.existsSync)(browserEntry) ? browserEntry : void 0;
48
+ } catch {
49
+ return void 0;
50
+ }
51
+ }
28
52
  function defineReactGameVitestConfig(options) {
53
+ const phaser3BrowserEntry = resolvePhaser3BrowserEntry(options.projectRoot);
29
54
  return (0, import_config.defineConfig)({
30
55
  resolve: {
31
- alias: { ...options.aliases, "@": (0, import_node_path.resolve)(options.projectRoot, "src") }
56
+ alias: {
57
+ ...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},
58
+ ...options.aliases,
59
+ "@": (0, import_node_path.resolve)(options.projectRoot, "src")
60
+ }
32
61
  },
33
62
  test: {
63
+ env: {
64
+ NODE_ENV: "test"
65
+ },
34
66
  include: [
35
67
  "src/**/*.{test,spec}.{ts,tsx}",
36
68
  "tests/**/*.{test,spec}.{ts,tsx}"
37
69
  ],
70
+ execArgv: getJSDOMWorkerExecArgv(),
38
71
  environment: "jsdom",
39
72
  environmentOptions: {
40
73
  jsdom: { url: "http://localhost/", pretendToBeVisual: true }
@@ -43,6 +76,9 @@ function defineReactGameVitestConfig(options) {
43
76
  "miaoda-game-devkit/react/vitest-setup",
44
77
  ...options.additionalSetupFiles ?? []
45
78
  ],
79
+ sequence: {
80
+ setupFiles: "list"
81
+ },
46
82
  reporters: ["minimal"],
47
83
  restoreMocks: true,
48
84
  clearMocks: true,
@@ -1,16 +1,49 @@
1
1
  // src/react-vitest-config.ts
2
- import { resolve } from "path";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { dirname, resolve } from "path";
3
4
  import { defineConfig } from "vitest/config";
5
+
6
+ // src/testing/vitest-node-args.ts
7
+ function getJSDOMWorkerExecArgv() {
8
+ const nodeMajor = Number.parseInt(process.versions.node, 10);
9
+ return nodeMajor >= 25 ? ["--no-experimental-webstorage"] : [];
10
+ }
11
+
12
+ // src/react-vitest-config.ts
13
+ function resolvePhaser3BrowserEntry(projectRoot) {
14
+ const manifestPath = resolve(projectRoot, "node_modules/phaser/package.json");
15
+ if (!existsSync(manifestPath)) return void 0;
16
+ try {
17
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
18
+ if (!manifest.version?.startsWith("3.")) return void 0;
19
+ const browserEntry = resolve(
20
+ dirname(manifestPath),
21
+ manifest.browser ?? "dist/phaser.js"
22
+ );
23
+ return existsSync(browserEntry) ? browserEntry : void 0;
24
+ } catch {
25
+ return void 0;
26
+ }
27
+ }
4
28
  function defineReactGameVitestConfig(options) {
29
+ const phaser3BrowserEntry = resolvePhaser3BrowserEntry(options.projectRoot);
5
30
  return defineConfig({
6
31
  resolve: {
7
- alias: { ...options.aliases, "@": resolve(options.projectRoot, "src") }
32
+ alias: {
33
+ ...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},
34
+ ...options.aliases,
35
+ "@": resolve(options.projectRoot, "src")
36
+ }
8
37
  },
9
38
  test: {
39
+ env: {
40
+ NODE_ENV: "test"
41
+ },
10
42
  include: [
11
43
  "src/**/*.{test,spec}.{ts,tsx}",
12
44
  "tests/**/*.{test,spec}.{ts,tsx}"
13
45
  ],
46
+ execArgv: getJSDOMWorkerExecArgv(),
14
47
  environment: "jsdom",
15
48
  environmentOptions: {
16
49
  jsdom: { url: "http://localhost/", pretendToBeVisual: true }
@@ -19,6 +52,9 @@ function defineReactGameVitestConfig(options) {
19
52
  "miaoda-game-devkit/react/vitest-setup",
20
53
  ...options.additionalSetupFiles ?? []
21
54
  ],
55
+ sequence: {
56
+ setupFiles: "list"
57
+ },
22
58
  reporters: ["minimal"],
23
59
  restoreMocks: true,
24
60
  clearMocks: true,