miaoda-game-devkit 0.6.1 → 0.6.3
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/dist/cli/phaser-lint.js +187 -19
- package/dist/cli/react-lint.js +187 -19
- package/dist/react/index.js +11 -1
- package/dist/react/index.mjs +11 -1
- package/dist/react/testing.d.mts +19 -2
- package/dist/react/testing.d.ts +19 -2
- package/dist/react/testing.js +177 -31
- package/dist/react/testing.mjs +177 -31
- package/dist/react/vitest-config.js +342 -65
- package/dist/react/vitest-config.mjs +342 -65
- package/dist/react/vitest-setup.js +87 -4
- package/dist/react/vitest-setup.mjs +87 -4
- package/dist/rules/react-test-boundary-plugin.js +14 -1
- package/package.json +1 -1
package/dist/cli/phaser-lint.js
CHANGED
|
@@ -3,23 +3,185 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli/lint.ts
|
|
5
5
|
var import_node_child_process = require("child_process");
|
|
6
|
-
var
|
|
6
|
+
var import_node_fs2 = require("fs");
|
|
7
7
|
var import_node_module = require("module");
|
|
8
8
|
var import_node_os = require("os");
|
|
9
|
+
var import_node_path2 = require("path");
|
|
10
|
+
|
|
11
|
+
// src/cli/react-authoritative-playthrough.ts
|
|
12
|
+
var import_node_fs = require("fs");
|
|
9
13
|
var import_node_path = require("path");
|
|
10
|
-
var
|
|
14
|
+
var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
|
|
15
|
+
var REACT_RUNTIME_ENTRY = "miaoda-game-devkit/react";
|
|
16
|
+
var REACT_TESTING_ENTRY = "miaoda-game-devkit/react/testing";
|
|
17
|
+
var CLOCK_IMPORTS = /* @__PURE__ */ new Set(["browserGameClock"]);
|
|
18
|
+
var CONTROLLER_IMPORTS = /* @__PURE__ */ new Set([
|
|
19
|
+
"useGameController",
|
|
20
|
+
"useOwnedGameController"
|
|
21
|
+
]);
|
|
22
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
23
|
+
function extension(path) {
|
|
24
|
+
const index = path.lastIndexOf(".");
|
|
25
|
+
return index < 0 ? "" : path.slice(index);
|
|
26
|
+
}
|
|
27
|
+
function sourceFiles(root, directory = (0, import_node_path.join)(root, "src")) {
|
|
28
|
+
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
29
|
+
const files = [];
|
|
30
|
+
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
31
|
+
const path = (0, import_node_path.join)(directory, entry.name);
|
|
32
|
+
const projectPath = (0, import_node_path.relative)(root, path).replaceAll("\\", "/");
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
if (projectPath === "src/game/example") continue;
|
|
35
|
+
files.push(...sourceFiles(root, path));
|
|
36
|
+
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(extension(entry.name))) {
|
|
37
|
+
files.push(path);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
function withoutComments(source) {
|
|
43
|
+
let output = "";
|
|
44
|
+
let state = "code";
|
|
45
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
46
|
+
const char = source[index];
|
|
47
|
+
const next = source[index + 1];
|
|
48
|
+
if (state === "line") {
|
|
49
|
+
if (char === "\n") {
|
|
50
|
+
state = "code";
|
|
51
|
+
output += char;
|
|
52
|
+
} else {
|
|
53
|
+
output += " ";
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (state === "block") {
|
|
58
|
+
if (char === "*" && next === "/") {
|
|
59
|
+
output += " ";
|
|
60
|
+
index += 1;
|
|
61
|
+
state = "code";
|
|
62
|
+
} else {
|
|
63
|
+
output += char === "\n" ? "\n" : " ";
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (state === "code" && char === "/" && next === "/") {
|
|
68
|
+
output += " ";
|
|
69
|
+
index += 1;
|
|
70
|
+
state = "line";
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (state === "code" && char === "/" && next === "*") {
|
|
74
|
+
output += " ";
|
|
75
|
+
index += 1;
|
|
76
|
+
state = "block";
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (state === "code" && char === "'") state = "single";
|
|
80
|
+
else if (state === "code" && char === '"') state = "double";
|
|
81
|
+
else if (state === "code" && char === "`") state = "template";
|
|
82
|
+
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
83
|
+
state = "code";
|
|
84
|
+
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
85
|
+
state = "code";
|
|
86
|
+
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
87
|
+
state = "code";
|
|
88
|
+
}
|
|
89
|
+
output += char;
|
|
90
|
+
}
|
|
91
|
+
return output;
|
|
92
|
+
}
|
|
93
|
+
function codePositions(source) {
|
|
94
|
+
const positions = Array.from({ length: source.length }, () => false);
|
|
95
|
+
let state = "code";
|
|
96
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
97
|
+
const char = source[index];
|
|
98
|
+
if (state === "code") positions[index] = true;
|
|
99
|
+
if (state === "code" && char === "'") state = "single";
|
|
100
|
+
else if (state === "code" && char === '"') state = "double";
|
|
101
|
+
else if (state === "code" && char === "`") state = "template";
|
|
102
|
+
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
103
|
+
state = "code";
|
|
104
|
+
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
105
|
+
state = "code";
|
|
106
|
+
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
107
|
+
state = "code";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return positions;
|
|
111
|
+
}
|
|
112
|
+
function namedImports(source, moduleName) {
|
|
113
|
+
const names = /* @__PURE__ */ new Set();
|
|
114
|
+
const clean = withoutComments(source);
|
|
115
|
+
const positions = codePositions(clean);
|
|
116
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
117
|
+
const pattern = new RegExp(
|
|
118
|
+
`^\\s*import\\s+(?:type\\s+)?\\{([\\s\\S]*?)\\}\\s+from\\s+["']${escapedModule}["']`,
|
|
119
|
+
"gm"
|
|
120
|
+
);
|
|
121
|
+
for (const match of clean.matchAll(pattern)) {
|
|
122
|
+
if (!positions[match.index]) continue;
|
|
123
|
+
for (const specifier of match[1].split(",")) {
|
|
124
|
+
const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
|
|
125
|
+
if (imported) names.add(imported);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return names;
|
|
129
|
+
}
|
|
130
|
+
function containsAny(values, expected) {
|
|
131
|
+
return [...values].some((value) => expected.has(value));
|
|
132
|
+
}
|
|
133
|
+
function declaresObserve(source) {
|
|
134
|
+
const clean = withoutComments(source);
|
|
135
|
+
const positions = codePositions(clean);
|
|
136
|
+
for (const match of clean.matchAll(/\bobserve\s*:/g)) {
|
|
137
|
+
if (positions[match.index]) return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
142
|
+
const clockFiles = [];
|
|
143
|
+
const controllerFiles = [];
|
|
144
|
+
for (const file of sourceFiles(projectRoot2)) {
|
|
145
|
+
const imports = namedImports((0, import_node_fs.readFileSync)(file, "utf8"), REACT_RUNTIME_ENTRY);
|
|
146
|
+
const projectPath = (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/");
|
|
147
|
+
if (containsAny(imports, CLOCK_IMPORTS)) clockFiles.push(projectPath);
|
|
148
|
+
if (containsAny(imports, CONTROLLER_IMPORTS)) controllerFiles.push(projectPath);
|
|
149
|
+
}
|
|
150
|
+
const issues = [];
|
|
151
|
+
if (clockFiles.length === 0 && controllerFiles.length === 0) {
|
|
152
|
+
return { ok: true, issues, clockFiles, controllerFiles };
|
|
153
|
+
}
|
|
154
|
+
const testPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_PLAYTHROUGH);
|
|
155
|
+
const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
|
|
156
|
+
const testingImports = namedImports(testSource, REACT_TESTING_ENTRY);
|
|
157
|
+
const hasObserve = declaresObserve(testSource);
|
|
158
|
+
if (!hasObserve) {
|
|
159
|
+
issues.push(
|
|
160
|
+
`${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Observe the same production Controller rendered by <App /> through Telemetry; DOM labels are not an authoritative gameplay boundary.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (clockFiles.length > 0 && !testingImports.has("ManualGameClock")) {
|
|
164
|
+
issues.push(
|
|
165
|
+
`${PRODUCTION_PLAYTHROUGH} must import ManualGameClock because production owns gameplay time in ${clockFiles.join(", ")}. Inject it through the production <App /> factory and advance it with a non-empty deterministic step.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return { ok: issues.length === 0, issues, clockFiles, controllerFiles };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/cli/lint.ts
|
|
172
|
+
var devkitRoot = (0, import_node_path2.resolve)(__dirname, "../..");
|
|
11
173
|
var projectRoot = process.cwd();
|
|
12
|
-
var requireFromDevkit = (0, import_node_module.createRequire)((0,
|
|
13
|
-
var projectBiomeConfig = (0,
|
|
14
|
-
var biomeConfig = (0,
|
|
174
|
+
var requireFromDevkit = (0, import_node_module.createRequire)((0, import_node_path2.join)(devkitRoot, "package.json"));
|
|
175
|
+
var projectBiomeConfig = (0, import_node_path2.join)(projectRoot, "biome.json");
|
|
176
|
+
var biomeConfig = (0, import_node_fs2.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path2.join)(devkitRoot, "biome-config.json");
|
|
15
177
|
function resolveBin(packageName, binName) {
|
|
16
178
|
const packageJsonPath = requireFromDevkit.resolve(`${packageName}/package.json`);
|
|
17
|
-
const packageJson = JSON.parse((0,
|
|
179
|
+
const packageJson = JSON.parse((0, import_node_fs2.readFileSync)(packageJsonPath, "utf8"));
|
|
18
180
|
const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
|
|
19
181
|
if (!relativeBin) {
|
|
20
182
|
throw new Error(`${packageName} does not expose the ${binName} executable`);
|
|
21
183
|
}
|
|
22
|
-
return (0,
|
|
184
|
+
return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(packageJsonPath), relativeBin);
|
|
23
185
|
}
|
|
24
186
|
function run(name, packageName, binName, args) {
|
|
25
187
|
return new Promise((resolveResult) => {
|
|
@@ -41,21 +203,21 @@ function run(name, packageName, binName, args) {
|
|
|
41
203
|
});
|
|
42
204
|
}
|
|
43
205
|
function readProjectSource(path, checkName) {
|
|
44
|
-
const absolutePath = (0,
|
|
45
|
-
if (!(0,
|
|
206
|
+
const absolutePath = (0, import_node_path2.join)(projectRoot, path);
|
|
207
|
+
if (!(0, import_node_fs2.existsSync)(absolutePath)) {
|
|
46
208
|
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
209
|
return void 0;
|
|
48
210
|
}
|
|
49
|
-
return (0,
|
|
211
|
+
return (0, import_node_fs2.readFileSync)(absolutePath, "utf8");
|
|
50
212
|
}
|
|
51
213
|
function readProjectManifest() {
|
|
52
|
-
const manifestPath = (0,
|
|
53
|
-
if (!(0,
|
|
214
|
+
const manifestPath = (0, import_node_path2.join)(projectRoot, "package.json");
|
|
215
|
+
if (!(0, import_node_fs2.existsSync)(manifestPath)) {
|
|
54
216
|
console.error("[phaser-version] \u7F3A\u5C11\u9879\u76EE package.json\u3002");
|
|
55
217
|
return void 0;
|
|
56
218
|
}
|
|
57
219
|
try {
|
|
58
|
-
return JSON.parse((0,
|
|
220
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
|
|
59
221
|
} catch (error) {
|
|
60
222
|
console.error(`[phaser-version] \u65E0\u6CD5\u89E3\u6790 package.json\uFF1A${String(error)}`);
|
|
61
223
|
return void 0;
|
|
@@ -113,9 +275,15 @@ function checkVitestConfig(target) {
|
|
|
113
275
|
}
|
|
114
276
|
return { name, ok: true };
|
|
115
277
|
}
|
|
278
|
+
function checkReactAuthoritativePlaythrough() {
|
|
279
|
+
const name = "react-authoritative-playthrough";
|
|
280
|
+
const audit = auditReactAuthoritativePlaythrough(projectRoot);
|
|
281
|
+
for (const issue of audit.issues) console.error(`[${name}] ${issue}`);
|
|
282
|
+
return { name, ok: audit.ok };
|
|
283
|
+
}
|
|
116
284
|
async function runAllChecks(target) {
|
|
117
|
-
const targetChecks = target === "react" ? [checkVitestConfig("react")] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
|
|
118
|
-
const lintRoots = target === "react" && (0,
|
|
285
|
+
const targetChecks = target === "react" ? [checkVitestConfig("react"), checkReactAuthoritativePlaythrough()] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
|
|
286
|
+
const lintRoots = target === "react" && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
|
|
119
287
|
const [tsgo, biome] = await Promise.all([
|
|
120
288
|
run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
|
|
121
289
|
run("biome", "@biomejs/biome", "biome", [
|
|
@@ -125,17 +293,17 @@ async function runAllChecks(target) {
|
|
|
125
293
|
...lintRoots
|
|
126
294
|
])
|
|
127
295
|
]);
|
|
128
|
-
const tailwindOutput = (0,
|
|
296
|
+
const tailwindOutput = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
|
|
129
297
|
const tailwind = await run("tailwind", "tailwindcss", "tailwindcss", [
|
|
130
298
|
"-i",
|
|
131
|
-
(0,
|
|
299
|
+
(0, import_node_path2.join)(projectRoot, "src/index.css"),
|
|
132
300
|
"-o",
|
|
133
301
|
tailwindOutput
|
|
134
302
|
]);
|
|
135
|
-
(0,
|
|
303
|
+
(0, import_node_fs2.rmSync)(tailwindOutput, { force: true });
|
|
136
304
|
const oxlint = await run("oxlint", "oxlint", "oxlint", [
|
|
137
305
|
"-c",
|
|
138
|
-
(0,
|
|
306
|
+
(0, import_node_path2.join)(devkitRoot, "oxlint-config.json"),
|
|
139
307
|
...lintRoots
|
|
140
308
|
]);
|
|
141
309
|
return [...targetChecks, tsgo, biome, tailwind, oxlint];
|
package/dist/cli/react-lint.js
CHANGED
|
@@ -3,23 +3,185 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli/lint.ts
|
|
5
5
|
var import_node_child_process = require("child_process");
|
|
6
|
-
var
|
|
6
|
+
var import_node_fs2 = require("fs");
|
|
7
7
|
var import_node_module = require("module");
|
|
8
8
|
var import_node_os = require("os");
|
|
9
|
+
var import_node_path2 = require("path");
|
|
10
|
+
|
|
11
|
+
// src/cli/react-authoritative-playthrough.ts
|
|
12
|
+
var import_node_fs = require("fs");
|
|
9
13
|
var import_node_path = require("path");
|
|
10
|
-
var
|
|
14
|
+
var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
|
|
15
|
+
var REACT_RUNTIME_ENTRY = "miaoda-game-devkit/react";
|
|
16
|
+
var REACT_TESTING_ENTRY = "miaoda-game-devkit/react/testing";
|
|
17
|
+
var CLOCK_IMPORTS = /* @__PURE__ */ new Set(["browserGameClock"]);
|
|
18
|
+
var CONTROLLER_IMPORTS = /* @__PURE__ */ new Set([
|
|
19
|
+
"useGameController",
|
|
20
|
+
"useOwnedGameController"
|
|
21
|
+
]);
|
|
22
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
23
|
+
function extension(path) {
|
|
24
|
+
const index = path.lastIndexOf(".");
|
|
25
|
+
return index < 0 ? "" : path.slice(index);
|
|
26
|
+
}
|
|
27
|
+
function sourceFiles(root, directory = (0, import_node_path.join)(root, "src")) {
|
|
28
|
+
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
29
|
+
const files = [];
|
|
30
|
+
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
31
|
+
const path = (0, import_node_path.join)(directory, entry.name);
|
|
32
|
+
const projectPath = (0, import_node_path.relative)(root, path).replaceAll("\\", "/");
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
if (projectPath === "src/game/example") continue;
|
|
35
|
+
files.push(...sourceFiles(root, path));
|
|
36
|
+
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(extension(entry.name))) {
|
|
37
|
+
files.push(path);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
function withoutComments(source) {
|
|
43
|
+
let output = "";
|
|
44
|
+
let state = "code";
|
|
45
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
46
|
+
const char = source[index];
|
|
47
|
+
const next = source[index + 1];
|
|
48
|
+
if (state === "line") {
|
|
49
|
+
if (char === "\n") {
|
|
50
|
+
state = "code";
|
|
51
|
+
output += char;
|
|
52
|
+
} else {
|
|
53
|
+
output += " ";
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (state === "block") {
|
|
58
|
+
if (char === "*" && next === "/") {
|
|
59
|
+
output += " ";
|
|
60
|
+
index += 1;
|
|
61
|
+
state = "code";
|
|
62
|
+
} else {
|
|
63
|
+
output += char === "\n" ? "\n" : " ";
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (state === "code" && char === "/" && next === "/") {
|
|
68
|
+
output += " ";
|
|
69
|
+
index += 1;
|
|
70
|
+
state = "line";
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (state === "code" && char === "/" && next === "*") {
|
|
74
|
+
output += " ";
|
|
75
|
+
index += 1;
|
|
76
|
+
state = "block";
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (state === "code" && char === "'") state = "single";
|
|
80
|
+
else if (state === "code" && char === '"') state = "double";
|
|
81
|
+
else if (state === "code" && char === "`") state = "template";
|
|
82
|
+
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
83
|
+
state = "code";
|
|
84
|
+
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
85
|
+
state = "code";
|
|
86
|
+
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
87
|
+
state = "code";
|
|
88
|
+
}
|
|
89
|
+
output += char;
|
|
90
|
+
}
|
|
91
|
+
return output;
|
|
92
|
+
}
|
|
93
|
+
function codePositions(source) {
|
|
94
|
+
const positions = Array.from({ length: source.length }, () => false);
|
|
95
|
+
let state = "code";
|
|
96
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
97
|
+
const char = source[index];
|
|
98
|
+
if (state === "code") positions[index] = true;
|
|
99
|
+
if (state === "code" && char === "'") state = "single";
|
|
100
|
+
else if (state === "code" && char === '"') state = "double";
|
|
101
|
+
else if (state === "code" && char === "`") state = "template";
|
|
102
|
+
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
103
|
+
state = "code";
|
|
104
|
+
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
105
|
+
state = "code";
|
|
106
|
+
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
107
|
+
state = "code";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return positions;
|
|
111
|
+
}
|
|
112
|
+
function namedImports(source, moduleName) {
|
|
113
|
+
const names = /* @__PURE__ */ new Set();
|
|
114
|
+
const clean = withoutComments(source);
|
|
115
|
+
const positions = codePositions(clean);
|
|
116
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
117
|
+
const pattern = new RegExp(
|
|
118
|
+
`^\\s*import\\s+(?:type\\s+)?\\{([\\s\\S]*?)\\}\\s+from\\s+["']${escapedModule}["']`,
|
|
119
|
+
"gm"
|
|
120
|
+
);
|
|
121
|
+
for (const match of clean.matchAll(pattern)) {
|
|
122
|
+
if (!positions[match.index]) continue;
|
|
123
|
+
for (const specifier of match[1].split(",")) {
|
|
124
|
+
const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
|
|
125
|
+
if (imported) names.add(imported);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return names;
|
|
129
|
+
}
|
|
130
|
+
function containsAny(values, expected) {
|
|
131
|
+
return [...values].some((value) => expected.has(value));
|
|
132
|
+
}
|
|
133
|
+
function declaresObserve(source) {
|
|
134
|
+
const clean = withoutComments(source);
|
|
135
|
+
const positions = codePositions(clean);
|
|
136
|
+
for (const match of clean.matchAll(/\bobserve\s*:/g)) {
|
|
137
|
+
if (positions[match.index]) return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
142
|
+
const clockFiles = [];
|
|
143
|
+
const controllerFiles = [];
|
|
144
|
+
for (const file of sourceFiles(projectRoot2)) {
|
|
145
|
+
const imports = namedImports((0, import_node_fs.readFileSync)(file, "utf8"), REACT_RUNTIME_ENTRY);
|
|
146
|
+
const projectPath = (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/");
|
|
147
|
+
if (containsAny(imports, CLOCK_IMPORTS)) clockFiles.push(projectPath);
|
|
148
|
+
if (containsAny(imports, CONTROLLER_IMPORTS)) controllerFiles.push(projectPath);
|
|
149
|
+
}
|
|
150
|
+
const issues = [];
|
|
151
|
+
if (clockFiles.length === 0 && controllerFiles.length === 0) {
|
|
152
|
+
return { ok: true, issues, clockFiles, controllerFiles };
|
|
153
|
+
}
|
|
154
|
+
const testPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_PLAYTHROUGH);
|
|
155
|
+
const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
|
|
156
|
+
const testingImports = namedImports(testSource, REACT_TESTING_ENTRY);
|
|
157
|
+
const hasObserve = declaresObserve(testSource);
|
|
158
|
+
if (!hasObserve) {
|
|
159
|
+
issues.push(
|
|
160
|
+
`${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Observe the same production Controller rendered by <App /> through Telemetry; DOM labels are not an authoritative gameplay boundary.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (clockFiles.length > 0 && !testingImports.has("ManualGameClock")) {
|
|
164
|
+
issues.push(
|
|
165
|
+
`${PRODUCTION_PLAYTHROUGH} must import ManualGameClock because production owns gameplay time in ${clockFiles.join(", ")}. Inject it through the production <App /> factory and advance it with a non-empty deterministic step.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return { ok: issues.length === 0, issues, clockFiles, controllerFiles };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/cli/lint.ts
|
|
172
|
+
var devkitRoot = (0, import_node_path2.resolve)(__dirname, "../..");
|
|
11
173
|
var projectRoot = process.cwd();
|
|
12
|
-
var requireFromDevkit = (0, import_node_module.createRequire)((0,
|
|
13
|
-
var projectBiomeConfig = (0,
|
|
14
|
-
var biomeConfig = (0,
|
|
174
|
+
var requireFromDevkit = (0, import_node_module.createRequire)((0, import_node_path2.join)(devkitRoot, "package.json"));
|
|
175
|
+
var projectBiomeConfig = (0, import_node_path2.join)(projectRoot, "biome.json");
|
|
176
|
+
var biomeConfig = (0, import_node_fs2.existsSync)(projectBiomeConfig) ? projectBiomeConfig : (0, import_node_path2.join)(devkitRoot, "biome-config.json");
|
|
15
177
|
function resolveBin(packageName, binName) {
|
|
16
178
|
const packageJsonPath = requireFromDevkit.resolve(`${packageName}/package.json`);
|
|
17
|
-
const packageJson = JSON.parse((0,
|
|
179
|
+
const packageJson = JSON.parse((0, import_node_fs2.readFileSync)(packageJsonPath, "utf8"));
|
|
18
180
|
const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
|
|
19
181
|
if (!relativeBin) {
|
|
20
182
|
throw new Error(`${packageName} does not expose the ${binName} executable`);
|
|
21
183
|
}
|
|
22
|
-
return (0,
|
|
184
|
+
return (0, import_node_path2.resolve)((0, import_node_path2.dirname)(packageJsonPath), relativeBin);
|
|
23
185
|
}
|
|
24
186
|
function run(name, packageName, binName, args) {
|
|
25
187
|
return new Promise((resolveResult) => {
|
|
@@ -41,21 +203,21 @@ function run(name, packageName, binName, args) {
|
|
|
41
203
|
});
|
|
42
204
|
}
|
|
43
205
|
function readProjectSource(path, checkName) {
|
|
44
|
-
const absolutePath = (0,
|
|
45
|
-
if (!(0,
|
|
206
|
+
const absolutePath = (0, import_node_path2.join)(projectRoot, path);
|
|
207
|
+
if (!(0, import_node_fs2.existsSync)(absolutePath)) {
|
|
46
208
|
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
209
|
return void 0;
|
|
48
210
|
}
|
|
49
|
-
return (0,
|
|
211
|
+
return (0, import_node_fs2.readFileSync)(absolutePath, "utf8");
|
|
50
212
|
}
|
|
51
213
|
function readProjectManifest() {
|
|
52
|
-
const manifestPath = (0,
|
|
53
|
-
if (!(0,
|
|
214
|
+
const manifestPath = (0, import_node_path2.join)(projectRoot, "package.json");
|
|
215
|
+
if (!(0, import_node_fs2.existsSync)(manifestPath)) {
|
|
54
216
|
console.error("[phaser-version] \u7F3A\u5C11\u9879\u76EE package.json\u3002");
|
|
55
217
|
return void 0;
|
|
56
218
|
}
|
|
57
219
|
try {
|
|
58
|
-
return JSON.parse((0,
|
|
220
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(manifestPath, "utf8"));
|
|
59
221
|
} catch (error) {
|
|
60
222
|
console.error(`[phaser-version] \u65E0\u6CD5\u89E3\u6790 package.json\uFF1A${String(error)}`);
|
|
61
223
|
return void 0;
|
|
@@ -113,9 +275,15 @@ function checkVitestConfig(target) {
|
|
|
113
275
|
}
|
|
114
276
|
return { name, ok: true };
|
|
115
277
|
}
|
|
278
|
+
function checkReactAuthoritativePlaythrough() {
|
|
279
|
+
const name = "react-authoritative-playthrough";
|
|
280
|
+
const audit = auditReactAuthoritativePlaythrough(projectRoot);
|
|
281
|
+
for (const issue of audit.issues) console.error(`[${name}] ${issue}`);
|
|
282
|
+
return { name, ok: audit.ok };
|
|
283
|
+
}
|
|
116
284
|
async function runAllChecks(target) {
|
|
117
|
-
const targetChecks = target === "react" ? [checkVitestConfig("react")] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
|
|
118
|
-
const lintRoots = target === "react" && (0,
|
|
285
|
+
const targetChecks = target === "react" ? [checkVitestConfig("react"), checkReactAuthoritativePlaythrough()] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
|
|
286
|
+
const lintRoots = target === "react" && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
|
|
119
287
|
const [tsgo, biome] = await Promise.all([
|
|
120
288
|
run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
|
|
121
289
|
run("biome", "@biomejs/biome", "biome", [
|
|
@@ -125,17 +293,17 @@ async function runAllChecks(target) {
|
|
|
125
293
|
...lintRoots
|
|
126
294
|
])
|
|
127
295
|
]);
|
|
128
|
-
const tailwindOutput = (0,
|
|
296
|
+
const tailwindOutput = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
|
|
129
297
|
const tailwind = await run("tailwind", "tailwindcss", "tailwindcss", [
|
|
130
298
|
"-i",
|
|
131
|
-
(0,
|
|
299
|
+
(0, import_node_path2.join)(projectRoot, "src/index.css"),
|
|
132
300
|
"-o",
|
|
133
301
|
tailwindOutput
|
|
134
302
|
]);
|
|
135
|
-
(0,
|
|
303
|
+
(0, import_node_fs2.rmSync)(tailwindOutput, { force: true });
|
|
136
304
|
const oxlint = await run("oxlint", "oxlint", "oxlint", [
|
|
137
305
|
"-c",
|
|
138
|
-
(0,
|
|
306
|
+
(0, import_node_path2.join)(devkitRoot, "oxlint-config.json"),
|
|
139
307
|
...lintRoots
|
|
140
308
|
]);
|
|
141
309
|
return [...targetChecks, tsgo, biome, tailwind, oxlint];
|
package/dist/react/index.js
CHANGED
|
@@ -54,6 +54,15 @@ function useOwnedGameController(createController) {
|
|
|
54
54
|
|
|
55
55
|
// src/react/use-game-controller.ts
|
|
56
56
|
var import_react2 = require("react");
|
|
57
|
+
|
|
58
|
+
// src/react/react-error-diagnostics.ts
|
|
59
|
+
function codedError(code, message) {
|
|
60
|
+
const error = new Error(message);
|
|
61
|
+
error.code = code;
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/react/use-game-controller.ts
|
|
57
66
|
function useGameController(createController) {
|
|
58
67
|
const game = useOwnedGameController(createController);
|
|
59
68
|
useSnapshotIntegrityCheck(game);
|
|
@@ -91,7 +100,8 @@ function useSnapshotIntegrityCheck(game) {
|
|
|
91
100
|
}
|
|
92
101
|
if (previousJson !== void 0 && nextJson !== previousJson) {
|
|
93
102
|
reported.current = true;
|
|
94
|
-
throw
|
|
103
|
+
throw codedError(
|
|
104
|
+
"GAME_SNAPSHOT_REFERENCE_REUSED",
|
|
95
105
|
"Game state changed while snapshot() returned the same reference. React compares snapshots by reference and skips the render when Object.is(previous, next) is true, so the UI freezes. Publish a new top-level snapshot before each notification, for example cachedSnapshot = { ...state }, and never expose a mutable internal object as the snapshot."
|
|
96
106
|
);
|
|
97
107
|
}
|
package/dist/react/index.mjs
CHANGED
|
@@ -26,6 +26,15 @@ function useOwnedGameController(createController) {
|
|
|
26
26
|
|
|
27
27
|
// src/react/use-game-controller.ts
|
|
28
28
|
import { useEffect as useEffect2, useRef as useRef2, useSyncExternalStore } from "react";
|
|
29
|
+
|
|
30
|
+
// src/react/react-error-diagnostics.ts
|
|
31
|
+
function codedError(code, message) {
|
|
32
|
+
const error = new Error(message);
|
|
33
|
+
error.code = code;
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/react/use-game-controller.ts
|
|
29
38
|
function useGameController(createController) {
|
|
30
39
|
const game = useOwnedGameController(createController);
|
|
31
40
|
useSnapshotIntegrityCheck(game);
|
|
@@ -63,7 +72,8 @@ function useSnapshotIntegrityCheck(game) {
|
|
|
63
72
|
}
|
|
64
73
|
if (previousJson !== void 0 && nextJson !== previousJson) {
|
|
65
74
|
reported.current = true;
|
|
66
|
-
throw
|
|
75
|
+
throw codedError(
|
|
76
|
+
"GAME_SNAPSHOT_REFERENCE_REUSED",
|
|
67
77
|
"Game state changed while snapshot() returned the same reference. React compares snapshots by reference and skips the render when Object.is(previous, next) is true, so the UI freezes. Publish a new top-level snapshot before each notification, for example cachedSnapshot = { ...state }, and never expose a mutable internal object as the snapshot."
|
|
68
78
|
);
|
|
69
79
|
}
|
package/dist/react/testing.d.mts
CHANGED
|
@@ -22,6 +22,15 @@ declare class ManualGameClock implements GameClock {
|
|
|
22
22
|
pendingTimerCount(): number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
interface ReactFailureEntry {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
interface ReactFailureDiagnostic {
|
|
30
|
+
source: "playthrough" | "test-runtime";
|
|
31
|
+
entries: ReactFailureEntry[];
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
/** 有界推进的通用配置,不绑定任何特定 Core、phase 或时钟实现。 */
|
|
26
35
|
interface StepUntilOptions {
|
|
27
36
|
/** 条件仍不成立时允许执行的最大生产步数,默认 120。 */
|
|
@@ -57,13 +66,21 @@ interface ReactPlaythroughEvidence {
|
|
|
57
66
|
}
|
|
58
67
|
/** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
|
|
59
68
|
interface ReactPlaythroughMetadata {
|
|
60
|
-
version:
|
|
69
|
+
version: 5;
|
|
61
70
|
waiverReason?: string;
|
|
62
71
|
trace?: string;
|
|
72
|
+
failure?: ReactFailureDiagnostic;
|
|
63
73
|
evidence: ReactPlaythroughEvidence;
|
|
64
74
|
}
|
|
65
75
|
interface ReactPlaythroughOptions {
|
|
66
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Read authoritative production gameplay state when the DOM cannot prove the
|
|
78
|
+
* result, such as Canvas, Controller, time, or physics flows. The harness
|
|
79
|
+
* samples this state before and after each stage, not after every step.
|
|
80
|
+
* Individual steps may leave it unchanged, but the completed stage must
|
|
81
|
+
* produce a new gameplay result. Observation supplements real production
|
|
82
|
+
* input; it does not replace it.
|
|
83
|
+
*/
|
|
67
84
|
observe: () => unknown;
|
|
68
85
|
}
|
|
69
86
|
interface ReactPlaythroughAssertContext {
|
package/dist/react/testing.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ declare class ManualGameClock implements GameClock {
|
|
|
22
22
|
pendingTimerCount(): number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
interface ReactFailureEntry {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
interface ReactFailureDiagnostic {
|
|
30
|
+
source: "playthrough" | "test-runtime";
|
|
31
|
+
entries: ReactFailureEntry[];
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
/** 有界推进的通用配置,不绑定任何特定 Core、phase 或时钟实现。 */
|
|
26
35
|
interface StepUntilOptions {
|
|
27
36
|
/** 条件仍不成立时允许执行的最大生产步数,默认 120。 */
|
|
@@ -57,13 +66,21 @@ interface ReactPlaythroughEvidence {
|
|
|
57
66
|
}
|
|
58
67
|
/** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
|
|
59
68
|
interface ReactPlaythroughMetadata {
|
|
60
|
-
version:
|
|
69
|
+
version: 5;
|
|
61
70
|
waiverReason?: string;
|
|
62
71
|
trace?: string;
|
|
72
|
+
failure?: ReactFailureDiagnostic;
|
|
63
73
|
evidence: ReactPlaythroughEvidence;
|
|
64
74
|
}
|
|
65
75
|
interface ReactPlaythroughOptions {
|
|
66
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Read authoritative production gameplay state when the DOM cannot prove the
|
|
78
|
+
* result, such as Canvas, Controller, time, or physics flows. The harness
|
|
79
|
+
* samples this state before and after each stage, not after every step.
|
|
80
|
+
* Individual steps may leave it unchanged, but the completed stage must
|
|
81
|
+
* produce a new gameplay result. Observation supplements real production
|
|
82
|
+
* input; it does not replace it.
|
|
83
|
+
*/
|
|
67
84
|
observe: () => unknown;
|
|
68
85
|
}
|
|
69
86
|
interface ReactPlaythroughAssertContext {
|