@datamoc/mw_games 0.24.1 → 0.26.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/CHANGELOG.md +140 -0
- package/README.md +15 -0
- package/api-surface.json +14468 -0
- package/dist/actors/StatBlock.d.ts +2 -4
- package/dist/actors/StatBlock.d.ts.map +1 -1
- package/dist/actors/StatBlock.js +2 -4
- package/dist/actors/StatBlock.js.map +1 -1
- package/dist/ai/Heuristics.d.ts +1 -1
- package/dist/ai/Heuristics.js +1 -1
- package/dist/assets/index.d.ts +1 -0
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js.map +1 -1
- package/dist/core/Blob.d.ts.map +1 -1
- package/dist/core/Blob.js +2 -8
- package/dist/core/Blob.js.map +1 -1
- package/dist/core/Grid.d.ts +115 -0
- package/dist/core/Grid.d.ts.map +1 -1
- package/dist/core/Grid.js +125 -0
- package/dist/core/Grid.js.map +1 -1
- package/dist/core/Handles.d.ts +42 -0
- package/dist/core/Handles.d.ts.map +1 -0
- package/dist/core/Handles.js +38 -0
- package/dist/core/Handles.js.map +1 -0
- package/dist/core/HttpTransport.d.ts +14 -0
- package/dist/core/HttpTransport.d.ts.map +1 -1
- package/dist/core/HttpTransport.js +14 -0
- package/dist/core/HttpTransport.js.map +1 -1
- package/dist/core/Random.d.ts +54 -13
- package/dist/core/Random.d.ts.map +1 -1
- package/dist/core/Random.js +48 -25
- package/dist/core/Random.js.map +1 -1
- package/dist/core/index.d.ts +8 -4
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/index.js.map +1 -1
- package/dist/headless/index.d.ts +87 -0
- package/dist/headless/index.d.ts.map +1 -0
- package/dist/headless/index.js +87 -0
- package/dist/headless/index.js.map +1 -0
- package/dist/mw_games.global.js +113 -112
- package/dist/mwl/catalog.d.ts +13 -2
- package/dist/mwl/catalog.d.ts.map +1 -1
- package/dist/mwl/catalog.js +14 -5
- package/dist/mwl/catalog.js.map +1 -1
- package/dist/mwl/index.d.ts +3 -1
- package/dist/mwl/index.d.ts.map +1 -1
- package/dist/mwl/index.js +1 -0
- package/dist/mwl/index.js.map +1 -1
- package/dist/mwl/tables.d.ts +93 -0
- package/dist/mwl/tables.d.ts.map +1 -0
- package/dist/mwl/tables.js +87 -0
- package/dist/mwl/tables.js.map +1 -0
- package/dist/roguelike/Level.d.ts +1 -1
- package/dist/roguelike/Level.d.ts.map +1 -1
- package/dist/roguelike/Level.js +3 -21
- package/dist/roguelike/Level.js.map +1 -1
- package/dist/roguelike/Targeting.d.ts.map +1 -1
- package/dist/roguelike/Targeting.js +2 -1
- package/dist/roguelike/Targeting.js.map +1 -1
- package/dist/rpg/index.d.ts +1 -1
- package/dist/rpg/index.d.ts.map +1 -1
- package/dist/simulation/Runtime.d.ts +18 -2
- package/dist/simulation/Runtime.d.ts.map +1 -1
- package/dist/simulation/Runtime.js +7 -3
- package/dist/simulation/Runtime.js.map +1 -1
- package/dist/two-d/pixi-interop.d.ts +24 -1
- package/dist/two-d/pixi-interop.d.ts.map +1 -1
- package/dist/two-d/pixi-interop.js +23 -1
- package/dist/two-d/pixi-interop.js.map +1 -1
- package/dist/two-d/render/index.d.ts +2 -2
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +16 -3
- package/sbom.cdx.json +6 -6
- package/tools/audit-adoptions.d.mts +65 -0
- package/tools/audit-adoptions.mjs +252 -0
- package/tools/browser-driver.d.mts +78 -0
- package/tools/browser-driver.mjs +642 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The consumer adoption auditor: the mechanism behind "every export is adopted or
|
|
7
|
+
* explicitly reasoned", shipped so no consumer rebuilds the gate by hand.
|
|
8
|
+
*
|
|
9
|
+
* A framework that ships its doctrine as prose makes every consumer rebuild the same
|
|
10
|
+
* audit - which exports are used, which are deliberately not, which local definitions
|
|
11
|
+
* shadow framework names, which directories may only import types, which subpaths are
|
|
12
|
+
* denied, and how many files may reach past the facade - differently, and let it rot.
|
|
13
|
+
* This tool is the shared checker; every value expressing *policy* (the reasons, the
|
|
14
|
+
* globs, the budgets) stays in the consumer repository.
|
|
15
|
+
*
|
|
16
|
+
* It reads the P28 manifest (`api-surface.json`: `{ name, kind, subpath,
|
|
17
|
+
* declaringModule }` per export), the consumer's own import rows and local
|
|
18
|
+
* definitions (via `scanImports`/`scanLocals`, or any producer of the same shapes),
|
|
19
|
+
* and a consumer-authored config:
|
|
20
|
+
*
|
|
21
|
+
* ```json
|
|
22
|
+
* {
|
|
23
|
+
* "reasons": {
|
|
24
|
+
* "./board :: AlphaBetaGame": "chess engine, no board game in this project",
|
|
25
|
+
* "local :: World": "documented collision: the game's own map container, never the framework export"
|
|
26
|
+
* },
|
|
27
|
+
* "denySubpaths": ["./three-d"],
|
|
28
|
+
* "typeOnlyDirs": ["src/simulation"],
|
|
29
|
+
* "budgets": [{ "match": "./two-d/pixi-interop", "limit": 37, "reason": "escape hatch, one reason per file" }]
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Reasons are keyed either by export (`<subpath> :: <name>`, see `adoptionKey`) or by
|
|
34
|
+
* `local :: <Name>` for a documented shadowing collision. The audit reports, with
|
|
35
|
+
* `file:line` and a non-zero exit:
|
|
36
|
+
*
|
|
37
|
+
* - `unadopted` - a manifest export the consumer neither imports nor reasons about;
|
|
38
|
+
* - `stale-reason` - a reason naming no manifest export and no local definition;
|
|
39
|
+
* - `collision` - a local definition shadowing a never-imported manifest export with
|
|
40
|
+
* no `local ::` reason documenting it;
|
|
41
|
+
* - `denied-subpath` - an import from a denied subpath (a denied prefix also covers
|
|
42
|
+
* its children: `./two-d` covers `./two-d/render`);
|
|
43
|
+
* - `type-only` - a runtime import from a file under a type-only directory;
|
|
44
|
+
* - `budget` - more importing files than a budget allows, naming the first file past
|
|
45
|
+
* the limit.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export function adoptionKey(subpath, name) {
|
|
49
|
+
return `${subpath} :: ${name}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isUnder(file, dir) {
|
|
53
|
+
return file === dir || file.startsWith(`${dir}/`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function subpathMatches(subpath, match) {
|
|
57
|
+
return subpath === match || subpath.startsWith(`${match}/`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every `import ... from '@datamoc/mw_games/<sub>'` row in the given files, as
|
|
62
|
+
* `{ file, line, subpath, names, typeOnly }`. `import type` rows are type-only; a
|
|
63
|
+
* mixed value/type named list counts as runtime (conservative: the file still pulls
|
|
64
|
+
* the module in at runtime). A default or namespace import carries no names, so it
|
|
65
|
+
* adopts the whole subpath.
|
|
66
|
+
*/
|
|
67
|
+
export function scanImports(files) {
|
|
68
|
+
const rows = [];
|
|
69
|
+
const pattern =
|
|
70
|
+
/^\s*import\s+(type\s+)?(?:\*\s+as\s+(\w+)|{([^}]*)}|(\w+))?\s*from\s*['"]@datamoc\/mw_games(\/[^'"]*)?['"]/gm;
|
|
71
|
+
|
|
72
|
+
for (const { path, text } of files) {
|
|
73
|
+
for (const match of text.matchAll(pattern)) {
|
|
74
|
+
const line = text.slice(0, match.index).split('\n').length;
|
|
75
|
+
const subpath = match[5] === undefined ? '.' : `.${match[5]}`;
|
|
76
|
+
const names = (match[3] ?? '')
|
|
77
|
+
.split(',')
|
|
78
|
+
.map((entry) =>
|
|
79
|
+
entry
|
|
80
|
+
.trim()
|
|
81
|
+
.replace(/^type\s+/, '')
|
|
82
|
+
.split(/\s+as\s+/)
|
|
83
|
+
.pop()
|
|
84
|
+
.trim(),
|
|
85
|
+
)
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
rows.push({ file: path, line, subpath, names, typeOnly: Boolean(match[1]) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return rows;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Every top-level `interface`/`type`/`class`/`function`/`const`/`enum` the consumer
|
|
96
|
+
* defines itself, as `{ name, file, line }` - the shadowing candidates.
|
|
97
|
+
*/
|
|
98
|
+
export function scanLocals(files) {
|
|
99
|
+
const locals = [];
|
|
100
|
+
const pattern = /^(?:export\s+)?(?:interface|type|class|function|const|enum)\s+([A-Za-z0-9_]+)/gm;
|
|
101
|
+
|
|
102
|
+
for (const { path, text } of files) {
|
|
103
|
+
for (const match of text.matchAll(pattern)) {
|
|
104
|
+
const line = text.slice(0, match.index).split('\n').length;
|
|
105
|
+
locals.push({ name: match[1], file: path, line });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return locals;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Runs every rule over the manifest, the consumer's import rows and locals, and its
|
|
114
|
+
* config. Returns findings in a stable order (rule, then message); empty means the
|
|
115
|
+
* adoption is fully accounted for.
|
|
116
|
+
*/
|
|
117
|
+
export function auditAdoptions(manifest, imports, locals, config) {
|
|
118
|
+
const findings = [];
|
|
119
|
+
const reasons = config.reasons ?? {};
|
|
120
|
+
const manifestKeys = new Set(manifest.map((entry) => adoptionKey(entry.subpath, entry.name)));
|
|
121
|
+
const importedKeys = new Set();
|
|
122
|
+
const starSubpaths = new Set();
|
|
123
|
+
for (const row of imports) {
|
|
124
|
+
for (const name of row.names) importedKeys.add(adoptionKey(row.subpath, name));
|
|
125
|
+
if (row.names.length === 0) starSubpaths.add(row.subpath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const entry of manifest) {
|
|
129
|
+
const key = adoptionKey(entry.subpath, entry.name);
|
|
130
|
+
if (!importedKeys.has(key) && !starSubpaths.has(entry.subpath) && !(key in reasons)) {
|
|
131
|
+
findings.push({
|
|
132
|
+
rule: 'unadopted',
|
|
133
|
+
message: `${key} is neither imported nor reasoned (${entry.kind}, ${entry.declaringModule})`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const key of Object.keys(reasons)) {
|
|
139
|
+
if (key.startsWith('local :: ')) {
|
|
140
|
+
const name = key.slice('local :: '.length);
|
|
141
|
+
if (!locals.some((local) => local.name === name)) {
|
|
142
|
+
findings.push({ rule: 'stale-reason', message: `${key} documents no local definition` });
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!manifestKeys.has(key)) {
|
|
147
|
+
findings.push({ rule: 'stale-reason', message: `${key} names no manifest export` });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const exportedNames = new Set(manifest.map((entry) => entry.name));
|
|
152
|
+
for (const local of locals) {
|
|
153
|
+
if (!exportedNames.has(local.name)) continue;
|
|
154
|
+
const shadowed = manifest
|
|
155
|
+
.filter((entry) => entry.name === local.name)
|
|
156
|
+
.every((entry) => !importedKeys.has(adoptionKey(entry.subpath, entry.name)));
|
|
157
|
+
if (shadowed && !(`local :: ${local.name}` in reasons)) {
|
|
158
|
+
findings.push({
|
|
159
|
+
rule: 'collision',
|
|
160
|
+
message: `${local.name} shadows a never-imported framework export with no documented reason`,
|
|
161
|
+
file: local.file,
|
|
162
|
+
line: local.line,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const denied of config.denySubpaths ?? []) {
|
|
168
|
+
for (const row of imports) {
|
|
169
|
+
if (subpathMatches(row.subpath, denied)) {
|
|
170
|
+
findings.push({
|
|
171
|
+
rule: 'denied-subpath',
|
|
172
|
+
message: `${row.subpath} is denied by consumer config`,
|
|
173
|
+
file: row.file,
|
|
174
|
+
line: row.line,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const dir of config.typeOnlyDirs ?? []) {
|
|
181
|
+
for (const row of imports) {
|
|
182
|
+
if (!row.typeOnly && isUnder(row.file, dir)) {
|
|
183
|
+
findings.push({
|
|
184
|
+
rule: 'type-only',
|
|
185
|
+
message: `${row.file} may only import types, but imports ${row.subpath} at runtime`,
|
|
186
|
+
file: row.file,
|
|
187
|
+
line: row.line,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const budget of config.budgets ?? []) {
|
|
194
|
+
const offenders = [];
|
|
195
|
+
for (const row of imports) {
|
|
196
|
+
if (!subpathMatches(row.subpath, budget.match) || row.typeOnly) continue;
|
|
197
|
+
if (!offenders.some((offender) => offender.file === row.file)) offenders.push(row);
|
|
198
|
+
}
|
|
199
|
+
if (offenders.length > budget.limit) {
|
|
200
|
+
const firstPast = offenders[budget.limit];
|
|
201
|
+
findings.push({
|
|
202
|
+
rule: 'budget',
|
|
203
|
+
message: `${offenders.length} files import past ${budget.match} (limit ${budget.limit}: ${budget.reason})`,
|
|
204
|
+
file: firstPast.file,
|
|
205
|
+
line: firstPast.line,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
findings.sort((a, b) => a.rule.localeCompare(b.rule) || a.message.localeCompare(b.message));
|
|
211
|
+
return findings;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** `file:line: [rule] message`, or `[rule] message` for manifest-level findings */
|
|
215
|
+
export function formatFindings(findings) {
|
|
216
|
+
return findings.map((finding) =>
|
|
217
|
+
finding.file === undefined
|
|
218
|
+
? `[${finding.rule}] ${finding.message}`
|
|
219
|
+
: `${finding.file}:${finding.line ?? 0}: [${finding.rule}] ${finding.message}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function main() {
|
|
224
|
+
const args = process.argv.slice(2);
|
|
225
|
+
const valueOf = (flag) => {
|
|
226
|
+
const index = args.indexOf(flag);
|
|
227
|
+
return index === -1 ? null : (args[index + 1] ?? null);
|
|
228
|
+
};
|
|
229
|
+
const manifestFile = valueOf('--manifest');
|
|
230
|
+
const importsFile = valueOf('--imports');
|
|
231
|
+
const localsFile = valueOf('--locals');
|
|
232
|
+
const configFile = valueOf('--config');
|
|
233
|
+
|
|
234
|
+
if (!manifestFile || !importsFile || !localsFile || !configFile) {
|
|
235
|
+
console.error(
|
|
236
|
+
'usage: mwg-audit-adoptions --manifest api-surface.json --imports imports.json --locals locals.json --config audit.json',
|
|
237
|
+
);
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const manifest = JSON.parse(readFileSync(manifestFile, 'utf8'));
|
|
243
|
+
const imports = JSON.parse(readFileSync(importsFile, 'utf8'));
|
|
244
|
+
const locals = JSON.parse(readFileSync(localsFile, 'utf8'));
|
|
245
|
+
const config = JSON.parse(readFileSync(configFile, 'utf8'));
|
|
246
|
+
const lines = formatFindings(auditAdoptions(manifest, imports, locals, config));
|
|
247
|
+
for (const line of lines) console.log(line);
|
|
248
|
+
console.log(`${lines.length} adoption finding(s)`);
|
|
249
|
+
if (lines.length > 0) process.exitCode = 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) main();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface ServedDir {
|
|
2
|
+
url: string;
|
|
3
|
+
port: number;
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CdpMouseEvent {
|
|
8
|
+
type: 'mouseMoved' | 'mousePressed' | 'mouseReleased';
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
button: 'none' | 'left';
|
|
12
|
+
buttonValue?: number;
|
|
13
|
+
clickCount?: number;
|
|
14
|
+
buttons: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BidiPointerAction {
|
|
18
|
+
type: 'pointer';
|
|
19
|
+
id: string;
|
|
20
|
+
parameters: { pointerType: 'mouse' };
|
|
21
|
+
actions: (
|
|
22
|
+
| { type: 'pointerMove'; x: number; y: number }
|
|
23
|
+
| { type: 'pointerDown'; button: number }
|
|
24
|
+
| { type: 'pointerUp'; button: number }
|
|
25
|
+
)[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BrowserDriver {
|
|
29
|
+
kind: 'chrome-cdp' | 'firefox-bidi';
|
|
30
|
+
goto(url: string): Promise<void>;
|
|
31
|
+
waitReady(url: string, timeoutMs?: number): Promise<void>;
|
|
32
|
+
evaluate<T>(expression: string): Promise<T>;
|
|
33
|
+
script<Args extends unknown[], T>(fn: (...args: Args) => T | Promise<T>, ...args: Args): Promise<T>;
|
|
34
|
+
tap(xFraction: number, yFraction: number): Promise<void>;
|
|
35
|
+
screenshot(): Promise<Buffer>;
|
|
36
|
+
consoleErrors(): string[];
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DriveOptions {
|
|
41
|
+
executable?: string;
|
|
42
|
+
chromeArgs?: string[];
|
|
43
|
+
firefoxArgs?: string[];
|
|
44
|
+
profileDir?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** the readiness predicate browser-smoke's waitForGame waits on, as page JS */
|
|
48
|
+
export function readinessExpression(): string;
|
|
49
|
+
|
|
50
|
+
/** an OS-chosen free port, so Windows' excluded-port ranges cannot bite */
|
|
51
|
+
export function pickFreePort(): Promise<number>;
|
|
52
|
+
|
|
53
|
+
/** serves root over HTTP on an OS-chosen free port */
|
|
54
|
+
export function serveDir(root: string): Promise<ServedDir>;
|
|
55
|
+
|
|
56
|
+
/** the ws:// endpoint Chrome prints as DevTools listening on ..., or null */
|
|
57
|
+
export function parseDevToolsEndpoint(stderr: string): string | null;
|
|
58
|
+
|
|
59
|
+
/** the ws:// endpoint Firefox prints as WebDriver BiDi listening on ..., or null */
|
|
60
|
+
export function parseBidiEndpoint(stderr: string): string | null;
|
|
61
|
+
|
|
62
|
+
/** the CDP Input.dispatchMouseEvent sequence a tap is, at CSS pixels */
|
|
63
|
+
export function buildTapSequence(x: number, y: number): CdpMouseEvent[];
|
|
64
|
+
|
|
65
|
+
/** the BiDi input.performActions pointer half of a tap, at CSS pixels */
|
|
66
|
+
export function buildPointerActions(x: number, y: number): BidiPointerAction[];
|
|
67
|
+
|
|
68
|
+
/** unwraps a WebDriver BiDi remote value into plain JSON */
|
|
69
|
+
export function unwrapRemoteValue(node: unknown): unknown;
|
|
70
|
+
|
|
71
|
+
/** locates a browser executable without a driver install, or null when neither yields anything */
|
|
72
|
+
export function findBrowser(kind: 'chrome' | 'firefox'): string | null;
|
|
73
|
+
|
|
74
|
+
/** drives headless Chrome over CDP */
|
|
75
|
+
export function driveChrome(options?: DriveOptions): Promise<BrowserDriver>;
|
|
76
|
+
|
|
77
|
+
/** drives headless Firefox over WebDriver BiDi, with the same surface as driveChrome */
|
|
78
|
+
export function driveFirefox(options?: DriveOptions): Promise<BrowserDriver>;
|