miaoda-game-devkit 0.2.12 → 0.2.13

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.
@@ -4,6 +4,73 @@
4
4
  var import_vitest = require("@testing-library/jest-dom/vitest");
5
5
  var import_react = require("@testing-library/react");
6
6
  var import_vitest2 = require("vitest");
7
+
8
+ // src/testing/jsdom-canvas.ts
9
+ var contexts = /* @__PURE__ */ new WeakMap();
10
+ function createCanvasContext(canvas) {
11
+ const imageData = (width = 1, height = 1) => ({
12
+ data: new Uint8ClampedArray(width * height * 4),
13
+ width,
14
+ height,
15
+ colorSpace: "srgb"
16
+ });
17
+ const state = {
18
+ canvas,
19
+ font: "16px sans-serif"
20
+ };
21
+ const context = new Proxy(state, {
22
+ get(target, property) {
23
+ if (property in target) return target[property];
24
+ if (property === "createImageData" || property === "getImageData") {
25
+ return (...args) => imageData(args.at(-2), args.at(-1));
26
+ }
27
+ if (property === "measureText") {
28
+ return (value) => {
29
+ const font = String(target.font ?? "16px sans-serif");
30
+ const size = Number.parseFloat(font.match(/([\d.]+)px/)?.[1] ?? "16");
31
+ const width = [...String(value)].reduce((total, character) => {
32
+ if (/\s/.test(character)) return total + size * 0.33;
33
+ if (/[^\u0000-\u00ff]/.test(character)) return total + size;
34
+ if (/[MW@#%&]/.test(character)) return total + size * 0.9;
35
+ if (/[ilI1.,'`]/.test(character)) return total + size * 0.32;
36
+ if (/[A-Z0-9]/.test(character)) return total + size * 0.64;
37
+ return total + size * 0.56;
38
+ }, 0);
39
+ return {
40
+ width,
41
+ actualBoundingBoxAscent: size * 0.8,
42
+ actualBoundingBoxDescent: size * 0.2
43
+ };
44
+ };
45
+ }
46
+ if (property === "createLinearGradient" || property === "createRadialGradient") {
47
+ return () => ({ addColorStop() {
48
+ } });
49
+ }
50
+ return () => void 0;
51
+ }
52
+ });
53
+ return context;
54
+ }
55
+ function installJSDOMCanvasContext() {
56
+ HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
57
+ if (contextId !== "2d") return null;
58
+ const existing = contexts.get(this);
59
+ if (existing) return existing;
60
+ const context = createCanvasContext(this);
61
+ contexts.set(this, context);
62
+ return context;
63
+ };
64
+ }
65
+
66
+ // src/testing/jsdom-storage.ts
67
+ function clearJSDOMStorage() {
68
+ window.localStorage.clear();
69
+ window.sessionStorage.clear();
70
+ }
71
+
72
+ // src/react-vitest-setup.ts
73
+ installJSDOMCanvasContext();
7
74
  var failures = [];
8
75
  var consoleError;
9
76
  var onError = (event) => failures.push(event.error ?? event.message);
@@ -22,6 +89,7 @@ var onRejection = (event) => failures.push(event.reason);
22
89
  } catch (error) {
23
90
  failures.push(error);
24
91
  } finally {
92
+ clearJSDOMStorage();
25
93
  window.removeEventListener("error", onError);
26
94
  window.removeEventListener("unhandledrejection", onRejection);
27
95
  consoleError?.mockRestore();
@@ -30,6 +98,9 @@ var onRejection = (event) => failures.push(event.reason);
30
98
  if (failures.length > 0) {
31
99
  const captured = failures;
32
100
  failures = [];
33
- throw new AggregateError(captured, "Unexpected React/JSDOM runtime failures");
101
+ throw new AggregateError(
102
+ captured,
103
+ "Unexpected React/JSDOM runtime failures"
104
+ );
34
105
  }
35
106
  });
@@ -2,6 +2,73 @@
2
2
  import "@testing-library/jest-dom/vitest";
3
3
  import { cleanup } from "@testing-library/react";
4
4
  import { afterEach, beforeEach, vi } from "vitest";
5
+
6
+ // src/testing/jsdom-canvas.ts
7
+ var contexts = /* @__PURE__ */ new WeakMap();
8
+ function createCanvasContext(canvas) {
9
+ const imageData = (width = 1, height = 1) => ({
10
+ data: new Uint8ClampedArray(width * height * 4),
11
+ width,
12
+ height,
13
+ colorSpace: "srgb"
14
+ });
15
+ const state = {
16
+ canvas,
17
+ font: "16px sans-serif"
18
+ };
19
+ const context = new Proxy(state, {
20
+ get(target, property) {
21
+ if (property in target) return target[property];
22
+ if (property === "createImageData" || property === "getImageData") {
23
+ return (...args) => imageData(args.at(-2), args.at(-1));
24
+ }
25
+ if (property === "measureText") {
26
+ return (value) => {
27
+ const font = String(target.font ?? "16px sans-serif");
28
+ const size = Number.parseFloat(font.match(/([\d.]+)px/)?.[1] ?? "16");
29
+ const width = [...String(value)].reduce((total, character) => {
30
+ if (/\s/.test(character)) return total + size * 0.33;
31
+ if (/[^\u0000-\u00ff]/.test(character)) return total + size;
32
+ if (/[MW@#%&]/.test(character)) return total + size * 0.9;
33
+ if (/[ilI1.,'`]/.test(character)) return total + size * 0.32;
34
+ if (/[A-Z0-9]/.test(character)) return total + size * 0.64;
35
+ return total + size * 0.56;
36
+ }, 0);
37
+ return {
38
+ width,
39
+ actualBoundingBoxAscent: size * 0.8,
40
+ actualBoundingBoxDescent: size * 0.2
41
+ };
42
+ };
43
+ }
44
+ if (property === "createLinearGradient" || property === "createRadialGradient") {
45
+ return () => ({ addColorStop() {
46
+ } });
47
+ }
48
+ return () => void 0;
49
+ }
50
+ });
51
+ return context;
52
+ }
53
+ function installJSDOMCanvasContext() {
54
+ HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
55
+ if (contextId !== "2d") return null;
56
+ const existing = contexts.get(this);
57
+ if (existing) return existing;
58
+ const context = createCanvasContext(this);
59
+ contexts.set(this, context);
60
+ return context;
61
+ };
62
+ }
63
+
64
+ // src/testing/jsdom-storage.ts
65
+ function clearJSDOMStorage() {
66
+ window.localStorage.clear();
67
+ window.sessionStorage.clear();
68
+ }
69
+
70
+ // src/react-vitest-setup.ts
71
+ installJSDOMCanvasContext();
5
72
  var failures = [];
6
73
  var consoleError;
7
74
  var onError = (event) => failures.push(event.error ?? event.message);
@@ -20,6 +87,7 @@ afterEach(() => {
20
87
  } catch (error) {
21
88
  failures.push(error);
22
89
  } finally {
90
+ clearJSDOMStorage();
23
91
  window.removeEventListener("error", onError);
24
92
  window.removeEventListener("unhandledrejection", onRejection);
25
93
  consoleError?.mockRestore();
@@ -28,6 +96,9 @@ afterEach(() => {
28
96
  if (failures.length > 0) {
29
97
  const captured = failures;
30
98
  failures = [];
31
- throw new AggregateError(captured, "Unexpected React/JSDOM runtime failures");
99
+ throw new AggregateError(
100
+ captured,
101
+ "Unexpected React/JSDOM runtime failures"
102
+ );
32
103
  }
33
104
  });
@@ -627,6 +627,12 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${tests.filter((test) => test.metadata).le
627
627
  }
628
628
  };
629
629
 
630
+ // src/testing/vitest-node-args.ts
631
+ function getJSDOMWorkerExecArgv() {
632
+ const nodeMajor = Number.parseInt(process.versions.node, 10);
633
+ return nodeMajor >= 25 ? ["--no-experimental-webstorage"] : [];
634
+ }
635
+
630
636
  // src/vite.ts
631
637
  var import_node_fs2 = require("fs");
632
638
  var import_node_module = require("module");
@@ -674,7 +680,10 @@ function phaserRexUIScenePlugin(projectRoot) {
674
680
  var PHASER_FACADE_DEPENDENCY = /miaoda-game-devkit[\\/]dist[\\/]phaser-facade\.mjs$/;
675
681
  var REXUI_DEPENDENCY = /phaser4-rex-plugins/;
676
682
  function defineGameVitestConfig(options) {
677
- const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
683
+ const auditIssues = validateGameplayAuditOptions(
684
+ options.gameplayAudit,
685
+ options.projectRoot
686
+ );
678
687
  if (auditIssues.length > 0) {
679
688
  throw new Error(
680
689
  `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
@@ -696,6 +705,7 @@ function defineGameVitestConfig(options) {
696
705
  },
697
706
  test: {
698
707
  include: ["tests/**/*.test.ts"],
708
+ execArgv: getJSDOMWorkerExecArgv(),
699
709
  testTimeout: options.testTimeout,
700
710
  hookTimeout: options.hookTimeout,
701
711
  server: {
@@ -721,6 +731,11 @@ function defineGameVitestConfig(options) {
721
731
  "miaoda-game-devkit/vitest-setup",
722
732
  ...options.additionalSetupFiles ?? []
723
733
  ],
734
+ sequence: {
735
+ // Vitest 默认并行执行多个 setup file;这里保证项目 setup
736
+ // 始终在 devkit 完成环境边界后运行。
737
+ setupFiles: "list"
738
+ },
724
739
  reporters: [
725
740
  // Vitest's minimal reporter is optimized for AI output: passing tests
726
741
  // stay quiet while failed tests retain their useful error message.
@@ -603,6 +603,12 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${tests.filter((test) => test.metadata).le
603
603
  }
604
604
  };
605
605
 
606
+ // src/testing/vitest-node-args.ts
607
+ function getJSDOMWorkerExecArgv() {
608
+ const nodeMajor = Number.parseInt(process.versions.node, 10);
609
+ return nodeMajor >= 25 ? ["--no-experimental-webstorage"] : [];
610
+ }
611
+
606
612
  // src/vite.ts
607
613
  import { existsSync } from "fs";
608
614
  import { createRequire } from "module";
@@ -650,7 +656,10 @@ function phaserRexUIScenePlugin(projectRoot) {
650
656
  var PHASER_FACADE_DEPENDENCY = /miaoda-game-devkit[\\/]dist[\\/]phaser-facade\.mjs$/;
651
657
  var REXUI_DEPENDENCY = /phaser4-rex-plugins/;
652
658
  function defineGameVitestConfig(options) {
653
- const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
659
+ const auditIssues = validateGameplayAuditOptions(
660
+ options.gameplayAudit,
661
+ options.projectRoot
662
+ );
654
663
  if (auditIssues.length > 0) {
655
664
  throw new Error(
656
665
  `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
@@ -672,6 +681,7 @@ function defineGameVitestConfig(options) {
672
681
  },
673
682
  test: {
674
683
  include: ["tests/**/*.test.ts"],
684
+ execArgv: getJSDOMWorkerExecArgv(),
675
685
  testTimeout: options.testTimeout,
676
686
  hookTimeout: options.hookTimeout,
677
687
  server: {
@@ -697,6 +707,11 @@ function defineGameVitestConfig(options) {
697
707
  "miaoda-game-devkit/vitest-setup",
698
708
  ...options.additionalSetupFiles ?? []
699
709
  ],
710
+ sequence: {
711
+ // Vitest 默认并行执行多个 setup file;这里保证项目 setup
712
+ // 始终在 devkit 完成环境边界后运行。
713
+ setupFiles: "list"
714
+ },
700
715
  reporters: [
701
716
  // Vitest's minimal reporter is optimized for AI output: passing tests
702
717
  // stay quiet while failed tests retain their useful error message.
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.12",
4
- "description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
3
+ "version": "0.2.13",
4
+ "description": "Shared React and Phaser game lint plus deterministic testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
8
8
  "types": "./dist/index.d.ts",
9
9
  "bin": {
10
- "miaoda-game-lint": "bin/miaoda-game-lint.js"
10
+ "miaoda-phaser-game-lint": "bin/miaoda-phaser-game-lint.js",
11
+ "miaoda-react-game-lint": "bin/miaoda-react-game-lint.js"
11
12
  },
12
13
  "exports": {
13
14
  ".": {
@@ -82,6 +83,8 @@
82
83
  "oxlint-config.json",
83
84
  "tsconfig-base.json",
84
85
  "!dist/**/*.map",
86
+ "!dist/lint/*.contract.mjs",
87
+ "!dist/lint/contracts.config.mjs",
85
88
  "!dist/lint/**/*.test.*"
86
89
  ],
87
90
  "nx": {
@@ -105,7 +108,7 @@
105
108
  "build"
106
109
  ],
107
110
  "options": {
108
- "command": "./bin/miaoda-game-lint.js --contracts-only",
111
+ "command": "vitest run --config dist/lint/contracts.config.mjs",
109
112
  "cwd": "packages/game-devkit"
110
113
  }
111
114
  }
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- require('../dist/cli/lint.js');
package/dist/cli/lint.js DELETED
@@ -1,127 +0,0 @@
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 contractsOnly = process.argv.slice(2).includes("--contracts-only");
14
- var projectBiomeConfig = (0, import_node_path.join)(projectRoot, "biome.json");
15
- var biomeConfig = (0, import_node_fs.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path.join)(devkitRoot, "biome-config.json");
16
- function resolveBin(packageName, binName) {
17
- const packageJsonPath = requireFromDevkit.resolve(`${packageName}/package.json`);
18
- const packageJson = JSON.parse((0, import_node_fs.readFileSync)(packageJsonPath, "utf8"));
19
- const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
20
- if (!relativeBin) {
21
- throw new Error(`${packageName} does not expose the ${binName} executable`);
22
- }
23
- return (0, import_node_path.resolve)((0, import_node_path.dirname)(packageJsonPath), relativeBin);
24
- }
25
- function run(name, packageName, binName, args) {
26
- return new Promise((resolveResult) => {
27
- const child = (0, import_node_child_process.spawn)(process.execPath, [resolveBin(packageName, binName), ...args], {
28
- cwd: projectRoot,
29
- env: process.env,
30
- stdio: "inherit"
31
- });
32
- child.once("error", (error) => {
33
- console.error(`[${name}] ${error.message}`);
34
- resolveResult({ name, ok: false });
35
- });
36
- child.once("close", (code, signal) => {
37
- if (signal) {
38
- console.error(`[${name}] terminated by ${signal}`);
39
- }
40
- resolveResult({ name, ok: code === 0 && signal === null });
41
- });
42
- });
43
- }
44
- async function runContracts() {
45
- return run(
46
- "vitest",
47
- "vitest",
48
- "vitest",
49
- ["run", "--config", (0, import_node_path.join)(devkitRoot, "dist/lint/contracts.config.mjs")]
50
- );
51
- }
52
- function checkGameViteConfig() {
53
- const viteConfigPath = (0, import_node_path.join)(projectRoot, "vite.config.ts");
54
- if (!(0, import_node_fs.existsSync)(viteConfigPath)) {
55
- console.error(
56
- "[vite-config] \u7F3A\u5C11\u6A21\u677F\u5FC5\u9700\u7684 vite.config.ts\u3002\u8BF7\u6062\u590D\u6A21\u677F\u539F\u6587\u4EF6\u540E\u91CD\u65B0\u8FD0\u884C pnpm lint\u3002"
57
- );
58
- return { name: "vite-config", ok: false };
59
- }
60
- const source = (0, import_node_fs.readFileSync)(viteConfigPath, "utf8");
61
- const hasDevkitImport = /from\s+["']miaoda-game-devkit\/vite["']/.test(source);
62
- const hasConfigCall = /defineGameViteConfig\s*\(/.test(source);
63
- if (!hasDevkitImport || !hasConfigCall) {
64
- console.error(
65
- "[vite-config] vite.config.ts \u672A\u4F7F\u7528\u6A21\u677F\u89C4\u5B9A\u7684\u914D\u7F6E\u5165\u53E3\u3002\n\u8BF7\u4ECE miaoda-game-devkit/vite \u5BFC\u5165 defineGameViteConfig\uFF0C\u5E76\u7528 defineGameViteConfig({ ... }) \u5305\u4F4F\u5F53\u524D\u7684\u914D\u7F6E\u5BF9\u8C61\u3002\n\u53EA\u66FF\u6362\u914D\u7F6E\u51FD\u6570\uFF0C\u4FDD\u7559\u73B0\u6709 plugins\u3001resolve \u7B49\u5BF9\u8C61\u5185\u5BB9\u4E0D\u53D8\u3002"
66
- );
67
- return { name: "vite-config", ok: false };
68
- }
69
- return { name: "vite-config", ok: true };
70
- }
71
- function projectUsesPhaser4() {
72
- const manifestPath = (0, import_node_path.join)(projectRoot, "package.json");
73
- if (!(0, import_node_fs.existsSync)(manifestPath)) return false;
74
- try {
75
- const manifest = JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
76
- const version = manifest.dependencies?.phaser ?? manifest.devDependencies?.phaser;
77
- return typeof version === "string" && /(?:^|[^0-9])4(?:\.|$)/.test(version);
78
- } catch {
79
- return false;
80
- }
81
- }
82
- async function runAllChecks() {
83
- if (contractsOnly) {
84
- return [await runContracts()];
85
- }
86
- const viteConfig = projectUsesPhaser4() ? [checkGameViteConfig()] : [];
87
- const [tsgo, biome] = await Promise.all([
88
- run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
89
- run("biome", "@biomejs/biome", "biome", [
90
- "lint",
91
- "--config-path",
92
- biomeConfig,
93
- "src"
94
- ])
95
- ]);
96
- const tailwindOutput = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
97
- const tailwind = await run("tailwind", "tailwindcss", "tailwindcss", [
98
- "-i",
99
- (0, import_node_path.join)(projectRoot, "src/index.css"),
100
- "-o",
101
- tailwindOutput
102
- ]);
103
- (0, import_node_fs.rmSync)(tailwindOutput, { force: true });
104
- const oxlint = await run("oxlint", "oxlint", "oxlint", [
105
- "-c",
106
- (0, import_node_path.join)(devkitRoot, "oxlint-config.json"),
107
- "src"
108
- ]);
109
- const contracts = await runContracts();
110
- return [...viteConfig, tsgo, biome, tailwind, oxlint, contracts];
111
- }
112
- async function main() {
113
- const results = await runAllChecks();
114
- const failed = results.filter((result) => !result.ok);
115
- console.log("");
116
- if (failed.length === 0) {
117
- console.log("RESULT: ALL CHECKS PASSED - Finished with 0 errors. Found no issues.");
118
- return;
119
- }
120
- console.error(`RESULT: FAILED - Found errors in: ${failed.map(({ name }) => name).join(", ")}`);
121
- console.error("Fix all lint errors, then re-run 'pnpm lint'.");
122
- process.exitCode = 1;
123
- }
124
- main().catch((error) => {
125
- console.error(error);
126
- process.exitCode = 1;
127
- });
@@ -1,38 +0,0 @@
1
- // src/lint/vitest.config.ts
2
- import { dirname, resolve } from "path";
3
- import { fileURLToPath } from "url";
4
- import { defineConfig } from "vitest/config";
5
- var phaserPackageRoot = dirname(
6
- fileURLToPath(import.meta.resolve("phaser/package.json"))
7
- );
8
- var vitest_config_default = defineConfig({
9
- resolve: {
10
- alias: {
11
- phaser: resolve(phaserPackageRoot, "dist/phaser.js")
12
- }
13
- },
14
- test: {
15
- server: {
16
- deps: {
17
- // 合约文件运行在 Vitest 的 Node worker 中,不能被 Vite
18
- // 按浏览器模块转换。
19
- external: [/miaoda-game-devkit/]
20
- }
21
- },
22
- environment: "jsdom",
23
- environmentOptions: {
24
- jsdom: {
25
- url: "http://localhost/"
26
- }
27
- },
28
- include: [resolve(import.meta.dirname, "*.contract.mjs")],
29
- // 发布后的契约位于使用方的 node_modules 目录中。
30
- exclude: [],
31
- setupFiles: [resolve(import.meta.dirname, "setup.mjs")],
32
- restoreMocks: true,
33
- clearMocks: true
34
- }
35
- });
36
- export {
37
- vitest_config_default as default
38
- };