miaoda-game-devkit 0.9.0 → 0.10.1
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/README.md +7 -0
- package/dist/lint/setup.mjs +91 -4
- package/dist/react/vitest-config.js +31 -8
- package/dist/react/vitest-config.mjs +32 -9
- package/dist/react/vitest-setup.js +91 -4
- package/dist/react/vitest-setup.mjs +91 -4
- package/mechanics/list-game-mechanics-source.mjs +254 -0
- package/mechanics/materialize-game-mechanics-source.mjs +22 -1
- package/mechanics/resolve-game-mechanics-source-index.mjs +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,12 +107,19 @@ Devkit 发布统一的 `miaoda` 命令。裸 `miaoda-game-*` 包名默认从公
|
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
pnpm exec miaoda mechanics --help
|
|
110
|
+
pnpm exec miaoda mechanics list
|
|
111
|
+
pnpm exec miaoda mechanics list --domain=grid --engine=react
|
|
110
112
|
pnpm exec miaoda mechanics add miaoda-game-beam-core
|
|
111
113
|
pnpm exec miaoda mechanics add miaoda-game-beam-core@1.2.3 \
|
|
112
114
|
--source-index=https://public.example.com/game-mechanics/stable.json
|
|
113
115
|
pnpm exec miaoda mechanics status
|
|
114
116
|
```
|
|
115
117
|
|
|
118
|
+
`list` 只列出稳定索引中实际可安装的包,并从 Devkit 自带的 capabilities JSON 动态汇总
|
|
119
|
+
domain 的 `owns` 能力;带筛选时会显示包负责和不负责的边界、使用指引及可测试性。它不会安装
|
|
120
|
+
源码,也不会维护另一份容易过时的文字目录。先选择覆盖需求的最小包集合,再单独运行 `add`,
|
|
121
|
+
成功后阅读生成的 `src/game-mechanics/README.md`。
|
|
122
|
+
|
|
116
123
|
默认索引是 `https://resource-static.bj.bcebos.com/miaoda-game/stable.json`。只有调试、测试或
|
|
117
124
|
私有镜像场景才需要通过 `--source-index` 或 `MIAODA_MECHANICS_INDEX_URL` 覆盖。
|
|
118
125
|
|
package/dist/lint/setup.mjs
CHANGED
|
@@ -1,8 +1,92 @@
|
|
|
1
1
|
// src/lint/setup.ts
|
|
2
2
|
import { afterEach } from "vitest";
|
|
3
3
|
|
|
4
|
-
// src/testing/jsdom-
|
|
4
|
+
// src/testing/jsdom-webgl.ts
|
|
5
|
+
var CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
6
|
+
var TEXTURE_UNIT_LIMIT = 32;
|
|
7
|
+
var TEXTURE_SIZE_LIMIT = 4096;
|
|
8
|
+
var VIEWPORT_LIMIT = 4096;
|
|
9
|
+
function parameterValue(name) {
|
|
10
|
+
if (name === "VERSION") return "WebGL 2.0 (miaoda-game-devkit stub)";
|
|
11
|
+
if (name === "SHADING_LANGUAGE_VERSION") return "WebGL GLSL ES 3.00 (stub)";
|
|
12
|
+
if (name === "VENDOR" || name === "RENDERER") return "miaoda-game-devkit";
|
|
13
|
+
if (name === "MAX_VIEWPORT_DIMS") {
|
|
14
|
+
return new Int32Array([VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
15
|
+
}
|
|
16
|
+
if (name === "VIEWPORT" || name === "SCISSOR_BOX") {
|
|
17
|
+
return new Int32Array([0, 0, VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
18
|
+
}
|
|
19
|
+
if (name.includes("MAX_") && name.includes("SIZE")) return TEXTURE_SIZE_LIMIT;
|
|
20
|
+
if (name.startsWith("MAX_")) return TEXTURE_UNIT_LIMIT;
|
|
21
|
+
if (name.endsWith("_BITS")) return 8;
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
function createWebGLContext(canvas, attributes) {
|
|
25
|
+
const constants = /* @__PURE__ */ new Map();
|
|
26
|
+
const constantNames = /* @__PURE__ */ new Map();
|
|
27
|
+
const state = {
|
|
28
|
+
canvas,
|
|
29
|
+
drawingBufferWidth: canvas.width,
|
|
30
|
+
drawingBufferHeight: canvas.height
|
|
31
|
+
};
|
|
32
|
+
const method = (name) => {
|
|
33
|
+
if (name === "getContextAttributes") return () => attributes;
|
|
34
|
+
if (name === "getExtension") return () => null;
|
|
35
|
+
if (name === "getSupportedExtensions") return () => [];
|
|
36
|
+
if (name === "getParameter") {
|
|
37
|
+
return (pname) => parameterValue(constantNames.get(pname) ?? "UNKNOWN");
|
|
38
|
+
}
|
|
39
|
+
if (name === "getShaderPrecisionFormat") {
|
|
40
|
+
return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
41
|
+
}
|
|
42
|
+
if (name === "getShaderInfoLog" || name === "getProgramInfoLog") return () => "";
|
|
43
|
+
if (name === "getError") return () => 0;
|
|
44
|
+
if (name === "isContextLost") return () => false;
|
|
45
|
+
if (name.startsWith("get") && name.endsWith("Parameter")) return () => true;
|
|
46
|
+
if (name.startsWith("get")) return () => null;
|
|
47
|
+
if (name.startsWith("create") || name.startsWith("getUniformLocation")) {
|
|
48
|
+
return () => ({});
|
|
49
|
+
}
|
|
50
|
+
return () => void 0;
|
|
51
|
+
};
|
|
52
|
+
return new Proxy(state, {
|
|
53
|
+
get(target, property) {
|
|
54
|
+
if (property in target) return target[property];
|
|
55
|
+
if (typeof property !== "string") return void 0;
|
|
56
|
+
if (CONSTANT_NAME.test(property)) {
|
|
57
|
+
const existing = constants.get(property);
|
|
58
|
+
if (existing !== void 0) return existing;
|
|
59
|
+
const value = constants.size + 1;
|
|
60
|
+
constants.set(property, value);
|
|
61
|
+
constantNames.set(value, property);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
const implementation = method(property);
|
|
65
|
+
target[property] = implementation;
|
|
66
|
+
return implementation;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
5
70
|
var contexts = /* @__PURE__ */ new WeakMap();
|
|
71
|
+
function getJSDOMWebGLContext(canvas, options) {
|
|
72
|
+
const existing = contexts.get(canvas);
|
|
73
|
+
if (existing) return existing;
|
|
74
|
+
const attributes = {
|
|
75
|
+
alpha: true,
|
|
76
|
+
antialias: false,
|
|
77
|
+
depth: true,
|
|
78
|
+
premultipliedAlpha: true,
|
|
79
|
+
preserveDrawingBuffer: false,
|
|
80
|
+
stencil: false,
|
|
81
|
+
...typeof options === "object" && options !== null ? options : {}
|
|
82
|
+
};
|
|
83
|
+
const context = createWebGLContext(canvas, attributes);
|
|
84
|
+
contexts.set(canvas, context);
|
|
85
|
+
return context;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/testing/jsdom-canvas.ts
|
|
89
|
+
var contexts2 = /* @__PURE__ */ new WeakMap();
|
|
6
90
|
function createCanvasContext(canvas) {
|
|
7
91
|
const imageData = (width = 1, height = 1) => ({
|
|
8
92
|
data: new Uint8ClampedArray(width * height * 4),
|
|
@@ -49,12 +133,15 @@ function createCanvasContext(canvas) {
|
|
|
49
133
|
return context;
|
|
50
134
|
}
|
|
51
135
|
function installJSDOMCanvasContext() {
|
|
52
|
-
HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
|
|
136
|
+
HTMLCanvasElement.prototype.getContext = function getContext(contextId, options) {
|
|
137
|
+
if (contextId === "webgl" || contextId === "webgl2") {
|
|
138
|
+
return getJSDOMWebGLContext(this, options);
|
|
139
|
+
}
|
|
53
140
|
if (contextId !== "2d") return null;
|
|
54
|
-
const existing =
|
|
141
|
+
const existing = contexts2.get(this);
|
|
55
142
|
if (existing) return existing;
|
|
56
143
|
const context = createCanvasContext(this);
|
|
57
|
-
|
|
144
|
+
contexts2.set(this, context);
|
|
58
145
|
return context;
|
|
59
146
|
};
|
|
60
147
|
}
|
|
@@ -1452,7 +1452,36 @@ function resolvePhaser3BrowserEntry(projectRoot) {
|
|
|
1452
1452
|
return void 0;
|
|
1453
1453
|
}
|
|
1454
1454
|
}
|
|
1455
|
+
var COVERAGE_INCLUDE = [
|
|
1456
|
+
"src/game/core/**/*.{ts,tsx}",
|
|
1457
|
+
"src/game/runtime/**/*.{ts,tsx}"
|
|
1458
|
+
];
|
|
1459
|
+
var COVERAGE_EXCLUDE = [
|
|
1460
|
+
"src/game/{core,runtime}/**/*.d.ts",
|
|
1461
|
+
"src/game/{core,runtime}/**/*.{spec,test}.{ts,tsx}"
|
|
1462
|
+
];
|
|
1463
|
+
var GATE_SCAN_EXCLUDE = [
|
|
1464
|
+
"src/**/*.d.ts",
|
|
1465
|
+
"src/**/*.{spec,test}.{ts,tsx}",
|
|
1466
|
+
"src/game/example/**"
|
|
1467
|
+
];
|
|
1468
|
+
function assertCoverageScopeIsPopulated(projectRoot) {
|
|
1469
|
+
const scan = (pattern) => (0, import_node_fs3.globSync)(pattern, { cwd: projectRoot, exclude: GATE_SCAN_EXCLUDE });
|
|
1470
|
+
if (COVERAGE_INCLUDE.flatMap(scan).length > 0) return;
|
|
1471
|
+
const misplaced = scan("src/**/{core,runtime}/**/*.{ts,tsx}");
|
|
1472
|
+
if (misplaced.length === 0) return;
|
|
1473
|
+
throw new Error(
|
|
1474
|
+
[
|
|
1475
|
+
"The coverage gate matched no source file, so it proves nothing.",
|
|
1476
|
+
"Deterministic rules belong in src/game/core/; the loop, scheduling,",
|
|
1477
|
+
"Controller, and Telemetry belong in src/game/runtime/.",
|
|
1478
|
+
"Move these files under those two directories:",
|
|
1479
|
+
...misplaced.slice(0, 10).map((file) => ` ${file}`)
|
|
1480
|
+
].join("\n")
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1455
1483
|
function defineReactGameVitestConfig(options) {
|
|
1484
|
+
assertCoverageScopeIsPopulated(options.projectRoot);
|
|
1456
1485
|
const phaser3BrowserEntry = resolvePhaser3BrowserEntry(options.projectRoot);
|
|
1457
1486
|
return (0, import_config.defineConfig)({
|
|
1458
1487
|
// Keep discovery and dependency resolution anchored to the generated app even
|
|
@@ -1495,14 +1524,8 @@ function defineReactGameVitestConfig(options) {
|
|
|
1495
1524
|
clearMocks: true,
|
|
1496
1525
|
coverage: {
|
|
1497
1526
|
provider: "v8",
|
|
1498
|
-
include:
|
|
1499
|
-
|
|
1500
|
-
"src/game/runtime/**/*.{ts,tsx}"
|
|
1501
|
-
],
|
|
1502
|
-
exclude: [
|
|
1503
|
-
"src/game/{core,runtime}/**/*.d.ts",
|
|
1504
|
-
"src/game/{core,runtime}/**/*.{spec,test}.{ts,tsx}"
|
|
1505
|
-
],
|
|
1527
|
+
include: COVERAGE_INCLUDE,
|
|
1528
|
+
exclude: COVERAGE_EXCLUDE,
|
|
1506
1529
|
reporter: ["text-summary"],
|
|
1507
1530
|
thresholds: {
|
|
1508
1531
|
perFile: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/react-vitest-config.ts
|
|
2
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
2
|
+
import { existsSync as existsSync3, globSync, readFileSync as readFileSync2 } from "fs";
|
|
3
3
|
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
4
4
|
import { defineConfig } from "vitest/config";
|
|
5
5
|
|
|
@@ -1418,7 +1418,36 @@ function resolvePhaser3BrowserEntry(projectRoot) {
|
|
|
1418
1418
|
return void 0;
|
|
1419
1419
|
}
|
|
1420
1420
|
}
|
|
1421
|
+
var COVERAGE_INCLUDE = [
|
|
1422
|
+
"src/game/core/**/*.{ts,tsx}",
|
|
1423
|
+
"src/game/runtime/**/*.{ts,tsx}"
|
|
1424
|
+
];
|
|
1425
|
+
var COVERAGE_EXCLUDE = [
|
|
1426
|
+
"src/game/{core,runtime}/**/*.d.ts",
|
|
1427
|
+
"src/game/{core,runtime}/**/*.{spec,test}.{ts,tsx}"
|
|
1428
|
+
];
|
|
1429
|
+
var GATE_SCAN_EXCLUDE = [
|
|
1430
|
+
"src/**/*.d.ts",
|
|
1431
|
+
"src/**/*.{spec,test}.{ts,tsx}",
|
|
1432
|
+
"src/game/example/**"
|
|
1433
|
+
];
|
|
1434
|
+
function assertCoverageScopeIsPopulated(projectRoot) {
|
|
1435
|
+
const scan = (pattern) => globSync(pattern, { cwd: projectRoot, exclude: GATE_SCAN_EXCLUDE });
|
|
1436
|
+
if (COVERAGE_INCLUDE.flatMap(scan).length > 0) return;
|
|
1437
|
+
const misplaced = scan("src/**/{core,runtime}/**/*.{ts,tsx}");
|
|
1438
|
+
if (misplaced.length === 0) return;
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
[
|
|
1441
|
+
"The coverage gate matched no source file, so it proves nothing.",
|
|
1442
|
+
"Deterministic rules belong in src/game/core/; the loop, scheduling,",
|
|
1443
|
+
"Controller, and Telemetry belong in src/game/runtime/.",
|
|
1444
|
+
"Move these files under those two directories:",
|
|
1445
|
+
...misplaced.slice(0, 10).map((file) => ` ${file}`)
|
|
1446
|
+
].join("\n")
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1421
1449
|
function defineReactGameVitestConfig(options) {
|
|
1450
|
+
assertCoverageScopeIsPopulated(options.projectRoot);
|
|
1422
1451
|
const phaser3BrowserEntry = resolvePhaser3BrowserEntry(options.projectRoot);
|
|
1423
1452
|
return defineConfig({
|
|
1424
1453
|
// Keep discovery and dependency resolution anchored to the generated app even
|
|
@@ -1461,14 +1490,8 @@ function defineReactGameVitestConfig(options) {
|
|
|
1461
1490
|
clearMocks: true,
|
|
1462
1491
|
coverage: {
|
|
1463
1492
|
provider: "v8",
|
|
1464
|
-
include:
|
|
1465
|
-
|
|
1466
|
-
"src/game/runtime/**/*.{ts,tsx}"
|
|
1467
|
-
],
|
|
1468
|
-
exclude: [
|
|
1469
|
-
"src/game/{core,runtime}/**/*.d.ts",
|
|
1470
|
-
"src/game/{core,runtime}/**/*.{spec,test}.{ts,tsx}"
|
|
1471
|
-
],
|
|
1493
|
+
include: COVERAGE_INCLUDE,
|
|
1494
|
+
exclude: COVERAGE_EXCLUDE,
|
|
1472
1495
|
reporter: ["text-summary"],
|
|
1473
1496
|
thresholds: {
|
|
1474
1497
|
perFile: true,
|
|
@@ -184,8 +184,92 @@ function runtimeFailureError(code, value) {
|
|
|
184
184
|
return error;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// src/testing/jsdom-
|
|
187
|
+
// src/testing/jsdom-webgl.ts
|
|
188
|
+
var CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
189
|
+
var TEXTURE_UNIT_LIMIT = 32;
|
|
190
|
+
var TEXTURE_SIZE_LIMIT = 4096;
|
|
191
|
+
var VIEWPORT_LIMIT = 4096;
|
|
192
|
+
function parameterValue(name) {
|
|
193
|
+
if (name === "VERSION") return "WebGL 2.0 (miaoda-game-devkit stub)";
|
|
194
|
+
if (name === "SHADING_LANGUAGE_VERSION") return "WebGL GLSL ES 3.00 (stub)";
|
|
195
|
+
if (name === "VENDOR" || name === "RENDERER") return "miaoda-game-devkit";
|
|
196
|
+
if (name === "MAX_VIEWPORT_DIMS") {
|
|
197
|
+
return new Int32Array([VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
198
|
+
}
|
|
199
|
+
if (name === "VIEWPORT" || name === "SCISSOR_BOX") {
|
|
200
|
+
return new Int32Array([0, 0, VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
201
|
+
}
|
|
202
|
+
if (name.includes("MAX_") && name.includes("SIZE")) return TEXTURE_SIZE_LIMIT;
|
|
203
|
+
if (name.startsWith("MAX_")) return TEXTURE_UNIT_LIMIT;
|
|
204
|
+
if (name.endsWith("_BITS")) return 8;
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
function createWebGLContext(canvas, attributes) {
|
|
208
|
+
const constants = /* @__PURE__ */ new Map();
|
|
209
|
+
const constantNames = /* @__PURE__ */ new Map();
|
|
210
|
+
const state = {
|
|
211
|
+
canvas,
|
|
212
|
+
drawingBufferWidth: canvas.width,
|
|
213
|
+
drawingBufferHeight: canvas.height
|
|
214
|
+
};
|
|
215
|
+
const method = (name) => {
|
|
216
|
+
if (name === "getContextAttributes") return () => attributes;
|
|
217
|
+
if (name === "getExtension") return () => null;
|
|
218
|
+
if (name === "getSupportedExtensions") return () => [];
|
|
219
|
+
if (name === "getParameter") {
|
|
220
|
+
return (pname) => parameterValue(constantNames.get(pname) ?? "UNKNOWN");
|
|
221
|
+
}
|
|
222
|
+
if (name === "getShaderPrecisionFormat") {
|
|
223
|
+
return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
224
|
+
}
|
|
225
|
+
if (name === "getShaderInfoLog" || name === "getProgramInfoLog") return () => "";
|
|
226
|
+
if (name === "getError") return () => 0;
|
|
227
|
+
if (name === "isContextLost") return () => false;
|
|
228
|
+
if (name.startsWith("get") && name.endsWith("Parameter")) return () => true;
|
|
229
|
+
if (name.startsWith("get")) return () => null;
|
|
230
|
+
if (name.startsWith("create") || name.startsWith("getUniformLocation")) {
|
|
231
|
+
return () => ({});
|
|
232
|
+
}
|
|
233
|
+
return () => void 0;
|
|
234
|
+
};
|
|
235
|
+
return new Proxy(state, {
|
|
236
|
+
get(target, property) {
|
|
237
|
+
if (property in target) return target[property];
|
|
238
|
+
if (typeof property !== "string") return void 0;
|
|
239
|
+
if (CONSTANT_NAME.test(property)) {
|
|
240
|
+
const existing = constants.get(property);
|
|
241
|
+
if (existing !== void 0) return existing;
|
|
242
|
+
const value = constants.size + 1;
|
|
243
|
+
constants.set(property, value);
|
|
244
|
+
constantNames.set(value, property);
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
const implementation = method(property);
|
|
248
|
+
target[property] = implementation;
|
|
249
|
+
return implementation;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
188
253
|
var contexts = /* @__PURE__ */ new WeakMap();
|
|
254
|
+
function getJSDOMWebGLContext(canvas, options) {
|
|
255
|
+
const existing = contexts.get(canvas);
|
|
256
|
+
if (existing) return existing;
|
|
257
|
+
const attributes = {
|
|
258
|
+
alpha: true,
|
|
259
|
+
antialias: false,
|
|
260
|
+
depth: true,
|
|
261
|
+
premultipliedAlpha: true,
|
|
262
|
+
preserveDrawingBuffer: false,
|
|
263
|
+
stencil: false,
|
|
264
|
+
...typeof options === "object" && options !== null ? options : {}
|
|
265
|
+
};
|
|
266
|
+
const context = createWebGLContext(canvas, attributes);
|
|
267
|
+
contexts.set(canvas, context);
|
|
268
|
+
return context;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/testing/jsdom-canvas.ts
|
|
272
|
+
var contexts2 = /* @__PURE__ */ new WeakMap();
|
|
189
273
|
function createCanvasContext(canvas) {
|
|
190
274
|
const imageData = (width = 1, height = 1) => ({
|
|
191
275
|
data: new Uint8ClampedArray(width * height * 4),
|
|
@@ -232,12 +316,15 @@ function createCanvasContext(canvas) {
|
|
|
232
316
|
return context;
|
|
233
317
|
}
|
|
234
318
|
function installJSDOMCanvasContext() {
|
|
235
|
-
HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
|
|
319
|
+
HTMLCanvasElement.prototype.getContext = function getContext(contextId, options) {
|
|
320
|
+
if (contextId === "webgl" || contextId === "webgl2") {
|
|
321
|
+
return getJSDOMWebGLContext(this, options);
|
|
322
|
+
}
|
|
236
323
|
if (contextId !== "2d") return null;
|
|
237
|
-
const existing =
|
|
324
|
+
const existing = contexts2.get(this);
|
|
238
325
|
if (existing) return existing;
|
|
239
326
|
const context = createCanvasContext(this);
|
|
240
|
-
|
|
327
|
+
contexts2.set(this, context);
|
|
241
328
|
return context;
|
|
242
329
|
};
|
|
243
330
|
}
|
|
@@ -182,8 +182,92 @@ function runtimeFailureError(code, value) {
|
|
|
182
182
|
return error;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
// src/testing/jsdom-
|
|
185
|
+
// src/testing/jsdom-webgl.ts
|
|
186
|
+
var CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
187
|
+
var TEXTURE_UNIT_LIMIT = 32;
|
|
188
|
+
var TEXTURE_SIZE_LIMIT = 4096;
|
|
189
|
+
var VIEWPORT_LIMIT = 4096;
|
|
190
|
+
function parameterValue(name) {
|
|
191
|
+
if (name === "VERSION") return "WebGL 2.0 (miaoda-game-devkit stub)";
|
|
192
|
+
if (name === "SHADING_LANGUAGE_VERSION") return "WebGL GLSL ES 3.00 (stub)";
|
|
193
|
+
if (name === "VENDOR" || name === "RENDERER") return "miaoda-game-devkit";
|
|
194
|
+
if (name === "MAX_VIEWPORT_DIMS") {
|
|
195
|
+
return new Int32Array([VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
196
|
+
}
|
|
197
|
+
if (name === "VIEWPORT" || name === "SCISSOR_BOX") {
|
|
198
|
+
return new Int32Array([0, 0, VIEWPORT_LIMIT, VIEWPORT_LIMIT]);
|
|
199
|
+
}
|
|
200
|
+
if (name.includes("MAX_") && name.includes("SIZE")) return TEXTURE_SIZE_LIMIT;
|
|
201
|
+
if (name.startsWith("MAX_")) return TEXTURE_UNIT_LIMIT;
|
|
202
|
+
if (name.endsWith("_BITS")) return 8;
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
function createWebGLContext(canvas, attributes) {
|
|
206
|
+
const constants = /* @__PURE__ */ new Map();
|
|
207
|
+
const constantNames = /* @__PURE__ */ new Map();
|
|
208
|
+
const state = {
|
|
209
|
+
canvas,
|
|
210
|
+
drawingBufferWidth: canvas.width,
|
|
211
|
+
drawingBufferHeight: canvas.height
|
|
212
|
+
};
|
|
213
|
+
const method = (name) => {
|
|
214
|
+
if (name === "getContextAttributes") return () => attributes;
|
|
215
|
+
if (name === "getExtension") return () => null;
|
|
216
|
+
if (name === "getSupportedExtensions") return () => [];
|
|
217
|
+
if (name === "getParameter") {
|
|
218
|
+
return (pname) => parameterValue(constantNames.get(pname) ?? "UNKNOWN");
|
|
219
|
+
}
|
|
220
|
+
if (name === "getShaderPrecisionFormat") {
|
|
221
|
+
return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
222
|
+
}
|
|
223
|
+
if (name === "getShaderInfoLog" || name === "getProgramInfoLog") return () => "";
|
|
224
|
+
if (name === "getError") return () => 0;
|
|
225
|
+
if (name === "isContextLost") return () => false;
|
|
226
|
+
if (name.startsWith("get") && name.endsWith("Parameter")) return () => true;
|
|
227
|
+
if (name.startsWith("get")) return () => null;
|
|
228
|
+
if (name.startsWith("create") || name.startsWith("getUniformLocation")) {
|
|
229
|
+
return () => ({});
|
|
230
|
+
}
|
|
231
|
+
return () => void 0;
|
|
232
|
+
};
|
|
233
|
+
return new Proxy(state, {
|
|
234
|
+
get(target, property) {
|
|
235
|
+
if (property in target) return target[property];
|
|
236
|
+
if (typeof property !== "string") return void 0;
|
|
237
|
+
if (CONSTANT_NAME.test(property)) {
|
|
238
|
+
const existing = constants.get(property);
|
|
239
|
+
if (existing !== void 0) return existing;
|
|
240
|
+
const value = constants.size + 1;
|
|
241
|
+
constants.set(property, value);
|
|
242
|
+
constantNames.set(value, property);
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
const implementation = method(property);
|
|
246
|
+
target[property] = implementation;
|
|
247
|
+
return implementation;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
186
251
|
var contexts = /* @__PURE__ */ new WeakMap();
|
|
252
|
+
function getJSDOMWebGLContext(canvas, options) {
|
|
253
|
+
const existing = contexts.get(canvas);
|
|
254
|
+
if (existing) return existing;
|
|
255
|
+
const attributes = {
|
|
256
|
+
alpha: true,
|
|
257
|
+
antialias: false,
|
|
258
|
+
depth: true,
|
|
259
|
+
premultipliedAlpha: true,
|
|
260
|
+
preserveDrawingBuffer: false,
|
|
261
|
+
stencil: false,
|
|
262
|
+
...typeof options === "object" && options !== null ? options : {}
|
|
263
|
+
};
|
|
264
|
+
const context = createWebGLContext(canvas, attributes);
|
|
265
|
+
contexts.set(canvas, context);
|
|
266
|
+
return context;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/testing/jsdom-canvas.ts
|
|
270
|
+
var contexts2 = /* @__PURE__ */ new WeakMap();
|
|
187
271
|
function createCanvasContext(canvas) {
|
|
188
272
|
const imageData = (width = 1, height = 1) => ({
|
|
189
273
|
data: new Uint8ClampedArray(width * height * 4),
|
|
@@ -230,12 +314,15 @@ function createCanvasContext(canvas) {
|
|
|
230
314
|
return context;
|
|
231
315
|
}
|
|
232
316
|
function installJSDOMCanvasContext() {
|
|
233
|
-
HTMLCanvasElement.prototype.getContext = function getContext(contextId) {
|
|
317
|
+
HTMLCanvasElement.prototype.getContext = function getContext(contextId, options) {
|
|
318
|
+
if (contextId === "webgl" || contextId === "webgl2") {
|
|
319
|
+
return getJSDOMWebGLContext(this, options);
|
|
320
|
+
}
|
|
234
321
|
if (contextId !== "2d") return null;
|
|
235
|
-
const existing =
|
|
322
|
+
const existing = contexts2.get(this);
|
|
236
323
|
if (existing) return existing;
|
|
237
324
|
const context = createCanvasContext(this);
|
|
238
|
-
|
|
325
|
+
contexts2.set(this, context);
|
|
239
326
|
return context;
|
|
240
327
|
};
|
|
241
328
|
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { listIndexedMechanics } from './resolve-game-mechanics-source-index.mjs';
|
|
3
|
+
|
|
4
|
+
const ENGINES = new Set(['neutral', 'react', 'phaser', 'cocos']);
|
|
5
|
+
const FILTER_NAMES = ['domain', 'engine', 'owns'];
|
|
6
|
+
|
|
7
|
+
function readCapabilityDocument(path) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
10
|
+
} catch (error) {
|
|
11
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
12
|
+
throw new Error(`miaoda mechanics list: cannot read capabilities JSON: ${detail}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function optionValue(argv, name) {
|
|
17
|
+
const prefix = `--${name}=`;
|
|
18
|
+
const argument = argv.find((value) => value.startsWith(prefix));
|
|
19
|
+
return argument?.slice(prefix.length).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseMechanicListArguments(argv, defaultSourceIndexUrl) {
|
|
23
|
+
const allowed = new Set(['--json', '--help', '-h']);
|
|
24
|
+
for (const argument of argv.slice(1)) {
|
|
25
|
+
if (allowed.has(argument)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (['--domain=', '--engine=', '--owns=', '--source-index='].some((prefix) => argument.startsWith(prefix))) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(
|
|
32
|
+
`miaoda mechanics list: unsupported argument ${JSON.stringify(argument)}. ` +
|
|
33
|
+
'Use --domain=<domain>, --engine=<engine>, --owns=<capability>, or --json.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const filters = Object.fromEntries(
|
|
37
|
+
FILTER_NAMES.map((name) => [name, optionValue(argv, name)]).filter(([, value]) => Boolean(value)),
|
|
38
|
+
);
|
|
39
|
+
if (filters.engine && !ENGINES.has(filters.engine)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`miaoda mechanics list: engine must be neutral, react, phaser, or cocos; received ${filters.engine}.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
filters,
|
|
46
|
+
json: argv.includes('--json'),
|
|
47
|
+
sourceIndexUrl: optionValue(argv, 'source-index') || defaultSourceIndexUrl,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function packageMatches(entry, filters) {
|
|
52
|
+
if (!entry.annotated) {
|
|
53
|
+
return Object.keys(filters).length === 0;
|
|
54
|
+
}
|
|
55
|
+
if (filters.domain && !entry.domains.includes(filters.domain)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (filters.owns && !entry.provides.includes(filters.owns)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (filters.engine) {
|
|
62
|
+
const compatible =
|
|
63
|
+
filters.engine === 'neutral'
|
|
64
|
+
? entry.engine === 'neutral'
|
|
65
|
+
: entry.engine === 'neutral' || entry.engine === filters.engine;
|
|
66
|
+
if (!compatible) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function packageEntry(name, version, capability, installableNames) {
|
|
74
|
+
if (!capability) {
|
|
75
|
+
return { name, version, annotated: false };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
version,
|
|
80
|
+
annotated: true,
|
|
81
|
+
engine: capability.engine,
|
|
82
|
+
domains: capability.domains,
|
|
83
|
+
provides: capability.owns,
|
|
84
|
+
leavesOutside: capability.doesNotOwn,
|
|
85
|
+
compatibleWith: capability.compatibleWith.filter((candidate) => installableNames.has(candidate)),
|
|
86
|
+
useFor: capability.guidance?.useFor ?? [],
|
|
87
|
+
keepOutside: capability.guidance?.keepOutside ?? [],
|
|
88
|
+
rules: capability.guidance?.rules ?? [],
|
|
89
|
+
useInsteadWhen: capability.useInsteadWhen,
|
|
90
|
+
testability: capability.testability,
|
|
91
|
+
persistence: capability.persistence,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function summarizeDomains(packages) {
|
|
96
|
+
const domains = new Map();
|
|
97
|
+
for (const entry of packages) {
|
|
98
|
+
if (!entry.annotated) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const domain of entry.domains) {
|
|
102
|
+
const summary = domains.get(domain) ?? { name: domain, packageNames: new Set(), provides: new Set() };
|
|
103
|
+
summary.packageNames.add(entry.name);
|
|
104
|
+
for (const capability of entry.provides) {
|
|
105
|
+
summary.provides.add(capability);
|
|
106
|
+
}
|
|
107
|
+
domains.set(domain, summary);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...domains.values()]
|
|
111
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
112
|
+
.map((summary) => ({
|
|
113
|
+
name: summary.name,
|
|
114
|
+
packageCount: summary.packageNames.size,
|
|
115
|
+
provides: [...summary.provides].sort(),
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createMechanicCatalog(indexedPackages, capabilityDocument, filters = {}, sourceIndexUrl) {
|
|
120
|
+
const installableNames = new Set(indexedPackages.map((entry) => entry.name));
|
|
121
|
+
const allPackages = indexedPackages
|
|
122
|
+
.map(({ name, version }) =>
|
|
123
|
+
packageEntry(name, version, capabilityDocument.packages?.[name], installableNames),
|
|
124
|
+
)
|
|
125
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
126
|
+
const matchedPackages = allPackages.filter((entry) => packageMatches(entry, filters));
|
|
127
|
+
const matchedNames = new Set(matchedPackages.map((entry) => entry.name));
|
|
128
|
+
const packages = matchedPackages.map((entry) => {
|
|
129
|
+
if (!entry.annotated) {
|
|
130
|
+
return entry;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...entry,
|
|
134
|
+
compatibleWith: entry.compatibleWith.filter((candidate) => matchedNames.has(candidate)),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
const filtersActive = Object.keys(filters).length > 0;
|
|
138
|
+
return {
|
|
139
|
+
schemaVersion: 1,
|
|
140
|
+
sourceIndexUrl,
|
|
141
|
+
filters,
|
|
142
|
+
installablePackageCount: allPackages.length,
|
|
143
|
+
unannotatedPackageCount: allPackages.filter((entry) => !entry.annotated).length,
|
|
144
|
+
domains: summarizeDomains(filtersActive ? packages : allPackages),
|
|
145
|
+
packages,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function listMechanics({ sourceIndexUrl, capabilityPath, filters }) {
|
|
150
|
+
const indexed = await listIndexedMechanics(sourceIndexUrl);
|
|
151
|
+
const capabilityDocument = readCapabilityDocument(capabilityPath);
|
|
152
|
+
return createMechanicCatalog(indexed.packages, capabilityDocument, filters, indexed.sourceIndexUrl);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatList(values) {
|
|
156
|
+
return values.length > 0 ? values.join(', ') : 'none declared';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatTestability(testability) {
|
|
160
|
+
if (!testability) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return `observation=${testability.observation}; advance=${testability.advance}; ` +
|
|
164
|
+
`observe: ${testability.methods.observe}; exercise: ${testability.methods.advance}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatPackage(entry) {
|
|
168
|
+
if (!entry.annotated) {
|
|
169
|
+
return `${entry.name}@${entry.version}\n Capability metadata: unavailable; inspect its README after add.`;
|
|
170
|
+
}
|
|
171
|
+
const lines = [
|
|
172
|
+
`${entry.name}@${entry.version} [${entry.engine}]`,
|
|
173
|
+
` Provides: ${formatList(entry.provides)}`,
|
|
174
|
+
` Leaves outside: ${formatList(entry.leavesOutside)}`,
|
|
175
|
+
];
|
|
176
|
+
if (entry.useFor.length > 0) {
|
|
177
|
+
lines.push(` Use for: ${entry.useFor.join(' ')}`);
|
|
178
|
+
}
|
|
179
|
+
if (entry.keepOutside.length > 0) {
|
|
180
|
+
lines.push(` Keep outside: ${entry.keepOutside.join(' ')}`);
|
|
181
|
+
}
|
|
182
|
+
if (entry.rules.length > 0) {
|
|
183
|
+
lines.push(` Rules: ${entry.rules.join(' ')}`);
|
|
184
|
+
}
|
|
185
|
+
if (entry.compatibleWith.length > 0) {
|
|
186
|
+
lines.push(` Works with: ${entry.compatibleWith.join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
for (const alternative of entry.useInsteadWhen) {
|
|
189
|
+
lines.push(` Use ${alternative.package} instead when: ${alternative.condition}`);
|
|
190
|
+
}
|
|
191
|
+
const testability = formatTestability(entry.testability);
|
|
192
|
+
if (testability) {
|
|
193
|
+
lines.push(` Testability: ${testability}`);
|
|
194
|
+
}
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatFilters(filters) {
|
|
199
|
+
const values = Object.entries(filters).map(([name, value]) => `${name}=${value}`);
|
|
200
|
+
return values.length > 0 ? values.join(', ') : 'none';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function formatMechanicList(catalog, { json = false } = {}) {
|
|
204
|
+
if (json) {
|
|
205
|
+
return JSON.stringify(catalog, null, 2);
|
|
206
|
+
}
|
|
207
|
+
const filtersActive = Object.keys(catalog.filters).length > 0;
|
|
208
|
+
if (!filtersActive) {
|
|
209
|
+
const domainLines = catalog.domains.map((domain) => {
|
|
210
|
+
const preview = domain.provides.slice(0, 3).join(', ');
|
|
211
|
+
const remaining = Math.max(0, domain.provides.length - 3);
|
|
212
|
+
const suffix = remaining > 0 ? `, +${remaining} more` : '';
|
|
213
|
+
return ` ${domain.name} (${domain.packageCount}) — can provide: ${preview}${suffix}`;
|
|
214
|
+
});
|
|
215
|
+
const annotationNote =
|
|
216
|
+
catalog.unannotatedPackageCount > 0
|
|
217
|
+
? `\n${catalog.unannotatedPackageCount} installable packages lack capability annotations.`
|
|
218
|
+
: '';
|
|
219
|
+
return `Installable Miaoda mechanic domains (${catalog.domains.length} domains, ` +
|
|
220
|
+
`${catalog.installablePackageCount} packages):\n${domainLines.join('\n')}${annotationNote}\n\n` +
|
|
221
|
+
'Next:\n' +
|
|
222
|
+
' Rerun with --domain=<domain>, --engine=<react|phaser|cocos|neutral>, or --owns=<capability>.\n' +
|
|
223
|
+
' Use --json for complete machine-readable facts. Do not install every match.';
|
|
224
|
+
}
|
|
225
|
+
if (catalog.packages.length === 0) {
|
|
226
|
+
return `No installable mechanics matched ${formatFilters(catalog.filters)}.\n` +
|
|
227
|
+
'Run "pnpm exec miaoda mechanics list" to inspect available domains and exact capability names.';
|
|
228
|
+
}
|
|
229
|
+
return `Matched ${catalog.packages.length} installable mechanic packages (${formatFilters(catalog.filters)}):\n\n` +
|
|
230
|
+
`${catalog.packages.map(formatPackage).join('\n\n')}\n\n` +
|
|
231
|
+
'Next:\n' +
|
|
232
|
+
' Select the smallest set whose Provides entries own required mechanics, then run:\n' +
|
|
233
|
+
' pnpm exec miaoda mechanics add <full miaoda-game-* names>\n' +
|
|
234
|
+
' Run add by itself, then read src/game-mechanics/README.md before gameplay edits.';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const LIST_HELP = `Usage: miaoda mechanics list [filters]
|
|
238
|
+
|
|
239
|
+
Discover packages that are currently installable through the source index. Package
|
|
240
|
+
meaning comes from the bundled capabilities JSON: owns becomes Provides, doesNotOwn
|
|
241
|
+
becomes Leaves outside, and guidance/testability are shown when annotated. This command
|
|
242
|
+
is read-only and never installs source.
|
|
243
|
+
|
|
244
|
+
Without filters, list prints domain summaries with real owned-capability previews.
|
|
245
|
+
With filters, it prints package versions and ownership boundaries. An engine filter
|
|
246
|
+
includes engine-neutral cores plus adapters for that engine.
|
|
247
|
+
|
|
248
|
+
Filters:
|
|
249
|
+
--domain=<domain> Match one exact domain from the summary
|
|
250
|
+
--engine=<engine> neutral, react, phaser, or cocos
|
|
251
|
+
--owns=<capability> Match one exact owned-capability token
|
|
252
|
+
--json Emit complete machine-readable JSON
|
|
253
|
+
--source-index=<url> Override the public stable.json URL
|
|
254
|
+
-h, --help Show this help`;
|
|
@@ -16,6 +16,12 @@ import {
|
|
|
16
16
|
} from 'node:fs';
|
|
17
17
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import {
|
|
20
|
+
formatMechanicList,
|
|
21
|
+
LIST_HELP,
|
|
22
|
+
listMechanics,
|
|
23
|
+
parseMechanicListArguments,
|
|
24
|
+
} from './list-game-mechanics-source.mjs';
|
|
19
25
|
import {
|
|
20
26
|
areIndexedPackageSpecs,
|
|
21
27
|
cleanupIndexedMechanics,
|
|
@@ -657,6 +663,7 @@ Manage editable Miaoda game-mechanic TypeScript source in src/game-mechanics.
|
|
|
657
663
|
|
|
658
664
|
Commands:
|
|
659
665
|
add <package...> Resolve packages from the source index and add editable source
|
|
666
|
+
list Discover installable packages and their ownership boundaries
|
|
660
667
|
status Show clean, modified, or missing source packages
|
|
661
668
|
help Show this help
|
|
662
669
|
|
|
@@ -860,10 +867,24 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
860
867
|
const mechanicsArguments = argv[0] === 'mechanics' ? argv.slice(1) : argv;
|
|
861
868
|
const command = mechanicsArguments[0];
|
|
862
869
|
const wantsHelp = mechanicsArguments.includes('--help') || mechanicsArguments.includes('-h');
|
|
863
|
-
if (!command || command === 'help' || (wantsHelp && !['add', 'status'].includes(command))) {
|
|
870
|
+
if (!command || command === 'help' || (wantsHelp && !['add', 'list', 'status'].includes(command))) {
|
|
864
871
|
console.log(ROOT_HELP);
|
|
865
872
|
return;
|
|
866
873
|
}
|
|
874
|
+
if (command === 'list') {
|
|
875
|
+
if (wantsHelp) {
|
|
876
|
+
console.log(LIST_HELP);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const options = parseMechanicListArguments(mechanicsArguments, DEFAULT_SOURCE_INDEX_URL);
|
|
880
|
+
const catalog = await listMechanics({
|
|
881
|
+
sourceIndexUrl: options.sourceIndexUrl,
|
|
882
|
+
capabilityPath,
|
|
883
|
+
filters: options.filters,
|
|
884
|
+
});
|
|
885
|
+
console.log(formatMechanicList(catalog, { json: options.json }));
|
|
886
|
+
return catalog;
|
|
887
|
+
}
|
|
867
888
|
if (command === 'add') {
|
|
868
889
|
if (wantsHelp) {
|
|
869
890
|
console.log(ADD_HELP);
|
|
@@ -312,6 +312,20 @@ export async function resolveIndexedMechanics({ indexUrl, previousRoots = {}, sp
|
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
export async function listIndexedMechanics(indexUrl) {
|
|
316
|
+
const loaded = await readIndex(indexUrl);
|
|
317
|
+
const packages = Object.keys(loaded.index.packages)
|
|
318
|
+
.sort()
|
|
319
|
+
.map((name) => {
|
|
320
|
+
const selected = selectVersion(loaded.index, name, undefined, loaded.indexUrl);
|
|
321
|
+
return { name: selected.name, version: selected.version };
|
|
322
|
+
});
|
|
323
|
+
return {
|
|
324
|
+
sourceIndexUrl: loaded.indexUrl,
|
|
325
|
+
packages,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
315
329
|
export function cleanupIndexedMechanics(resolution) {
|
|
316
330
|
if (resolution?.temporaryRoot) {
|
|
317
331
|
rmSync(resolution.temporaryRoot, { recursive: true, force: true });
|