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.
@@ -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 withoutComments(source) {
58
- let output = "";
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 codePositions(source) {
109
- const positions = Array.from({ length: source.length }, () => false);
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 importedModuleSpecifiers(source) {
128
- const clean = withoutComments(source);
129
- const positions = codePositions(clean);
130
- const modules = [];
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 clean = withoutComments(source);
156
- const positions = codePositions(clean);
157
- const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
158
- const pattern = new RegExp(
159
- `^\\s*import\\s+(?:type\\s+)?\\{([\\s\\S]*?)\\}\\s+from\\s+["']${escapedModule}["']`,
160
- "gm"
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 declaresObserve(source) {
175
- const clean = withoutComments(source);
176
- const positions = codePositions(clean);
177
- for (const match of clean.matchAll(/\bobserve\s*:/g)) {
178
- if (positions[match.index]) return true;
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 false;
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((file) => importsExampleAlias((0, import_node_fs.readFileSync)(file, "utf8"))).map((file) => (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/"));
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(testSource, REACT_TESTING_ENTRY);
222
- const hasObserve = declaresObserve(testSource);
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 checkReactAuthoritativePlaythrough() {
352
- const name = "react-authoritative-playthrough";
307
+ function checkReactProductTestAlignment() {
308
+ const name = "react-product-test-alignment";
353
309
  const audit = auditReactAuthoritativePlaythrough(projectRoot);
354
- for (const issue of audit.issues) console.error(`[${name}] ${issue}`);
355
- return { name, ok: audit.ok };
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"), checkReactAuthoritativePlaythrough()] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
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"]),
@@ -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 withoutComments(source) {
58
- let output = "";
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 codePositions(source) {
109
- const positions = Array.from({ length: source.length }, () => false);
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 importedModuleSpecifiers(source) {
128
- const clean = withoutComments(source);
129
- const positions = codePositions(clean);
130
- const modules = [];
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 clean = withoutComments(source);
156
- const positions = codePositions(clean);
157
- const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
158
- const pattern = new RegExp(
159
- `^\\s*import\\s+(?:type\\s+)?\\{([\\s\\S]*?)\\}\\s+from\\s+["']${escapedModule}["']`,
160
- "gm"
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 declaresObserve(source) {
175
- const clean = withoutComments(source);
176
- const positions = codePositions(clean);
177
- for (const match of clean.matchAll(/\bobserve\s*:/g)) {
178
- if (positions[match.index]) return true;
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 false;
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((file) => importsExampleAlias((0, import_node_fs.readFileSync)(file, "utf8"))).map((file) => (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/"));
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(testSource, REACT_TESTING_ENTRY);
222
- const hasObserve = declaresObserve(testSource);
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 checkReactAuthoritativePlaythrough() {
352
- const name = "react-authoritative-playthrough";
307
+ function checkReactProductTestAlignment() {
308
+ const name = "react-product-test-alignment";
353
309
  const audit = auditReactAuthoritativePlaythrough(projectRoot);
354
- for (const issue of audit.issues) console.error(`[${name}] ${issue}`);
355
- return { name, ok: audit.ok };
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"), checkReactAuthoritativePlaythrough()] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
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"]),
@@ -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 会认为"什么都没变"而不刷新界面——游戏悄悄卡死,没有
@@ -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 会认为"什么都没变"而不刷新界面——游戏悄悄卡死,没有
@@ -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 {
@@ -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 {