miaoda-game-devkit 0.6.2 → 0.6.4
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 +78 -5
- package/dist/cli/react-lint.js +78 -5
- package/dist/react/index.js +11 -1
- package/dist/react/index.mjs +11 -1
- package/dist/react/testing.d.mts +11 -1
- package/dist/react/testing.d.ts +11 -1
- package/dist/react/testing.js +158 -31
- package/dist/react/testing.mjs +158 -31
- package/dist/react/vitest-config.js +598 -89
- package/dist/react/vitest-config.mjs +599 -90
- package/dist/react/vitest-setup.js +87 -4
- package/dist/react/vitest-setup.mjs +87 -4
- package/package.json +1 -1
package/dist/cli/phaser-lint.js
CHANGED
|
@@ -12,6 +12,8 @@ var import_node_path2 = require("path");
|
|
|
12
12
|
var import_node_fs = require("fs");
|
|
13
13
|
var import_node_path = require("path");
|
|
14
14
|
var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
|
|
15
|
+
var PRODUCTION_APP = "src/App.tsx";
|
|
16
|
+
var EXAMPLE_IMPORT_PREFIX = "@/game/example/";
|
|
15
17
|
var REACT_RUNTIME_ENTRY = "miaoda-game-devkit/react";
|
|
16
18
|
var REACT_TESTING_ENTRY = "miaoda-game-devkit/react/testing";
|
|
17
19
|
var CLOCK_IMPORTS = /* @__PURE__ */ new Set(["browserGameClock"]);
|
|
@@ -20,6 +22,19 @@ var CONTROLLER_IMPORTS = /* @__PURE__ */ new Set([
|
|
|
20
22
|
"useOwnedGameController"
|
|
21
23
|
]);
|
|
22
24
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
25
|
+
function runnableTestFiles(root, directory = (0, import_node_path.join)(root, "tests")) {
|
|
26
|
+
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
27
|
+
const files = [];
|
|
28
|
+
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
29
|
+
const path = (0, import_node_path.join)(directory, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
files.push(...runnableTestFiles(root, path));
|
|
32
|
+
} else if (entry.isFile() && /\.test\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
33
|
+
files.push(path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
23
38
|
function extension(path) {
|
|
24
39
|
const index = path.lastIndexOf(".");
|
|
25
40
|
return index < 0 ? "" : path.slice(index);
|
|
@@ -109,6 +124,32 @@ function codePositions(source) {
|
|
|
109
124
|
}
|
|
110
125
|
return positions;
|
|
111
126
|
}
|
|
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]);
|
|
134
|
+
}
|
|
135
|
+
return modules;
|
|
136
|
+
}
|
|
137
|
+
function productionFileImportsExample(file, projectRoot2) {
|
|
138
|
+
const exampleRoot = (0, import_node_path.join)(projectRoot2, "src/game/example");
|
|
139
|
+
return importedModuleSpecifiers((0, import_node_fs.readFileSync)(file, "utf8")).some(
|
|
140
|
+
(moduleName) => {
|
|
141
|
+
if (moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)) return true;
|
|
142
|
+
if (!moduleName.startsWith(".")) return false;
|
|
143
|
+
const target = (0, import_node_path.resolve)((0, import_node_path.dirname)(file), moduleName);
|
|
144
|
+
return target === exampleRoot || target.startsWith(`${exampleRoot}${import_node_path.sep}`);
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function importsExampleAlias(source) {
|
|
149
|
+
return importedModuleSpecifiers(source).some(
|
|
150
|
+
(moduleName) => moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
112
153
|
function namedImports(source, moduleName) {
|
|
113
154
|
const names = /* @__PURE__ */ new Set();
|
|
114
155
|
const clean = withoutComments(source);
|
|
@@ -141,15 +182,39 @@ function declaresObserve(source) {
|
|
|
141
182
|
function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
142
183
|
const clockFiles = [];
|
|
143
184
|
const controllerFiles = [];
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
const productionFiles = sourceFiles(projectRoot2);
|
|
186
|
+
for (const file of productionFiles) {
|
|
187
|
+
const imports = namedImports(
|
|
188
|
+
(0, import_node_fs.readFileSync)(file, "utf8"),
|
|
189
|
+
REACT_RUNTIME_ENTRY
|
|
190
|
+
);
|
|
146
191
|
const projectPath = (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/");
|
|
147
192
|
if (containsAny(imports, CLOCK_IMPORTS)) clockFiles.push(projectPath);
|
|
148
|
-
if (containsAny(imports, CONTROLLER_IMPORTS))
|
|
193
|
+
if (containsAny(imports, CONTROLLER_IMPORTS))
|
|
194
|
+
controllerFiles.push(projectPath);
|
|
149
195
|
}
|
|
196
|
+
const appPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_APP);
|
|
197
|
+
const productionEntryExists = (0, import_node_fs.existsSync)(appPath);
|
|
198
|
+
const productionUsesExample = productionFiles.some(
|
|
199
|
+
(file) => productionFileImportsExample(file, projectRoot2)
|
|
200
|
+
);
|
|
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("\\", "/"));
|
|
150
202
|
const issues = [];
|
|
203
|
+
if (staleExampleTestFiles.length > 0) {
|
|
204
|
+
issues.push(
|
|
205
|
+
`Production ${PRODUCTION_APP} no longer imports ${EXAMPLE_IMPORT_PREFIX}*, but runnable product tests still do: ${staleExampleTestFiles.join(", ")}. Replace those CollectGame tests with tests for the production game or delete genuinely inapplicable slots. Teaching examples under tests/examples/**/*.example.* remain allowed.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
151
208
|
if (clockFiles.length === 0 && controllerFiles.length === 0) {
|
|
152
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
ok: issues.length === 0,
|
|
211
|
+
issues,
|
|
212
|
+
clockFiles,
|
|
213
|
+
controllerFiles,
|
|
214
|
+
productionEntryExists,
|
|
215
|
+
productionUsesExample,
|
|
216
|
+
staleExampleTestFiles
|
|
217
|
+
};
|
|
153
218
|
}
|
|
154
219
|
const testPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_PLAYTHROUGH);
|
|
155
220
|
const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
|
|
@@ -165,7 +230,15 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
|
165
230
|
`${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
231
|
);
|
|
167
232
|
}
|
|
168
|
-
return {
|
|
233
|
+
return {
|
|
234
|
+
ok: issues.length === 0,
|
|
235
|
+
issues,
|
|
236
|
+
clockFiles,
|
|
237
|
+
controllerFiles,
|
|
238
|
+
productionEntryExists,
|
|
239
|
+
productionUsesExample,
|
|
240
|
+
staleExampleTestFiles
|
|
241
|
+
};
|
|
169
242
|
}
|
|
170
243
|
|
|
171
244
|
// src/cli/lint.ts
|
package/dist/cli/react-lint.js
CHANGED
|
@@ -12,6 +12,8 @@ var import_node_path2 = require("path");
|
|
|
12
12
|
var import_node_fs = require("fs");
|
|
13
13
|
var import_node_path = require("path");
|
|
14
14
|
var PRODUCTION_PLAYTHROUGH = "tests/production-playthrough.test.tsx";
|
|
15
|
+
var PRODUCTION_APP = "src/App.tsx";
|
|
16
|
+
var EXAMPLE_IMPORT_PREFIX = "@/game/example/";
|
|
15
17
|
var REACT_RUNTIME_ENTRY = "miaoda-game-devkit/react";
|
|
16
18
|
var REACT_TESTING_ENTRY = "miaoda-game-devkit/react/testing";
|
|
17
19
|
var CLOCK_IMPORTS = /* @__PURE__ */ new Set(["browserGameClock"]);
|
|
@@ -20,6 +22,19 @@ var CONTROLLER_IMPORTS = /* @__PURE__ */ new Set([
|
|
|
20
22
|
"useOwnedGameController"
|
|
21
23
|
]);
|
|
22
24
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
25
|
+
function runnableTestFiles(root, directory = (0, import_node_path.join)(root, "tests")) {
|
|
26
|
+
if (!(0, import_node_fs.existsSync)(directory)) return [];
|
|
27
|
+
const files = [];
|
|
28
|
+
for (const entry of (0, import_node_fs.readdirSync)(directory, { withFileTypes: true })) {
|
|
29
|
+
const path = (0, import_node_path.join)(directory, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
files.push(...runnableTestFiles(root, path));
|
|
32
|
+
} else if (entry.isFile() && /\.test\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
33
|
+
files.push(path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
}
|
|
23
38
|
function extension(path) {
|
|
24
39
|
const index = path.lastIndexOf(".");
|
|
25
40
|
return index < 0 ? "" : path.slice(index);
|
|
@@ -109,6 +124,32 @@ function codePositions(source) {
|
|
|
109
124
|
}
|
|
110
125
|
return positions;
|
|
111
126
|
}
|
|
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]);
|
|
134
|
+
}
|
|
135
|
+
return modules;
|
|
136
|
+
}
|
|
137
|
+
function productionFileImportsExample(file, projectRoot2) {
|
|
138
|
+
const exampleRoot = (0, import_node_path.join)(projectRoot2, "src/game/example");
|
|
139
|
+
return importedModuleSpecifiers((0, import_node_fs.readFileSync)(file, "utf8")).some(
|
|
140
|
+
(moduleName) => {
|
|
141
|
+
if (moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)) return true;
|
|
142
|
+
if (!moduleName.startsWith(".")) return false;
|
|
143
|
+
const target = (0, import_node_path.resolve)((0, import_node_path.dirname)(file), moduleName);
|
|
144
|
+
return target === exampleRoot || target.startsWith(`${exampleRoot}${import_node_path.sep}`);
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function importsExampleAlias(source) {
|
|
149
|
+
return importedModuleSpecifiers(source).some(
|
|
150
|
+
(moduleName) => moduleName.startsWith(EXAMPLE_IMPORT_PREFIX)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
112
153
|
function namedImports(source, moduleName) {
|
|
113
154
|
const names = /* @__PURE__ */ new Set();
|
|
114
155
|
const clean = withoutComments(source);
|
|
@@ -141,15 +182,39 @@ function declaresObserve(source) {
|
|
|
141
182
|
function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
142
183
|
const clockFiles = [];
|
|
143
184
|
const controllerFiles = [];
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
const productionFiles = sourceFiles(projectRoot2);
|
|
186
|
+
for (const file of productionFiles) {
|
|
187
|
+
const imports = namedImports(
|
|
188
|
+
(0, import_node_fs.readFileSync)(file, "utf8"),
|
|
189
|
+
REACT_RUNTIME_ENTRY
|
|
190
|
+
);
|
|
146
191
|
const projectPath = (0, import_node_path.relative)(projectRoot2, file).replaceAll("\\", "/");
|
|
147
192
|
if (containsAny(imports, CLOCK_IMPORTS)) clockFiles.push(projectPath);
|
|
148
|
-
if (containsAny(imports, CONTROLLER_IMPORTS))
|
|
193
|
+
if (containsAny(imports, CONTROLLER_IMPORTS))
|
|
194
|
+
controllerFiles.push(projectPath);
|
|
149
195
|
}
|
|
196
|
+
const appPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_APP);
|
|
197
|
+
const productionEntryExists = (0, import_node_fs.existsSync)(appPath);
|
|
198
|
+
const productionUsesExample = productionFiles.some(
|
|
199
|
+
(file) => productionFileImportsExample(file, projectRoot2)
|
|
200
|
+
);
|
|
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("\\", "/"));
|
|
150
202
|
const issues = [];
|
|
203
|
+
if (staleExampleTestFiles.length > 0) {
|
|
204
|
+
issues.push(
|
|
205
|
+
`Production ${PRODUCTION_APP} no longer imports ${EXAMPLE_IMPORT_PREFIX}*, but runnable product tests still do: ${staleExampleTestFiles.join(", ")}. Replace those CollectGame tests with tests for the production game or delete genuinely inapplicable slots. Teaching examples under tests/examples/**/*.example.* remain allowed.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
151
208
|
if (clockFiles.length === 0 && controllerFiles.length === 0) {
|
|
152
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
ok: issues.length === 0,
|
|
211
|
+
issues,
|
|
212
|
+
clockFiles,
|
|
213
|
+
controllerFiles,
|
|
214
|
+
productionEntryExists,
|
|
215
|
+
productionUsesExample,
|
|
216
|
+
staleExampleTestFiles
|
|
217
|
+
};
|
|
153
218
|
}
|
|
154
219
|
const testPath = (0, import_node_path.join)(projectRoot2, PRODUCTION_PLAYTHROUGH);
|
|
155
220
|
const testSource = (0, import_node_fs.existsSync)(testPath) ? (0, import_node_fs.readFileSync)(testPath, "utf8") : "";
|
|
@@ -165,7 +230,15 @@ function auditReactAuthoritativePlaythrough(projectRoot2) {
|
|
|
165
230
|
`${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
231
|
);
|
|
167
232
|
}
|
|
168
|
-
return {
|
|
233
|
+
return {
|
|
234
|
+
ok: issues.length === 0,
|
|
235
|
+
issues,
|
|
236
|
+
clockFiles,
|
|
237
|
+
controllerFiles,
|
|
238
|
+
productionEntryExists,
|
|
239
|
+
productionUsesExample,
|
|
240
|
+
staleExampleTestFiles
|
|
241
|
+
};
|
|
169
242
|
}
|
|
170
243
|
|
|
171
244
|
// src/cli/lint.ts
|
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,9 +66,10 @@ 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 {
|
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,9 +66,10 @@ 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 {
|