miaoda-game-devkit 0.6.5 → 0.7.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.
- package/dist/cli/phaser-lint.js +62 -105
- package/dist/cli/react-lint.js +62 -105
- package/dist/react/index.d.mts +3 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/testing.d.mts +22 -0
- package/dist/react/testing.d.ts +22 -0
- package/dist/react/testing.js +114 -16
- package/dist/react/testing.mjs +114 -16
- package/dist/react/vitest-config.d.mts +5 -0
- package/dist/react/vitest-config.d.ts +5 -0
- package/dist/react/vitest-config.js +522 -365
- package/dist/react/vitest-config.mjs +515 -358
- package/dist/react/vitest-setup.js +112 -14
- package/dist/react/vitest-setup.mjs +112 -14
- package/package.json +2 -1
package/dist/cli/phaser-lint.js
CHANGED
|
@@ -11,6 +11,7 @@ var import_node_path2 = require("path");
|
|
|
11
11
|
// src/cli/react-authoritative-playthrough.ts
|
|
12
12
|
var import_node_fs = require("fs");
|
|
13
13
|
var import_node_path = require("path");
|
|
14
|
+
var import_oxc_parser = require("oxc-parser");
|
|
14
15
|
var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
|
|
15
16
|
var PRODUCTION_APP = "src/App.tsx";
|
|
16
17
|
var EXAMPLE_IMPORT_PREFIX = "@/game/example/";
|
|
@@ -54,89 +55,36 @@ function sourceFiles(root, directory = (0, import_node_path.join)(root, "src"))
|
|
|
54
55
|
}
|
|
55
56
|
return files;
|
|
56
57
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
let state = "code";
|
|
60
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
61
|
-
const char = source[index];
|
|
62
|
-
const next = source[index + 1];
|
|
63
|
-
if (state === "line") {
|
|
64
|
-
if (char === "\n") {
|
|
65
|
-
state = "code";
|
|
66
|
-
output += char;
|
|
67
|
-
} else {
|
|
68
|
-
output += " ";
|
|
69
|
-
}
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (state === "block") {
|
|
73
|
-
if (char === "*" && next === "/") {
|
|
74
|
-
output += " ";
|
|
75
|
-
index += 1;
|
|
76
|
-
state = "code";
|
|
77
|
-
} else {
|
|
78
|
-
output += char === "\n" ? "\n" : " ";
|
|
79
|
-
}
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
if (state === "code" && char === "/" && next === "/") {
|
|
83
|
-
output += " ";
|
|
84
|
-
index += 1;
|
|
85
|
-
state = "line";
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (state === "code" && char === "/" && next === "*") {
|
|
89
|
-
output += " ";
|
|
90
|
-
index += 1;
|
|
91
|
-
state = "block";
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (state === "code" && char === "'") state = "single";
|
|
95
|
-
else if (state === "code" && char === '"') state = "double";
|
|
96
|
-
else if (state === "code" && char === "`") state = "template";
|
|
97
|
-
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
98
|
-
state = "code";
|
|
99
|
-
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
100
|
-
state = "code";
|
|
101
|
-
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
102
|
-
state = "code";
|
|
103
|
-
}
|
|
104
|
-
output += char;
|
|
105
|
-
}
|
|
106
|
-
return output;
|
|
58
|
+
function parseSource(file, source) {
|
|
59
|
+
return (0, import_oxc_parser.parseSync)(file, source, { sourceType: "module" });
|
|
107
60
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
let state = "code";
|
|
111
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
112
|
-
const char = source[index];
|
|
113
|
-
if (state === "code") positions[index] = true;
|
|
114
|
-
if (state === "code" && char === "'") state = "single";
|
|
115
|
-
else if (state === "code" && char === '"') state = "double";
|
|
116
|
-
else if (state === "code" && char === "`") state = "template";
|
|
117
|
-
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
118
|
-
state = "code";
|
|
119
|
-
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
120
|
-
state = "code";
|
|
121
|
-
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
122
|
-
state = "code";
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return positions;
|
|
61
|
+
function staticImports(file, source) {
|
|
62
|
+
return parseSource(file, source).module.staticImports;
|
|
126
63
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const pattern = /\b(?:from\s+|import\s*(?:\(\s*)?)["']([^"']+)["']\s*\)?/g;
|
|
132
|
-
for (const match of clean.matchAll(pattern)) {
|
|
133
|
-
if (positions[match.index]) modules.push(match[1]);
|
|
64
|
+
function collectDynamicImportSpecifiers(value, modules) {
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
for (const item of value) collectDynamicImportSpecifiers(item, modules);
|
|
67
|
+
return;
|
|
134
68
|
}
|
|
69
|
+
if (!isRecord(value)) return;
|
|
70
|
+
if (value.type === "ImportExpression" && isRecord(value.source) && value.source.type === "Literal" && typeof value.source.value === "string") {
|
|
71
|
+
modules.push(value.source.value);
|
|
72
|
+
}
|
|
73
|
+
for (const child of Object.values(value)) {
|
|
74
|
+
collectDynamicImportSpecifiers(child, modules);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function importedModuleSpecifiers(file, source) {
|
|
78
|
+
const parsed = parseSource(file, source);
|
|
79
|
+
const modules = parsed.module.staticImports.map(
|
|
80
|
+
({ moduleRequest }) => moduleRequest.value
|
|
81
|
+
);
|
|
82
|
+
collectDynamicImportSpecifiers(parsed.program, modules);
|
|
135
83
|
return modules;
|
|
136
84
|
}
|
|
137
85
|
function productionFileImportsExample(file, projectRoot2) {
|
|
138
86
|
const exampleRoot = (0, import_node_path.join)(projectRoot2, "src/game/example");
|
|
139
|
-
return importedModuleSpecifiers((0, import_node_fs.readFileSync)(file, "utf8")).some(
|
|
87
|
+
return importedModuleSpecifiers(file, (0, import_node_fs.readFileSync)(file, "utf8")).some(
|
|
140
88
|
(moduleName) => {
|
|
141
89
|
if (moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)) return true;
|
|
142
90
|
if (!moduleName.startsWith(".")) return false;
|
|
@@ -145,25 +93,19 @@ function productionFileImportsExample(file, projectRoot2) {
|
|
|
145
93
|
}
|
|
146
94
|
);
|
|
147
95
|
}
|
|
148
|
-
function importsExampleAlias(source) {
|
|
149
|
-
return importedModuleSpecifiers(source).some(
|
|
96
|
+
function importsExampleAlias(file, source) {
|
|
97
|
+
return importedModuleSpecifiers(file, source).some(
|
|
150
98
|
(moduleName) => moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)
|
|
151
99
|
);
|
|
152
100
|
}
|
|
153
|
-
function namedImports(source, moduleName) {
|
|
101
|
+
function namedImports(file, source, moduleName) {
|
|
154
102
|
const names = /* @__PURE__ */ new Set();
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
);
|
|
162
|
-
for (const match of clean.matchAll(pattern)) {
|
|
163
|
-
if (!positions[match.index]) continue;
|
|
164
|
-
for (const specifier of match[1].split(",")) {
|
|
165
|
-
const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
|
|
166
|
-
if (imported) names.add(imported);
|
|
103
|
+
for (const declaration of staticImports(file, source)) {
|
|
104
|
+
if (declaration.moduleRequest.value !== moduleName) continue;
|
|
105
|
+
for (const entry of declaration.entries) {
|
|
106
|
+
if (entry.importName.kind === "Name" && entry.importName.name) {
|
|
107
|
+
names.add(entry.importName.name);
|
|
108
|
+
}
|
|
167
109
|
}
|
|
168
110
|
}
|
|
169
111
|
return names;
|
|
@@ -171,13 +113,20 @@ function namedImports(source, moduleName) {
|
|
|
171
113
|
function containsAny(values, expected) {
|
|
172
114
|
return [...values].some((value) => expected.has(value));
|
|
173
115
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
116
|
+
function isRecord(value) {
|
|
117
|
+
return typeof value === "object" && value !== null;
|
|
118
|
+
}
|
|
119
|
+
function containsObserveProperty(value) {
|
|
120
|
+
if (Array.isArray(value)) return value.some(containsObserveProperty);
|
|
121
|
+
if (!isRecord(value)) return false;
|
|
122
|
+
if (value.type === "Property" && isRecord(value.key)) {
|
|
123
|
+
if (value.key.type === "Identifier" && value.key.name === "observe") return true;
|
|
124
|
+
if (value.key.type === "Literal" && value.key.value === "observe") return true;
|
|
179
125
|
}
|
|
180
|
-
return
|
|
126
|
+
return Object.values(value).some(containsObserveProperty);
|
|
127
|
+
}
|
|
128
|
+
function declaresObserve(file, source) {
|
|
129
|
+
return containsObserveProperty(parseSource(file, source).program);
|
|
181
130
|
}
|
|
182
131
|
function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
183
132
|
const clockFiles = [];
|
|
@@ -185,6 +134,7 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
|
185
134
|
const productionFiles = sourceFiles(projectRoot2);
|
|
186
135
|
for (const file of productionFiles) {
|
|
187
136
|
const imports = namedImports(
|
|
137
|
+
file,
|
|
188
138
|
(0, import_node_fs.readFileSync)(file, "utf8"),
|
|
189
139
|
REACT_RUNTIME_ENTRY
|
|
190
140
|
);
|
|
@@ -198,7 +148,9 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
|
198
148
|
const productionUsesExample = productionFiles.some(
|
|
199
149
|
(file) => productionFileImportsExample(file, projectRoot2)
|
|
200
150
|
);
|
|
201
|
-
const staleExampleTestFiles = productionUsesExample ? [] : runnableTestFiles(projectRoot2).filter(
|
|
151
|
+
const staleExampleTestFiles = productionUsesExample ? [] : runnableTestFiles(projectRoot2).filter(
|
|
152
|
+
(file) => importsExampleAlias(file, (0, import_node_fs.readFileSync)(file, "utf8"))
|
|
153
|
+
).map((file) => (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/"));
|
|
202
154
|
const issues = [];
|
|
203
155
|
if (staleExampleTestFiles.length > 0) {
|
|
204
156
|
issues.push(
|
|
@@ -218,8 +170,12 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
|
218
170
|
}
|
|
219
171
|
const testPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_PLAYTHROUGH);
|
|
220
172
|
const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
|
|
221
|
-
const testingImports = namedImports(
|
|
222
|
-
|
|
173
|
+
const testingImports = namedImports(
|
|
174
|
+
testPath,
|
|
175
|
+
testSource,
|
|
176
|
+
REACT_TESTING_ENTRY
|
|
177
|
+
);
|
|
178
|
+
const hasObserve = declaresObserve(testPath, testSource);
|
|
223
179
|
if (!hasObserve) {
|
|
224
180
|
issues.push(
|
|
225
181
|
`${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Pass { observe: () => telemetry.read.session() } to playthroughTest, using Telemetry backed by the same production Controller rendered by <App />; DOM labels are not an authoritative gameplay boundary.`
|
|
@@ -348,14 +304,15 @@ function checkVitestConfig(target) {
|
|
|
348
304
|
}
|
|
349
305
|
return { name, ok: true };
|
|
350
306
|
}
|
|
351
|
-
function
|
|
352
|
-
const name = "react-
|
|
307
|
+
function checkReactProductTestAlignment() {
|
|
308
|
+
const name = "react-product-test-alignment";
|
|
353
309
|
const audit = auditReactAuthoritativePlaythrough(projectRoot);
|
|
354
|
-
|
|
355
|
-
|
|
310
|
+
if (audit.staleExampleTestFiles.length === 0) return { name, ok: true };
|
|
311
|
+
console.error(`[${name}] ${audit.issues[0]}`);
|
|
312
|
+
return { name, ok: false };
|
|
356
313
|
}
|
|
357
314
|
async function runAllChecks(target) {
|
|
358
|
-
const targetChecks = target === "react" ? [checkVitestConfig("react"),
|
|
315
|
+
const targetChecks = target === "react" ? [checkVitestConfig("react"), checkReactProductTestAlignment()] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
|
|
359
316
|
const lintRoots = target === "react" && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
|
|
360
317
|
const [tsgo, biome] = await Promise.all([
|
|
361
318
|
run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
|
package/dist/cli/react-lint.js
CHANGED
|
@@ -11,6 +11,7 @@ var import_node_path2 = require("path");
|
|
|
11
11
|
// src/cli/react-authoritative-playthrough.ts
|
|
12
12
|
var import_node_fs = require("fs");
|
|
13
13
|
var import_node_path = require("path");
|
|
14
|
+
var import_oxc_parser = require("oxc-parser");
|
|
14
15
|
var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
|
|
15
16
|
var PRODUCTION_APP = "src/App.tsx";
|
|
16
17
|
var EXAMPLE_IMPORT_PREFIX = "@/game/example/";
|
|
@@ -54,89 +55,36 @@ function sourceFiles(root, directory = (0, import_node_path.join)(root, "src"))
|
|
|
54
55
|
}
|
|
55
56
|
return files;
|
|
56
57
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
let state = "code";
|
|
60
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
61
|
-
const char = source[index];
|
|
62
|
-
const next = source[index + 1];
|
|
63
|
-
if (state === "line") {
|
|
64
|
-
if (char === "\n") {
|
|
65
|
-
state = "code";
|
|
66
|
-
output += char;
|
|
67
|
-
} else {
|
|
68
|
-
output += " ";
|
|
69
|
-
}
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (state === "block") {
|
|
73
|
-
if (char === "*" && next === "/") {
|
|
74
|
-
output += " ";
|
|
75
|
-
index += 1;
|
|
76
|
-
state = "code";
|
|
77
|
-
} else {
|
|
78
|
-
output += char === "\n" ? "\n" : " ";
|
|
79
|
-
}
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
if (state === "code" && char === "/" && next === "/") {
|
|
83
|
-
output += " ";
|
|
84
|
-
index += 1;
|
|
85
|
-
state = "line";
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (state === "code" && char === "/" && next === "*") {
|
|
89
|
-
output += " ";
|
|
90
|
-
index += 1;
|
|
91
|
-
state = "block";
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (state === "code" && char === "'") state = "single";
|
|
95
|
-
else if (state === "code" && char === '"') state = "double";
|
|
96
|
-
else if (state === "code" && char === "`") state = "template";
|
|
97
|
-
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
98
|
-
state = "code";
|
|
99
|
-
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
100
|
-
state = "code";
|
|
101
|
-
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
102
|
-
state = "code";
|
|
103
|
-
}
|
|
104
|
-
output += char;
|
|
105
|
-
}
|
|
106
|
-
return output;
|
|
58
|
+
function parseSource(file, source) {
|
|
59
|
+
return (0, import_oxc_parser.parseSync)(file, source, { sourceType: "module" });
|
|
107
60
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
let state = "code";
|
|
111
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
112
|
-
const char = source[index];
|
|
113
|
-
if (state === "code") positions[index] = true;
|
|
114
|
-
if (state === "code" && char === "'") state = "single";
|
|
115
|
-
else if (state === "code" && char === '"') state = "double";
|
|
116
|
-
else if (state === "code" && char === "`") state = "template";
|
|
117
|
-
else if (state === "single" && char === "'" && source[index - 1] !== "\\") {
|
|
118
|
-
state = "code";
|
|
119
|
-
} else if (state === "double" && char === '"' && source[index - 1] !== "\\") {
|
|
120
|
-
state = "code";
|
|
121
|
-
} else if (state === "template" && char === "`" && source[index - 1] !== "\\") {
|
|
122
|
-
state = "code";
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return positions;
|
|
61
|
+
function staticImports(file, source) {
|
|
62
|
+
return parseSource(file, source).module.staticImports;
|
|
126
63
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const pattern = /\b(?:from\s+|import\s*(?:\(\s*)?)["']([^"']+)["']\s*\)?/g;
|
|
132
|
-
for (const match of clean.matchAll(pattern)) {
|
|
133
|
-
if (positions[match.index]) modules.push(match[1]);
|
|
64
|
+
function collectDynamicImportSpecifiers(value, modules) {
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
for (const item of value) collectDynamicImportSpecifiers(item, modules);
|
|
67
|
+
return;
|
|
134
68
|
}
|
|
69
|
+
if (!isRecord(value)) return;
|
|
70
|
+
if (value.type === "ImportExpression" && isRecord(value.source) && value.source.type === "Literal" && typeof value.source.value === "string") {
|
|
71
|
+
modules.push(value.source.value);
|
|
72
|
+
}
|
|
73
|
+
for (const child of Object.values(value)) {
|
|
74
|
+
collectDynamicImportSpecifiers(child, modules);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function importedModuleSpecifiers(file, source) {
|
|
78
|
+
const parsed = parseSource(file, source);
|
|
79
|
+
const modules = parsed.module.staticImports.map(
|
|
80
|
+
({ moduleRequest }) => moduleRequest.value
|
|
81
|
+
);
|
|
82
|
+
collectDynamicImportSpecifiers(parsed.program, modules);
|
|
135
83
|
return modules;
|
|
136
84
|
}
|
|
137
85
|
function productionFileImportsExample(file, projectRoot2) {
|
|
138
86
|
const exampleRoot = (0, import_node_path.join)(projectRoot2, "src/game/example");
|
|
139
|
-
return importedModuleSpecifiers((0, import_node_fs.readFileSync)(file, "utf8")).some(
|
|
87
|
+
return importedModuleSpecifiers(file, (0, import_node_fs.readFileSync)(file, "utf8")).some(
|
|
140
88
|
(moduleName) => {
|
|
141
89
|
if (moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)) return true;
|
|
142
90
|
if (!moduleName.startsWith(".")) return false;
|
|
@@ -145,25 +93,19 @@ function productionFileImportsExample(file, projectRoot2) {
|
|
|
145
93
|
}
|
|
146
94
|
);
|
|
147
95
|
}
|
|
148
|
-
function importsExampleAlias(source) {
|
|
149
|
-
return importedModuleSpecifiers(source).some(
|
|
96
|
+
function importsExampleAlias(file, source) {
|
|
97
|
+
return importedModuleSpecifiers(file, source).some(
|
|
150
98
|
(moduleName) => moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)
|
|
151
99
|
);
|
|
152
100
|
}
|
|
153
|
-
function namedImports(source, moduleName) {
|
|
101
|
+
function namedImports(file, source, moduleName) {
|
|
154
102
|
const names = /* @__PURE__ */ new Set();
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
);
|
|
162
|
-
for (const match of clean.matchAll(pattern)) {
|
|
163
|
-
if (!positions[match.index]) continue;
|
|
164
|
-
for (const specifier of match[1].split(",")) {
|
|
165
|
-
const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
|
|
166
|
-
if (imported) names.add(imported);
|
|
103
|
+
for (const declaration of staticImports(file, source)) {
|
|
104
|
+
if (declaration.moduleRequest.value !== moduleName) continue;
|
|
105
|
+
for (const entry of declaration.entries) {
|
|
106
|
+
if (entry.importName.kind === "Name" && entry.importName.name) {
|
|
107
|
+
names.add(entry.importName.name);
|
|
108
|
+
}
|
|
167
109
|
}
|
|
168
110
|
}
|
|
169
111
|
return names;
|
|
@@ -171,13 +113,20 @@ function namedImports(source, moduleName) {
|
|
|
171
113
|
function containsAny(values, expected) {
|
|
172
114
|
return [...values].some((value) => expected.has(value));
|
|
173
115
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
116
|
+
function isRecord(value) {
|
|
117
|
+
return typeof value === "object" && value !== null;
|
|
118
|
+
}
|
|
119
|
+
function containsObserveProperty(value) {
|
|
120
|
+
if (Array.isArray(value)) return value.some(containsObserveProperty);
|
|
121
|
+
if (!isRecord(value)) return false;
|
|
122
|
+
if (value.type === "Property" && isRecord(value.key)) {
|
|
123
|
+
if (value.key.type === "Identifier" && value.key.name === "observe") return true;
|
|
124
|
+
if (value.key.type === "Literal" && value.key.value === "observe") return true;
|
|
179
125
|
}
|
|
180
|
-
return
|
|
126
|
+
return Object.values(value).some(containsObserveProperty);
|
|
127
|
+
}
|
|
128
|
+
function declaresObserve(file, source) {
|
|
129
|
+
return containsObserveProperty(parseSource(file, source).program);
|
|
181
130
|
}
|
|
182
131
|
function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
183
132
|
const clockFiles = [];
|
|
@@ -185,6 +134,7 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
|
185
134
|
const productionFiles = sourceFiles(projectRoot2);
|
|
186
135
|
for (const file of productionFiles) {
|
|
187
136
|
const imports = namedImports(
|
|
137
|
+
file,
|
|
188
138
|
(0, import_node_fs.readFileSync)(file, "utf8"),
|
|
189
139
|
REACT_RUNTIME_ENTRY
|
|
190
140
|
);
|
|
@@ -198,7 +148,9 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
|
198
148
|
const productionUsesExample = productionFiles.some(
|
|
199
149
|
(file) => productionFileImportsExample(file, projectRoot2)
|
|
200
150
|
);
|
|
201
|
-
const staleExampleTestFiles = productionUsesExample ? [] : runnableTestFiles(projectRoot2).filter(
|
|
151
|
+
const staleExampleTestFiles = productionUsesExample ? [] : runnableTestFiles(projectRoot2).filter(
|
|
152
|
+
(file) => importsExampleAlias(file, (0, import_node_fs.readFileSync)(file, "utf8"))
|
|
153
|
+
).map((file) => (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/"));
|
|
202
154
|
const issues = [];
|
|
203
155
|
if (staleExampleTestFiles.length > 0) {
|
|
204
156
|
issues.push(
|
|
@@ -218,8 +170,12 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
|
218
170
|
}
|
|
219
171
|
const testPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_PLAYTHROUGH);
|
|
220
172
|
const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
|
|
221
|
-
const testingImports = namedImports(
|
|
222
|
-
|
|
173
|
+
const testingImports = namedImports(
|
|
174
|
+
testPath,
|
|
175
|
+
testSource,
|
|
176
|
+
REACT_TESTING_ENTRY
|
|
177
|
+
);
|
|
178
|
+
const hasObserve = declaresObserve(testPath, testSource);
|
|
223
179
|
if (!hasObserve) {
|
|
224
180
|
issues.push(
|
|
225
181
|
`${PRODUCTION_PLAYTHROUGH} must declare observe because production uses the devkit clock or Controller ownership hook. Pass { observe: () => telemetry.read.session() } to playthroughTest, using Telemetry backed by the same production Controller rendered by <App />; DOM labels are not an authoritative gameplay boundary.`
|
|
@@ -348,14 +304,15 @@ function checkVitestConfig(target) {
|
|
|
348
304
|
}
|
|
349
305
|
return { name, ok: true };
|
|
350
306
|
}
|
|
351
|
-
function
|
|
352
|
-
const name = "react-
|
|
307
|
+
function checkReactProductTestAlignment() {
|
|
308
|
+
const name = "react-product-test-alignment";
|
|
353
309
|
const audit = auditReactAuthoritativePlaythrough(projectRoot);
|
|
354
|
-
|
|
355
|
-
|
|
310
|
+
if (audit.staleExampleTestFiles.length === 0) return { name, ok: true };
|
|
311
|
+
console.error(`[${name}] ${audit.issues[0]}`);
|
|
312
|
+
return { name, ok: false };
|
|
356
313
|
}
|
|
357
314
|
async function runAllChecks(target) {
|
|
358
|
-
const targetChecks = target === "react" ? [checkVitestConfig("react"),
|
|
315
|
+
const targetChecks = target === "react" ? [checkVitestConfig("react"), checkReactProductTestAlignment()] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
|
|
359
316
|
const lintRoots = target === "react" && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
|
|
360
317
|
const [tsgo, biome] = await Promise.all([
|
|
361
318
|
run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
|
package/dist/react/index.d.mts
CHANGED
|
@@ -18,6 +18,9 @@ interface SubscribableGameController<TState> extends DisposableGameController {
|
|
|
18
18
|
/**
|
|
19
19
|
* 一次调用接管游戏 Controller 的创建、销毁与状态订阅:
|
|
20
20
|
* useOwnedGameController 的 ownership,加上 useSyncExternalStore 的渲染桥。
|
|
21
|
+
* `createController` 是零参数工厂;hook 不会向它传 clock、options 或 props。
|
|
22
|
+
* 需要注入依赖时在调用处闭包捕获,例如:
|
|
23
|
+
* `useGameController(() => gameFactory({ ...options, clock }))`。
|
|
21
24
|
*
|
|
22
25
|
* 附带一道开发期检查:状态内容变了、但 snapshot() 返回的还是原来那个
|
|
23
26
|
* 对象时,React 会认为"什么都没变"而不刷新界面——游戏悄悄卡死,没有
|
package/dist/react/index.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ interface SubscribableGameController<TState> extends DisposableGameController {
|
|
|
18
18
|
/**
|
|
19
19
|
* 一次调用接管游戏 Controller 的创建、销毁与状态订阅:
|
|
20
20
|
* useOwnedGameController 的 ownership,加上 useSyncExternalStore 的渲染桥。
|
|
21
|
+
* `createController` 是零参数工厂;hook 不会向它传 clock、options 或 props。
|
|
22
|
+
* 需要注入依赖时在调用处闭包捕获,例如:
|
|
23
|
+
* `useGameController(() => gameFactory({ ...options, clock }))`。
|
|
21
24
|
*
|
|
22
25
|
* 附带一道开发期检查:状态内容变了、但 snapshot() 返回的还是原来那个
|
|
23
26
|
* 对象时,React 会认为"什么都没变"而不刷新界面——游戏悄悄卡死,没有
|
package/dist/react/testing.d.mts
CHANGED
|
@@ -25,7 +25,18 @@ declare class ManualGameClock implements GameClock {
|
|
|
25
25
|
interface ReactFailureEntry {
|
|
26
26
|
code: string;
|
|
27
27
|
message: string;
|
|
28
|
+
errorName?: string;
|
|
29
|
+
actual?: string;
|
|
30
|
+
expected?: string;
|
|
31
|
+
origin?: ReactFailureOrigin;
|
|
32
|
+
layer?: ReactFailureLayer;
|
|
28
33
|
}
|
|
34
|
+
interface ReactFailureOrigin {
|
|
35
|
+
file: string;
|
|
36
|
+
line: number;
|
|
37
|
+
column: number;
|
|
38
|
+
}
|
|
39
|
+
type ReactFailureLayer = "product" | "test" | "harness" | "dependency" | "unknown";
|
|
29
40
|
interface ReactFailureDiagnostic {
|
|
30
41
|
source: "playthrough" | "test-runtime";
|
|
31
42
|
entries: ReactFailureEntry[];
|
|
@@ -88,6 +99,10 @@ interface ReactPlaythroughAssertContext {
|
|
|
88
99
|
user: UserEvent;
|
|
89
100
|
view: RenderResult;
|
|
90
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* 每个阶段独立声明完成条件、断言和推进上限。确定性 `step` 属于具体阶段,
|
|
104
|
+
* 不存在 playthrough 顶层的共享 step;这样失败 TRACE 才能归属到准确区间。
|
|
105
|
+
*/
|
|
91
106
|
interface ReactPlaythroughStageShared {
|
|
92
107
|
until: () => boolean;
|
|
93
108
|
assert: (context: ReactPlaythroughAssertContext) => void | Promise<void>;
|
|
@@ -118,7 +133,13 @@ interface ReactPlaythroughArguments {
|
|
|
118
133
|
}
|
|
119
134
|
type ReactPlaythroughRun = (arguments_: ReactPlaythroughArguments) => void | Promise<void>;
|
|
120
135
|
interface ReactPlaythroughTest {
|
|
136
|
+
/** 第一个参数是实际渲染的 ReactNode;简单游戏直接传 `<App />`。 */
|
|
121
137
|
(element: ReactNode, run: ReactPlaythroughRun): void;
|
|
138
|
+
/**
|
|
139
|
+
* Controller/Canvas 游戏仍传生产 `<App gameFactory={factory} />`,通过
|
|
140
|
+
* options.observe 读取同一个 Controller。不要把 `(clock) => <App />`
|
|
141
|
+
* 之类的元素工厂传到这里;clock 应由 App 的 Controller factory 闭包注入。
|
|
142
|
+
*/
|
|
122
143
|
(element: ReactNode, options: ReactPlaythroughOptions, run: ReactPlaythroughRun): void;
|
|
123
144
|
skip(reason: string, element: ReactNode): void;
|
|
124
145
|
}
|
|
@@ -126,6 +147,7 @@ interface ReactPlaythroughTest {
|
|
|
126
147
|
interface ReactPlaythroughAuditInput {
|
|
127
148
|
name: string;
|
|
128
149
|
state: "passed" | "failed" | "skipped" | "pending";
|
|
150
|
+
mode?: "run" | "only" | "skip" | "todo";
|
|
129
151
|
metadata?: ReactPlaythroughMetadata;
|
|
130
152
|
}
|
|
131
153
|
interface ReactPlaythroughAuditResult {
|
package/dist/react/testing.d.ts
CHANGED
|
@@ -25,7 +25,18 @@ declare class ManualGameClock implements GameClock {
|
|
|
25
25
|
interface ReactFailureEntry {
|
|
26
26
|
code: string;
|
|
27
27
|
message: string;
|
|
28
|
+
errorName?: string;
|
|
29
|
+
actual?: string;
|
|
30
|
+
expected?: string;
|
|
31
|
+
origin?: ReactFailureOrigin;
|
|
32
|
+
layer?: ReactFailureLayer;
|
|
28
33
|
}
|
|
34
|
+
interface ReactFailureOrigin {
|
|
35
|
+
file: string;
|
|
36
|
+
line: number;
|
|
37
|
+
column: number;
|
|
38
|
+
}
|
|
39
|
+
type ReactFailureLayer = "product" | "test" | "harness" | "dependency" | "unknown";
|
|
29
40
|
interface ReactFailureDiagnostic {
|
|
30
41
|
source: "playthrough" | "test-runtime";
|
|
31
42
|
entries: ReactFailureEntry[];
|
|
@@ -88,6 +99,10 @@ interface ReactPlaythroughAssertContext {
|
|
|
88
99
|
user: UserEvent;
|
|
89
100
|
view: RenderResult;
|
|
90
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* 每个阶段独立声明完成条件、断言和推进上限。确定性 `step` 属于具体阶段,
|
|
104
|
+
* 不存在 playthrough 顶层的共享 step;这样失败 TRACE 才能归属到准确区间。
|
|
105
|
+
*/
|
|
91
106
|
interface ReactPlaythroughStageShared {
|
|
92
107
|
until: () => boolean;
|
|
93
108
|
assert: (context: ReactPlaythroughAssertContext) => void | Promise<void>;
|
|
@@ -118,7 +133,13 @@ interface ReactPlaythroughArguments {
|
|
|
118
133
|
}
|
|
119
134
|
type ReactPlaythroughRun = (arguments_: ReactPlaythroughArguments) => void | Promise<void>;
|
|
120
135
|
interface ReactPlaythroughTest {
|
|
136
|
+
/** 第一个参数是实际渲染的 ReactNode;简单游戏直接传 `<App />`。 */
|
|
121
137
|
(element: ReactNode, run: ReactPlaythroughRun): void;
|
|
138
|
+
/**
|
|
139
|
+
* Controller/Canvas 游戏仍传生产 `<App gameFactory={factory} />`,通过
|
|
140
|
+
* options.observe 读取同一个 Controller。不要把 `(clock) => <App />`
|
|
141
|
+
* 之类的元素工厂传到这里;clock 应由 App 的 Controller factory 闭包注入。
|
|
142
|
+
*/
|
|
122
143
|
(element: ReactNode, options: ReactPlaythroughOptions, run: ReactPlaythroughRun): void;
|
|
123
144
|
skip(reason: string, element: ReactNode): void;
|
|
124
145
|
}
|
|
@@ -126,6 +147,7 @@ interface ReactPlaythroughTest {
|
|
|
126
147
|
interface ReactPlaythroughAuditInput {
|
|
127
148
|
name: string;
|
|
128
149
|
state: "passed" | "failed" | "skipped" | "pending";
|
|
150
|
+
mode?: "run" | "only" | "skip" | "todo";
|
|
129
151
|
metadata?: ReactPlaythroughMetadata;
|
|
130
152
|
}
|
|
131
153
|
interface ReactPlaythroughAuditResult {
|