feedback5: merge R6 (tooling) — correctness-only ESLint, a first checkJs slice + core typedefs, tools/check-imports.mjs, tools/package.mjs (+ --lite), CI tooling job

This commit is contained in:
sganggs committed 2026-10-05 16:34:01 +08:00
commit 3288afc787
14 files changed
+2174 -7

No files matched your search

+26
View File
@@ -66,3 +66,29 @@ jobs:
curl -fsS -o /dev/null http://127.0.0.1:3000/vendor/pixi.min.js
node tools/doctor.mjs || true
kill $pid || true
# Separate from the test matrix so lint / types / import boundaries run once, not four times.
# Lint warnings do not fail the job (npm run lint does not pass --max-warnings=0).
tooling:
name: tooling (lint, imports, typecheck)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- name: Install (npm ci → postinstall copies public/vendor)
run: npm ci --no-audit --no-fund
- name: Lint (warnings allowed)
run: npm run lint
- name: Import boundaries
run: npm run check:imports
- name: Typecheck
run: npm run typecheck
+93
View File
@@ -0,0 +1,93 @@
// Correctness lint only (npm run lint). No formatter, no stylistic rules, no repo-wide reformat.
//
// Node: server, shared, tools, scripts, types.
// Browser: public/js (also loaded under Node by unit tests, so `process` is a known global).
// Tests: Node globals plus browser globals (page.evaluate callbacks).
// Warnings (unused vars, and a few rules that fire on intentional patterns) do not fail the run.
// `npm run lint` does not pass --max-warnings=0.
import js from '@eslint/js';
import globals from 'globals';
const unused = ['warn', {
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true,
}];
// eslint:recommended, with the style-adjacent rules turned down. Empty `catch { }` is how this
// code skips a missing file; `no-useless-escape` / `no-extra-boolean-cast` / `no-regex-spaces`
// only complain about how a correct expression is written.
const rules = {
...js.configs.recommended.rules,
'no-unused-vars': unused,
'no-empty': ['warn', { allowEmptyCatch: true }],
'no-useless-escape': 'off',
'no-extra-boolean-cast': 'off',
'no-regex-spaces': 'off',
// `while (true)` is a normal loop. A constant condition anywhere else still warns.
'no-constant-condition': ['warn', { checkLoops: false }],
// Calling hasOwnProperty on a foreign object is a real footgun, but a lot of call sites are
// plain data. Warn; do not fail the build on them.
'no-prototype-builtins': 'warn',
// Redundant initializers (`let x = 0; x = …`) are common here and are not behavior bugs.
'no-useless-assignment': 'warn',
// Name sanitizers intentionally match control characters.
'no-control-regex': 'off',
// ESLint 10 wants `error.cause`. The two hits already put the original message in the text.
'preserve-caught-error': 'warn',
};
const nodeFiles = [
'server/**/*.js',
'shared/**/*.js',
'tools/**/*.js',
'tools/**/*.mjs',
'scripts/**/*.js',
'scripts/**/*.mjs',
'types/**/*.js',
];
export default [
{
ignores: [
'node_modules/**',
'public/vendor/**',
'public/assets/**',
'public/fonts/**',
'public/dev/**',
'.cache/**',
'coverage/**',
],
},
{
files: nodeFiles,
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: { ...globals.node },
},
rules,
},
{
files: ['public/js/**/*.js'],
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: { ...globals.browser, process: 'readonly' },
},
rules,
},
{
// Node test runner, plus browser tests whose page.evaluate callbacks use DOM globals.
// PIXI is the page global those render tests read inside evaluate().
files: ['test/**/*.js'],
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: { ...globals.node, ...globals.browser, PIXI: 'readonly' },
},
rules,
},
];
+29
View File
@@ -0,0 +1,29 @@
{
// First typecheck slice (npm run typecheck). strict stays off until a folder is clean.
// tsc does not load this file unless invoked with -p jsconfig.json.
// Widen one path at a time: see types/README.md.
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"checkJs": true,
"allowJs": true,
"noEmit": true,
"strict": false,
"skipLibCheck": true,
"types": [],
"lib": ["ES2022"],
"maxNodeModuleJsDepth": 0
},
"include": [
"shared/**/*.js",
"server/sim/rng.js",
"server/sim/dir.js",
"server/sim/constants.js",
"types/**/*.js"
],
"exclude": [
"shared/protocol.js",
"shared/bandBonds.js"
]
}
+949 -1
View File
@@ -18,7 +18,11 @@
"ws": "^8.22.0"
},
"devDependencies": {
"puppeteer-core": "^25.12.0"
"@eslint/js": "^10.0.1",
"eslint": "^10.12.0",
"globals": "^17.13.0",
"puppeteer-core": "^25.12.0",
"typescript": "^5.9.2"
},
"engines": {
"node": ">=22"
@@ -1401,6 +1405,950 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@eslint/js": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
"integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"eslint": "^10.0.0"
},
"peerDependenciesMeta": {
"eslint": {
"optional": true
}
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/eslint": {
"version": "10.12.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz",
"integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.14.0",
"debug": "^4.3.2",
"espree": "^11.2.0",
"ignore": "^5.2.0",
"esquery": "^1.7.0",
"esutils": "^2.0.2",
"find-up": "^5.0.0",
"is-glob": "^4.0.0",
"minimatch": "^10.2.5",
"optionator": "^0.9.3",
"cross-spawn": "^7.0.6",
"glob-parent": "^6.0.2",
"imurmurhash": "^0.1.4",
"@eslint/core": "^1.2.1",
"eslint-scope": "^9.1.2",
"@humanfs/node": "^0.16.6",
"@types/estree": "^1.0.6",
"fast-deep-equal": "^3.1.3",
"natural-compare": "^1.4.0",
"file-entry-cache": "11.1.5 || >11.1.6 <12",
"@eslint/plugin-kit": "^0.7.3",
"eslint-visitor-keys": "^5.0.1",
"@eslint/config-array": "^0.23.5",
"@humanwhocodes/retry": "^0.4.2",
"escape-string-regexp": "^4.0.0",
"@eslint/config-helpers": "^0.7.0",
"@eslint-community/regexpp": "^4.12.2",
"@eslint-community/eslint-utils": "^4.8.0",
"@humanwhocodes/module-importer": "^1.0.1",
"json-stable-stringify-without-jsonify": "^1.0.1"
},
"peerDependencies": {
"jiti": "*"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
}
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/globals": {
"version": "17.13.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz",
"integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/ajv": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"uri-js": "^4.2.2",
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^0.4.1",
"fast-json-stable-stringify": "^2.0.0"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/espree": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.16.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^5.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/esquery": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/minimatch": {
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.8"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
"deep-is": "^0.1.3",
"word-wrap": "^1.2.5",
"type-check": "^0.4.0",
"levn": "^0.4.1",
"fast-levenshtein": "^2.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"which": "^2.0.1",
"path-key": "^3.1.0",
"shebang-command": "^2.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
}
},
"node_modules/@eslint/core": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
"integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/eslint-scope": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@types/esrecurse": "^4.3.1",
"@types/estree": "^1.0.8",
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.2",
"@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
"integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^6.1.23"
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz",
"integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"levn": "^0.4.1",
"@eslint/core": "^1.2.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/config-array": {
"version": "0.23.5",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
"integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^3.0.5",
"debug": "^4.3.1",
"minimatch": "^10.2.4"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
"integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.2.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
"integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.22"
}
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/brace-expansion": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "./bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/esrecurse": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
"dev": true,
"license": "MIT"
},
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/flat-cache": {
"version": "6.1.23",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
"integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"flatted": "^3.4.2",
"cacheable": "^2.5.0",
"hookified": "^1.15.0"
}
},
"node_modules/@eslint/object-schema": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
"integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/uri-js/node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/flatted": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
"integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
"dev": true,
"license": "ISC"
},
"node_modules/cacheable": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
"integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"keyv": "^5.6.0",
"qified": "^0.10.1",
"hookified": "^1.15.0",
"@cacheable/utils": "^2.5.0",
"@cacheable/memory": "^2.2.0"
}
},
"node_modules/hookified": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
"integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
"dev": true,
"license": "MIT"
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/keyv": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
"integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
},
"node_modules/qified": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
"integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hookified": "^2.1.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@cacheable/utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
"integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"keyv": "^5.6.0",
"hashery": "^1.5.1"
}
},
"node_modules/@cacheable/memory": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
"integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"keyv": "^5.6.0",
"hookified": "^1.15.1",
"@keyv/bigmap": "^1.3.1",
"@cacheable/utils": "^2.5.0"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@keyv/serialize": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
"dev": true,
"license": "MIT"
},
"node_modules/qified/node_modules/hookified": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
"integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
"dev": true,
"license": "MIT"
},
"node_modules/hashery": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
"integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"hookified": "^1.15.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@keyv/bigmap": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
"integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"hashery": "^1.4.0",
"hookified": "^1.15.0"
},
"peerDependencies": {
"keyv": "^5.6.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+11 -2
View File
@@ -34,7 +34,12 @@
"postinstall": "node tools/vendor.mjs",
"setup": "node tools/setup.mjs",
"doctor": "node tools/doctor.mjs",
"launch": "node scripts/launch.mjs"
"launch": "node scripts/launch.mjs",
"lint": "eslint .",
"typecheck": "tsc --noEmit --checkJs -p jsconfig.json",
"check:imports": "node tools/check-imports.mjs",
"package": "node tools/package.mjs",
"package:lite": "node tools/package.mjs --lite"
},
"engines": {
"node": ">=22"
@@ -48,6 +53,10 @@
"ws": "^8.22.0"
},
"devDependencies": {
"puppeteer-core": "^25.12.0"
"@eslint/js": "^10.0.1",
"eslint": "^10.12.0",
"globals": "^17.13.0",
"puppeteer-core": "^25.12.0",
"typescript": "^5.9.2"
}
}
+1 -1
View File
@@ -354,7 +354,7 @@ export function useData(...names) {
const unsub = data.subscribe((n) => { if (alive && names.includes(n)) force(); });
for (const n of names) data.load(n);
return () => { alive = false; unsub(); };
}, [key]); // eslint-disable-line react-hooks/exhaustive-deps
}, [key]);
return names.every((n) => {
const s = data.status(n);
return s === 'ready' || s === 'missing';
+1 -1
View File
@@ -10,7 +10,7 @@
// plays 战场#01, 险境 draws one of 8, 绝境 / 终极 one of 7 (m01 excluded).
import { useEffect, useRef, useState } from '../../vendor/hooks.module.js';
import { DIFFICULTIES, DIFFICULTY_NAMES, DIFFICULTY_COLORS, ROOM_CODE_LEN, MAX_SEATS, MAX_SPECTATORS, modeIdFor } from '../../../shared/constants.js';
import { DIFFICULTIES, DIFFICULTY_NAMES, DIFFICULTY_COLORS, ROOM_CODE_LEN, MAX_SEATS, MAX_SPECTATORS, modeIdFor, ERR } from '../../../shared/constants.js';
import { html, Button, Icon, MicroLabel, Panel, TextField, PingPill, AvatarFrame, Tooltip, Spinner, DifficultyIcon, doctorNo } from '../ui/components.js';
import { toast, toastError } from '../ui/toasts.js';
import { GuideButton } from '../ui/guide.js';
+110
View File
@@ -0,0 +1,110 @@
// Boundary checker: fixture rules, plus the real tree's current violation list.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { findSpecifiers, scan } from '../tools/check-imports.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
function fixture(files) {
const root = mkdtempSync(join(tmpdir(), 'sp-imp-'));
for (const [rel, body] of Object.entries(files)) {
const abs = join(root, rel);
mkdirSync(dirname(abs), { recursive: true });
writeFileSync(abs, body);
}
return root;
}
test('comments are not imports, and a multiline import is', () => {
const specs = findSpecifiers(`
// import fs from 'node:fs'
/* require('node:path') */
import {
a,
b,
} from './tier1.js';
export function fromThing() { return 1; }
`);
assert.deepEqual(specs.map((s) => s.spec), ['./tier1.js']);
});
test('sim and match rules on a fixture tree', () => {
const root = fixture({
'server/sim/clean.js': `
// import fs from 'node:fs'
import { a } from './other.js';
export function load() { return 1; }
export { a };
`,
'server/sim/bad.js': `
import '../match/Match.js';
import '../lobby.js';
import '../index.js';
import '../net.js';
import '../../public/js/main.js';
import fs from 'node:fs';
import 'fs';
export { z } from '../match/board.js';
const p = await import('node:path');
`,
'server/match/ok.js': `
import '../sim/rng.js';
import 'node:fs';
import '../data.js';
`,
'server/match/bad.js': `
import '../../public/js/main.js';
import 'node:http';
import 'ws';
import '../net.js';
import '../index.js';
`,
});
try {
const keys = scan(root).violations.map((v) => `${v.file} ${v.spec} ${v.code}`).sort();
assert.deepEqual(keys, [
'server/match/bad.js ../../public/js/main.js match-client',
'server/match/bad.js ../index.js match-http',
'server/match/bad.js ../net.js match-http',
'server/match/bad.js node:http match-http',
'server/match/bad.js ws match-http',
'server/sim/bad.js ../../public/js/main.js sim-client',
'server/sim/bad.js ../index.js sim-entry',
'server/sim/bad.js ../lobby.js sim-lobby',
'server/sim/bad.js ../match/Match.js sim-match',
'server/sim/bad.js ../match/board.js sim-match',
'server/sim/bad.js ../net.js sim-http',
'server/sim/bad.js fs sim-node',
'server/sim/bad.js node:fs sim-node',
'server/sim/bad.js node:path sim-node',
]);
assert.equal(scan(root).notes.length, 0);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('this repo: only the Node data loader crosses the sim boundary', () => {
const { violations, notes } = scan(ROOT);
assert.deepEqual(violations.map((v) => `${v.file}:${v.line}: ${v.spec} [${v.code}]`), [
'server/sim/nodeData.js:11: node:fs [sim-node]',
'server/sim/nodeData.js:12: node:path [sim-node]',
'server/sim/nodeData.js:13: node:url [sim-node]',
]);
assert.ok(notes.some((n) => n.file === 'server/sim/nodeData.js' && n.resolved === 'server/data.js'));
assert.ok(notes.every((n) => n.resolved === 'server/data.js'));
});
test('the CLI lists hits and fails only with --strict', () => {
const loose = spawnSync(process.execPath, ['tools/check-imports.mjs'], { cwd: ROOT, encoding: 'utf8' });
assert.equal(loose.status, 0);
assert.match(loose.stdout, /server\/sim\/nodeData\.js:11: node:fs \[sim-node\]/);
const strict = spawnSync(process.execPath, ['tools/check-imports.mjs', '--strict'], { cwd: ROOT, encoding: 'utf8' });
assert.equal(strict.status, 1);
assert.match(strict.stdout, /node:fs/);
});
+155
View File
@@ -0,0 +1,155 @@
// Player-package file selection. Dry-run only: no zip, no npm ci.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
extraPaths,
isExcluded,
packageOutIsUnsafe,
selectTracked,
textHasHomePath,
trackedFiles,
} from '../tools/package.mjs';
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const SAMPLE = [
'server/index.js',
'server/sim/rng.js',
'shared/constants.js',
'public/index.html',
'public/js/main.js',
'public/vendor/preact.mjs',
'public/dev/game-mock.html',
'public/assets/board.png',
'public/fonts/fonts.css',
'data/chess.json',
'data/local-assets.json',
'tools/setup.mjs',
'tools/vendor.mjs',
'tools/fetch-assets.mjs',
'tools/assets/plan.mjs',
'tools/local-extract/extract.py',
'tools/golden.mjs',
'tools/check-imports.mjs',
'scripts/start.sh',
'docs/PLAYING.md',
'docs/DEPLOY.md',
'docs/WINDOWS.md',
'docs/ASSETS.md',
'docs/img/combat.jpg',
'docs/DESIGN.md',
'docs/SIM.md',
'docs/research/03-operators.json',
'docs/research/10-networking-hosting.md',
'test/version.test.js',
'package.json',
'package-lock.json',
'LICENSE',
'NOTICE.md',
'THIRD-PARTY-NOTICES.md',
'README.md',
'CHANGELOG.md',
'Dockerfile',
'.dockerignore',
'AGENTS.md',
'.env',
'.env.local',
'server/.env.production',
'pv/clip.mp4',
'3,9,11回合情况/note.txt',
'handoff/HANDOFF.md',
'node_modules/ws/index.js',
'.cache/x',
'eslint.config.js',
'jsconfig.json',
'types/core.js',
];
test('selectTracked keeps runtime files and drops the rest', () => {
const got = selectTracked(SAMPLE);
assert.deepEqual(got, selectTracked(SAMPLE, { lite: true }));
for (const rel of [
'server/index.js', 'tools/setup.mjs', 'tools/vendor.mjs', 'docs/PLAYING.md',
'docs/DEPLOY.md', 'public/index.html', 'data/chess.json', 'package.json',
'Dockerfile', 'scripts/start.sh', 'tools/assets/plan.mjs', 'tools/local-extract/extract.py',
]) assert.ok(got.includes(rel), rel);
for (const rel of [
'tools/golden.mjs', 'tools/check-imports.mjs', 'test/version.test.js', 'public/dev/game-mock.html',
'public/assets/board.png', 'public/fonts/fonts.css', 'docs/research/10-networking-hosting.md',
'docs/research/03-operators.json', 'docs/DESIGN.md', 'docs/SIM.md', 'AGENTS.md', '.env',
'.env.local', 'server/.env.production', 'pv/clip.mp4', '3,9,11回合情况/note.txt',
'handoff/HANDOFF.md', 'node_modules/ws/index.js', 'data/local-assets.json', 'eslint.config.js',
'types/core.js',
]) assert.ok(!got.includes(rel), rel);
assert.equal(isExcluded('docs/research/10-networking-hosting.md'), true);
});
test('lite extras skip art, fonts and the local manifest', () => {
assert.deepEqual(extraPaths({ lite: true, hasLocalManifest: true }), ['public/vendor']);
assert.deepEqual(extraPaths({ lite: false, hasLocalManifest: true }), [
'public/vendor', 'public/assets', 'public/fonts', 'data/local-assets.json',
]);
assert.deepEqual(extraPaths({ lite: false, hasLocalManifest: false }), [
'public/vendor', 'public/assets', 'public/fonts',
]);
});
test('home-directory text and unsafe output directories', () => {
assert.equal(textHasHomePath('see /Users/example/game'), 'mac-users');
assert.equal(textHasHomePath('C:\\Users\\example\\game'), 'win-users');
assert.equal(textHasHomePath('prefix /home/example/game'), 'posix-home');
assert.equal(textHasHomePath('https://github.com/example/game'), null);
assert.equal(packageOutIsUnsafe(ROOT, ROOT), true);
assert.equal(packageOutIsUnsafe(path.dirname(ROOT), ROOT), true);
assert.equal(packageOutIsUnsafe(path.join(ROOT, 'dist-pack'), ROOT), false);
});
test('real git ls-files: runtime in, dev and private out', () => {
const got = new Set(selectTracked(trackedFiles(ROOT)));
assert.ok(got.has('server/index.js'));
assert.ok(got.has('tools/setup.mjs'));
assert.ok(got.has('docs/PLAYING.md'));
assert.ok(got.has('package-lock.json'));
assert.equal(got.has('tools/golden.mjs'), false);
assert.equal(got.has('tools/package.mjs'), false);
assert.equal(got.has('test/golden.test.js'), false);
assert.equal(got.has('docs/DESIGN.md'), false);
assert.equal(got.has('eslint.config.js'), false);
for (const rel of got) {
assert.equal(rel.startsWith('docs/research/'), false, rel);
assert.equal(rel.startsWith('test/'), false, rel);
assert.equal(rel.startsWith('public/dev/'), false, rel);
assert.equal(rel.startsWith('handoff/'), false, rel);
}
});
test('dry-run lists the lite set and prints both size estimates', () => {
const lite = spawnSync(process.execPath, ['tools/package.mjs', '--dry-run', '--lite'], {
cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
});
assert.equal(lite.status, 0, lite.stderr);
assert.match(lite.stdout, /^mode: lite/m);
assert.match(lite.stdout, /^dry-run: yes/m);
assert.match(lite.stdout, /^file server\/index\.js$/m);
assert.match(lite.stdout, /^file tools\/setup\.mjs$/m);
assert.match(lite.stdout, /^file docs\/PLAYING\.md$/m);
assert.match(lite.stdout, /^extra public\/vendor$/m);
assert.doesNotMatch(lite.stdout, /^file tools\/golden\.mjs$/m);
assert.doesNotMatch(lite.stdout, /^file test\//m);
assert.doesNotMatch(lite.stdout, /^extra public\/assets$/m);
assert.match(lite.stdout, /^uncompressed lite: \d+/m);
assert.match(lite.stdout, /^uncompressed full estimate: \d+/m);
assert.match(lite.stdout, /puppeteer-core/);
const full = spawnSync(process.execPath, ['tools/package.mjs', '--dry-run'], {
cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
});
assert.equal(full.status, 0, full.stderr);
assert.match(full.stdout, /^mode: full/m);
assert.match(full.stdout, /^extra public\/assets$/m);
assert.match(full.stdout, /^extra public\/fonts$/m);
const assets = Number((full.stdout.match(/^assets bytes: (\d+)/m) || [])[1]);
assert.ok(assets > 300 * 1048576, `assets bytes ${assets}`);
});
+2 -2
View File
@@ -29,7 +29,7 @@ function splitEntry(line) {
* @returns {{ lines: string[], pages: AtlasPage[] }}
*/
export function parseAtlas(text) {
const lines = String(text).replace(/^/, '').replace(/\r\n?/g, '\n').split('\n');
const lines = String(text).replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n').split('\n');
/** @type {AtlasPage[]} */
const pages = [];
let page = null;
@@ -108,7 +108,7 @@ export function normalizeAtlas(text, { pageSize, pma = false, renamePage = null
}
}
const result = out.join('\n');
const original = String(text).replace(/^/, '').replace(/\r\n?/g, '\n');
const original = String(text).replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
return { text: result, changed: result !== original, pages: pages.map((p) => (replace.get(p.line) ?? p.name)), missingSize, fixedSize };
}
+310
View File
@@ -0,0 +1,310 @@
// Import boundaries for the sim and the match layer.
//
// server/sim is served to the browser, so it must not import the match, the lobby, the HTTP entry,
// the net layer, public/, or any Node builtin. server/match must not import public/ or that net layer.
// server/data.js is allowed (the Node loader reaches it on purpose).
//
// Default exit code is 0 even when violations are listed. --strict exits 1 when any are listed.
// Usage: node tools/check-imports.mjs [--strict] [--root <dir>]
import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
import { builtinModules } from 'node:module';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const HTTP_SPECS = new Set([
'http', 'https', 'http2', 'net', 'tls', 'dgram', 'ws',
'node:http', 'node:https', 'node:http2', 'node:net', 'node:tls', 'node:dgram',
]);
const builtins = new Set(builtinModules);
/** Drop comments. Strings, including the import specifiers, stay. Newlines stay so line numbers match. */
export function maskComments(src) {
let out = '';
let i = 0;
while (i < src.length) {
const c = src[i];
const n = src[i + 1];
if (c === '/' && n === '/') {
i += 2;
while (i < src.length && src[i] !== '\n') i++;
continue;
}
if (c === '/' && n === '*') {
i += 2;
while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) {
out += src[i] === '\n' ? '\n' : ' ';
i++;
}
i += 2;
continue;
}
if (c === "'" || c === '"') {
const end = endOfString(src, i);
out += src.slice(i, end);
i = end;
continue;
}
if (c === '`') {
const end = endOfTemplate(src, i);
out += src.slice(i, end);
i = end;
continue;
}
out += c;
i++;
}
return out;
}
function endOfString(src, i) {
const q = src[i];
let j = i + 1;
while (j < src.length && src[j] !== q) {
if (src[j] === '\\') { j += 2; continue; }
if (src[j] === '\n') break;
j++;
}
return Math.min(src.length, j + 1);
}
function endOfTemplate(src, i) {
let j = i + 1;
while (j < src.length && src[j] !== '`') {
if (src[j] === '\\') { j += 2; continue; }
if (src[j] === '$' && src[j + 1] === '{') {
j += 2;
let depth = 1;
while (j < src.length && depth > 0) {
if (src[j] === "'" || src[j] === '"') { j = endOfString(src, j); continue; }
if (src[j] === '`') { j = endOfTemplate(src, j); continue; }
if (src[j] === '{') depth++;
else if (src[j] === '}') depth--;
if (depth > 0) j++;
}
j++;
continue;
}
j++;
}
return Math.min(src.length, j + 1);
}
function lineOf(src, index) {
let line = 1;
for (let i = 0; i < index && i < src.length; i++) if (src[i] === '\n') line++;
return line;
}
function skipWs(src, i) {
while (i < src.length && /\s/.test(src[i])) i++;
return i;
}
function readQuoted(src, i) {
if (src[i] !== "'" && src[i] !== '"') return null;
const q = src[i];
let j = i + 1;
let spec = '';
while (j < src.length && src[j] !== q) {
if (src[j] === '\\') { spec += src[j + 1] ?? ''; j += 2; continue; }
if (src[j] === '\n') return null;
spec += src[j];
j++;
}
if (src[j] !== q) return null;
return { spec, quoteAt: i, end: j + 1 };
}
/** The `from 'spec'` of an import/export clause, or null when the statement is not one. */
function readFrom(src, i) {
let depth = 0;
const start = i;
while (i < src.length && i - start < 8000) {
const c = src[i];
if (c === "'" || c === '"') {
const str = readQuoted(src, i);
if (!str) return null;
i = str.end;
continue;
}
if (c === '{') depth++;
else if (c === '}') depth = Math.max(0, depth - 1);
else if (c === ';' && depth === 0) return null;
else if (depth === 0 && src.startsWith('from', i) && !/\w/.test(src[i - 1] ?? '') && !/\w/.test(src[i + 4] ?? '')) {
const str = readQuoted(src, skipWs(src, i + 4));
return str;
}
i++;
}
return null;
}
/**
* @param {string} src raw source
* @returns {{ spec: string, line: number }[]}
*/
export function findSpecifiers(src) {
const masked = maskComments(src);
/** @type {{ spec: string, line: number }[]} */
const found = [];
const re = /\b(import|export|require)\b/g;
let m;
while ((m = re.exec(masked))) {
const kw = m[1];
let i = skipWs(masked, m.index + kw.length);
if (kw === 'require') {
if (masked[i] !== '(') continue;
const str = readQuoted(masked, skipWs(masked, i + 1));
if (str) found.push({ spec: str.spec, line: lineOf(masked, str.quoteAt) });
continue;
}
if (kw === 'import' && masked[i] === '.') continue; // import.meta
if (kw === 'import' && masked[i] === '(') {
const str = readQuoted(masked, skipWs(masked, i + 1));
if (str) found.push({ spec: str.spec, line: lineOf(masked, str.quoteAt) });
continue;
}
if (kw === 'import' && (masked[i] === "'" || masked[i] === '"')) {
const str = readQuoted(masked, i);
if (str) found.push({ spec: str.spec, line: lineOf(masked, str.quoteAt) });
continue;
}
if (kw === 'export') {
if (masked.startsWith('type', i) && /\s/.test(masked[i + 4] ?? '')) i = skipWs(masked, i + 4);
const c = masked[i];
if (c !== '{' && c !== '*') continue;
}
const from = readFrom(masked, i);
if (from) found.push({ spec: from.spec, line: lineOf(masked, from.quoteAt) });
}
return found;
}
function isBuiltin(spec) {
if (spec.startsWith('node:')) return true;
return builtins.has(spec);
}
function isHttp(spec, resolved) {
if (HTTP_SPECS.has(spec)) return true;
if (spec === 'ws' || spec.startsWith('ws/')) return true;
if (resolved === 'server/net.js' || resolved === 'server/index.js') return true;
return false;
}
/**
* @param {string} spec
* @param {string} fromFile posix path relative to the root
* @param {'sim'|'match'} area
* @returns {{ code: string, resolved: string } | { note: true, resolved: string } | null}
*/
export function classify(spec, fromFile, area) {
const fromDir = path.posix.dirname(fromFile);
let resolved = spec;
if (spec.startsWith('.')) {
resolved = path.posix.normalize(path.posix.join(fromDir, spec));
}
const escaped = resolved.startsWith('..') || path.posix.isAbsolute(spec);
if (area === 'sim') {
if (resolved === 'server/match' || resolved.startsWith('server/match/')) return { code: 'sim-match', resolved };
if (resolved === 'server/lobby.js' || resolved.startsWith('server/lobby/')) return { code: 'sim-lobby', resolved };
if (resolved === 'server/index.js') return { code: 'sim-entry', resolved };
if (isHttp(spec, resolved)) return { code: 'sim-http', resolved };
if (resolved === 'public' || resolved.startsWith('public/')) return { code: 'sim-client', resolved };
if (escaped || isBuiltin(spec)) return { code: 'sim-node', resolved };
if (resolved === 'server/data.js') return { note: true, resolved };
return null;
}
if (resolved === 'public' || resolved.startsWith('public/')) return { code: 'match-client', resolved };
if (isHttp(spec, resolved)) return { code: 'match-http', resolved };
return null;
}
function walkJs(dir, rel, out) {
let entries;
try { entries = readdirSync(dir); } catch { return; }
for (const name of entries) {
if (name === 'node_modules') continue;
const abs = path.join(dir, name);
const child = rel ? `${rel}/${name}` : name;
let st;
try { st = statSync(abs); } catch { continue; }
if (st.isDirectory()) walkJs(abs, child, out);
else if (/\.(?:js|mjs|cjs)$/.test(name)) out.push(child.split(path.sep).join('/'));
}
}
/**
* @param {string} root absolute repository root
* @returns {{ violations: { file: string, line: number, spec: string, code: string, resolved: string }[], notes: { file: string, line: number, spec: string, resolved: string }[] }}
*/
export function scan(root) {
const files = [];
walkJs(path.join(root, 'server', 'sim'), 'server/sim', files);
walkJs(path.join(root, 'server', 'match'), 'server/match', files);
/** @type {{ file: string, line: number, spec: string, code: string, resolved: string }[]} */
const violations = [];
/** @type {{ file: string, line: number, spec: string, resolved: string }[]} */
const notes = [];
for (const file of files) {
const area = file.startsWith('server/sim/') ? 'sim' : 'match';
let src;
try { src = readFileSync(path.join(root, file), 'utf8'); } catch { continue; }
for (const hit of findSpecifiers(src)) {
const result = classify(hit.spec, file, area);
if (!result) continue;
if (result.note) notes.push({ file, line: hit.line, spec: hit.spec, resolved: result.resolved });
else violations.push({ file, line: hit.line, spec: hit.spec, code: result.code, resolved: result.resolved });
}
}
const byPos = (a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.spec.localeCompare(b.spec);
violations.sort(byPos);
notes.sort(byPos);
return { violations, notes };
}
export function formatReport({ violations, notes }) {
const lines = [];
if (violations.length === 0) lines.push('no import-boundary violations');
else {
lines.push(`${violations.length} import-boundary violation(s):`);
for (const v of violations) lines.push(`${v.file}:${v.line}: ${v.spec} [${v.code}]`);
}
if (notes.length) {
lines.push(`${notes.length} allowed note(s):`);
for (const n of notes) lines.push(`note ${n.file}:${n.line}: ${n.spec} -> ${n.resolved}`);
}
return lines.join('\n') + '\n';
}
function main(argv) {
let strict = false;
let root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--strict') strict = true;
else if (a === '--root') {
const dir = argv[++i];
if (!dir || dir.startsWith('--')) {
console.error('check-imports: --root needs a directory');
return 1;
}
root = path.resolve(dir);
} else if (a === '--help' || a === '-h') {
console.log('usage: node tools/check-imports.mjs [--strict] [--root <dir>]');
return 0;
} else {
console.error(`check-imports: unknown argument ${a}`);
return 1;
}
}
const report = scan(root);
process.stdout.write(formatReport(report));
if (strict && report.violations.length) return 1;
return 0;
}
const invoked = process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
if (invoked) process.exit(main(process.argv.slice(2)));
+343
View File
@@ -0,0 +1,343 @@
// Player package: git-tracked runtime files, production dependencies, and (unless --lite) local art.
//
// scripts/make-windows-bundle.mjs is the Windows portable-Node pack. This tool is the release zip:
// a runtime allowlist, not the whole tree. The Docker image still copies docs/research as a sim
// fallback; this zip does not. Players on --lite run `npm run setup` to download art and fonts.
//
// node tools/package.mjs [--dry-run] [--lite] [--out <dir>] [--no-install]
//
// --dry-run prints the file list and a size summary and writes nothing.
// A real full zip is large because public/assets is included. Do not point --out at the repo.
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const ROOT_FILES = new Set([
'package.json', 'package-lock.json', 'LICENSE', 'NOTICE.md', 'THIRD-PARTY-NOTICES.md',
'README.md', 'CHANGELOG.md', 'Dockerfile', '.dockerignore',
]);
const PLAYER_DOCS = new Set([
'docs/PLAYING.md', 'docs/DEPLOY.md', 'docs/WINDOWS.md', 'docs/ASSETS.md',
]);
const RUNTIME_TOOLS = new Set([
'tools/setup.mjs', 'tools/vendor.mjs', 'tools/fetch-assets.mjs',
'tools/doctor.mjs', 'tools/crop-board-atlas.mjs',
]);
const DROP_PREFIXES = [
'test/', 'public/dev/', 'public/assets/', 'public/fonts/', 'docs/research/',
'handoff/', 'node_modules/', '.cache/', 'pv/', '3,9,11回合情况/',
];
const TEXT_EXT = new Set([
'.js', '.mjs', '.cjs', '.json', '.md', '.html', '.css', '.txt', '.yml', '.yaml',
'.ps1', '.sh', '.cmd', '.bat', '.py', '.svg',
]);
const HOME_PATTERNS = [
{ name: 'mac-users', re: /\/Users\// },
{ name: 'win-users', re: /[A-Za-z]:\\Users\\/ },
{ name: 'posix-home', re: /\/home\// },
];
export function posixRel(rel) {
return String(rel).split('\\').join('/');
}
/** True when the path must never ship, even if an allow rule would match. */
export function isExcluded(rel) {
const p = posixRel(rel);
const base = p.slice(p.lastIndexOf('/') + 1);
if (base === '.env' || base.startsWith('.env.')) return true;
if (p === 'AGENTS.md' || p === 'docs/research/10-networking-hosting.md') return true;
return DROP_PREFIXES.some((pre) => p === pre.slice(0, -1) || p.startsWith(pre));
}
/** Git-tracked paths that belong in the player package. `lite` does not change this set. */
export function selectTracked(paths, _opts = {}) {
const out = [];
for (const raw of paths) {
const rel = posixRel(raw);
if (!rel || isExcluded(rel)) continue;
if (allowed(rel)) out.push(rel);
}
out.sort();
return out;
}
function allowed(rel) {
if (rel.startsWith('server/') || rel.startsWith('shared/')) return true;
if (rel.startsWith('public/')) return true;
if (rel.startsWith('data/')) return rel !== 'data/local-assets.json';
if (rel.startsWith('scripts/')) return true;
if (RUNTIME_TOOLS.has(rel)) return true;
if (rel.startsWith('tools/assets/') || rel.startsWith('tools/local-extract/')) return true;
if (PLAYER_DOCS.has(rel) || rel.startsWith('docs/img/')) return true;
return ROOT_FILES.has(rel);
}
/** Directories and files copied in addition to the git allowlist. */
export function extraPaths({ lite, hasLocalManifest }) {
const paths = ['public/vendor'];
if (!lite) {
paths.push('public/assets', 'public/fonts');
if (hasLocalManifest) paths.push('data/local-assets.json');
}
return paths;
}
export function textHasHomePath(text) {
for (const { name, re } of HOME_PATTERNS) {
if (re.test(text)) return name;
}
return null;
}
/** True when `out` is the repo or a parent of it (a wipe there would take the repo with it). */
export function packageOutIsUnsafe(out, root = ROOT) {
const r = path.resolve(root);
const o = path.resolve(out);
if (o === r) return true;
const rel = path.relative(o, r);
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
}
export function trackedFiles(root = ROOT) {
const r = spawnSync('git', ['-C', root, 'ls-files', '-z'], { maxBuffer: 32 * 1024 * 1024 });
if (r.error || r.status !== 0) {
throw new Error('git ls-files failed; package only tracked runtime files, so run it inside the repo');
}
return r.stdout.toString('utf8').split('\0').filter(Boolean);
}
function fileBytes(abs) {
try {
const st = fs.statSync(abs);
return st.isFile() ? st.size : 0;
} catch {
return 0;
}
}
/** Bytes under abs, following symlinks. Skips nested node_modules and .cache. */
export function treeBytes(abs) {
let total = 0;
const seen = new Set();
function walk(dir) {
let real;
try { real = fs.realpathSync(dir); } catch { return; }
if (seen.has(real)) return;
seen.add(real);
let entries;
try { entries = fs.readdirSync(real, { withFileTypes: true }); } catch { return; }
for (const ent of entries) {
if (ent.name === 'node_modules' || ent.name === '.cache') continue;
const p = path.join(real, ent.name);
let st;
try { st = fs.statSync(p); } catch { continue; }
if (st.isDirectory()) walk(p);
else if (st.isFile()) total += st.size;
}
}
try {
const st = fs.statSync(abs);
if (st.isFile()) return st.size;
if (st.isDirectory()) walk(abs);
} catch {
return 0;
}
return total;
}
/** Uncompressed size of production lockfile entries present under root/node_modules. Dev entries are skipped. */
export function prodModulesBytes(root, lock) {
let total = 0;
const packages = lock?.packages || {};
for (const [key, entry] of Object.entries(packages)) {
if (!key || entry.dev) continue;
total += treeBytes(path.join(root, key));
}
return total;
}
function exists(root, rel) {
try { return fs.statSync(path.join(root, rel)).isFile() || fs.statSync(path.join(root, rel)).isDirectory(); }
catch { return false; }
}
/**
* Selection plus uncompressed byte counts. Does not copy or install.
* @param {string} root
* @param {{ lite?: boolean, paths?: string[] }} [opts]
*/
export function measure(root, opts = {}) {
const lite = !!opts.lite;
const paths = opts.paths || trackedFiles(root);
const tracked = selectTracked(paths, { lite });
let trackedBytes = 0;
for (const rel of tracked) trackedBytes += fileBytes(path.join(root, rel));
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
const lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8'));
const hasLocal = exists(root, 'data/local-assets.json');
const vendorBytes = treeBytes(path.join(root, 'public', 'vendor'));
const fontsBytes = treeBytes(path.join(root, 'public', 'fonts'));
const assetsBytes = treeBytes(path.join(root, 'public', 'assets'));
const manifestBytes = hasLocal ? fileBytes(path.join(root, 'data', 'local-assets.json')) : 0;
const prodBytes = prodModulesBytes(root, lock);
const liteBytes = trackedBytes + vendorBytes + prodBytes;
const fullBytes = liteBytes + fontsBytes + assetsBytes + manifestBytes;
return {
version: pkg.version,
lite,
tracked,
extras: extraPaths({ lite, hasLocalManifest: hasLocal }),
trackedBytes,
vendorBytes,
fontsBytes,
assetsBytes,
manifestBytes,
prodBytes,
liteBytes,
fullBytes,
prodDeps: Object.keys(pkg.dependencies || {}).sort(),
devDeps: Object.keys(pkg.devDependencies || {}).sort(),
assetsPresent: exists(root, 'public/assets'),
fontsPresent: exists(root, 'public/fonts'),
vendorPresent: exists(root, 'public/vendor'),
};
}
export function formatSummary(m) {
const mb = (n) => `${(n / 1048576).toFixed(1)} MB`;
const lines = [
`mode: ${m.lite ? 'lite' : 'full'}`,
`version: ${m.version}`,
`tracked files: ${m.tracked.length}`,
`tracked bytes: ${m.trackedBytes}`,
`vendor bytes: ${m.vendorBytes}${m.vendorPresent ? '' : ' (absent)'}`,
`fonts bytes: ${m.fontsBytes}${m.lite ? ' (omitted in lite)' : ''}`,
`assets bytes: ${m.assetsBytes}${m.lite ? ' (omitted in lite)' : ''}`,
`local manifest bytes: ${m.manifestBytes}${m.lite ? ' (omitted in lite)' : ''}`,
`production node_modules estimate: ${m.prodBytes}`,
`uncompressed lite: ${m.liteBytes} (${mb(m.liteBytes)})`,
`uncompressed full estimate: ${m.fullBytes} (${mb(m.fullBytes)})`,
`production dependencies: ${m.prodDeps.join(' ')}`,
`omitted devDependencies: ${m.devDeps.join(' ')}`,
];
return lines.join('\n') + '\n';
}
function copyRel(root, stage, rel) {
const src = path.join(root, rel);
const dest = path.join(stage, rel);
if (!fs.existsSync(src)) throw new Error(`missing ${rel}`);
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.cpSync(src, dest, { dereference: true, recursive: true });
}
function scanStage(stage) {
const hits = [];
function walk(dir, rel) {
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
for (const ent of entries) {
const child = rel ? `${rel}/${ent.name}` : ent.name;
if (child === 'node_modules' || child.startsWith('node_modules/') || child.startsWith('public/assets') || child.startsWith('public/fonts')) continue;
const abs = path.join(dir, ent.name);
if (ent.isDirectory()) { walk(abs, child); continue; }
if (!ent.isFile()) continue;
const ext = path.extname(ent.name).toLowerCase();
const named = ent.name === 'Dockerfile' || ent.name === '.dockerignore';
if (!named && !TEXT_EXT.has(ext)) continue;
let text;
try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
const hit = textHasHomePath(text);
if (hit) hits.push(`${child} (${hit})`);
}
}
walk(stage, '');
return hits;
}
function build(root, m, { out, noInstall }) {
if (packageOutIsUnsafe(out, root)) throw new Error(`refusing to write the package into the repo or a parent of it: ${out}`);
const folder = `Stronghold-Protocol-${m.version}${m.lite ? '-lite' : ''}`;
const stage = path.join(out, folder);
if (fs.existsSync(stage)) throw new Error(`package directory already exists: ${stage}`);
if (!m.lite) {
if (!m.assetsPresent) throw new Error('public/assets is missing; a full package needs the local art (or pass --lite)');
if (!m.fontsPresent) throw new Error('public/fonts is missing; a full package needs the local fonts (or pass --lite)');
}
fs.mkdirSync(stage, { recursive: true });
for (const rel of m.tracked) copyRel(root, stage, rel);
for (const rel of m.extras) {
if (!fs.existsSync(path.join(root, rel))) {
if (rel === 'data/local-assets.json') continue;
if (rel === 'public/vendor' && !noInstall) continue; // npm ci postinstall (tools/vendor.mjs) fills it
throw new Error(`missing ${rel}`);
}
copyRel(root, stage, rel);
}
if (!noInstall) {
const r = spawnSync('npm', ['ci', '--omit=dev', '--no-audit', '--no-fund'], { cwd: stage, stdio: 'inherit' });
if (r.error || r.status !== 0) throw new Error('npm ci --omit=dev failed in the package directory');
fs.rmSync(path.join(stage, 'node_modules', '.cache'), { recursive: true, force: true });
}
const hits = scanStage(stage);
if (hits.length) {
throw new Error(`refusing to pack; home-directory paths in:\n${hits.join('\n')}`);
}
const zipPath = path.join(out, `${folder}.zip`);
const zip = spawnSync('zip', ['-r', '-q', zipPath, folder], { cwd: out });
if (zip.error || zip.status !== 0) throw new Error('zip failed');
return { stage, zipPath };
}
function main(argv) {
let lite = false;
let dry = false;
let noInstall = false;
let out = '';
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--lite') lite = true;
else if (a === '--dry-run') dry = true;
else if (a === '--no-install') noInstall = true;
else if (a === '--out') {
out = argv[++i];
if (!out || out.startsWith('--')) {
console.error('package: --out needs a directory');
return 1;
}
} else if (a === '--help' || a === '-h') {
console.log('usage: node tools/package.mjs [--dry-run] [--lite] [--out <dir>] [--no-install]');
return 0;
} else {
console.error(`package: unknown argument ${a}`);
return 1;
}
}
const m = measure(ROOT, { lite });
process.stdout.write(formatSummary(m));
if (dry) {
process.stdout.write('dry-run: yes\n');
for (const rel of m.tracked) process.stdout.write(`file ${rel}\n`);
for (const rel of m.extras) process.stdout.write(`extra ${rel}\n`);
return 0;
}
const dest = out || path.join(os.tmpdir(), `stronghold-protocol-package-${m.version}`);
const built = build(ROOT, m, { out: dest, noInstall });
console.log(`zip: ${built.zipPath}`);
return 0;
}
const invoked = process.argv[1]
&& import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href;
if (invoked) process.exit(main(process.argv.slice(2)));
+25
View File
@@ -0,0 +1,25 @@
# Typecheck
`npm run typecheck` runs `tsc --noEmit --checkJs -p jsconfig.json`.
The first slice is the closed set that already typechecks:
- `shared/` except `shared/protocol.js` and `shared/bandBonds.js` (their JSDoc does not match the code yet; see below)
- `server/sim/rng.js`, `server/sim/dir.js`, `server/sim/constants.js` (the last imports `GEO` from shared)
- `types/` — JSDoc for `Unit`, `DamageInfo`, the snapshot tuples, and the protocol message envelope
`strict` is off. `tsc` does not read `jsconfig.json` on its own; the script passes `-p`.
## Widen it
Add one file or one folder to `jsconfig.json` `include`, then run `npm run typecheck`. Fix JSDoc that disagrees with the code in that folder. Leave `strict` off until the folder reports nothing. Do not add `@ts-nocheck`.
Suggested order once the sim split settles: `server/sim/grid.js`, `server/sim/body.js`, `server/sim/buffs.js`, then `server/sim/damage.js` against the typedefs in `types/core.js`. The sim does not import `types/`. The typedefs are a reference, not a runtime dependency.
`types/core.js` follows docs/DESIGN.md §5.2, §5.4, §8 and docs/SIM.md §4, §9. Where those two disagree on `DamageInfo.type`, the typedef uses the union `makeDamageInfo` actually builds (`'elemental'` as well as `'element'`).
## Not in the first slice
`shared/protocol.js` — the catalogue JSDoc is ahead of a few object literals (`$optional` sits on a `Record` of checkers; `unitStatsEntry` does not list `side` or `silenced`, and the code reads `side`). Fix the comments, then remove it from `exclude`.
`shared/bandBonds.js` — the data-shape JSDoc rejects an empty fallback object and a tuple callback. Same treatment.
+119
View File
@@ -0,0 +1,119 @@
/**
* Core shapes for the sim and the wire protocol.
*
* Taken from docs/DESIGN.md §5.2 (Unit), §5.4 (DamageInfo), §8 (envelope, b.snap)
* and docs/SIM.md §4 (DamageInfo as makeDamageInfo builds it) and §9 (snapshot tuples).
* The sim does not import this file. It is here so the shapes have one JSDoc home
* while server/sim is still being split.
*/
/**
* A unit inside one battle. Minimum fields from DESIGN §5.2.
* Stats (`maxHp`, `atk`, …) are recomputed by the sim and are not all listed here.
*
* @typedef {Object} Unit
* @property {number} id unique per battle
* @property {'ally'|'enemy'} side
* @property {'op'|'token'|'enemy'|'device'} kind
* @property {string} defId
* @property {string|number} ownerId player id; enemies use the field owner or the source player
* @property {number} x
* @property {number} y
* @property {number} [tileR] operators
* @property {number} [tileC] operators
* @property {'UP'|'RIGHT'|'DOWN'|'LEFT'} [dir] allies: deploy direction. Enemies are not meaningful here.
* @property {number} facing horizontal sign, +1 or −1 (LEFT is −1)
* @property {number} hp
* @property {boolean} alive
* @property {number} sp
* @property {Object[]} [buffs]
* @property {number} [statusFlags]
* @property {number[]} [blocking] operators: ids of enemies they block
* @property {number|null} [blockedBy] enemies: blocker id
* @property {number[][]} [liveRangeGrid] the attack range the detail card shows
* @property {string} [form] model form (enemy mode, or a doll's `'doll'`)
*/
/**
* One damage instance, after `makeDamageInfo` (server/sim/damage.js).
* DESIGN §5.4 lists `type` as `'phys'|'arts'|'true'|'element'`.
* SIM §4 and the normaliser also use `'elemental'` (HP damage from an element burst)
* and keep `'element'` for gauge fill, which removes no HP.
*
* @typedef {Object} DamageInfo
* @property {number} amount pre-mitigation
* @property {'phys'|'arts'|'true'|'elemental'|'element'} type
* @property {'burn'|'necrosis'|'neural'|'apoptosis'|string|null} [element]
* @property {number} [atkScale]
* @property {number} [defIgnoreFlat]
* @property {number} [defIgnorePct]
* @property {number} [resIgnoreFlat]
* @property {number} [resIgnorePct]
* @property {number} [mul]
* @property {boolean} [canDodge]
* @property {boolean} [isSkill]
* @property {boolean} [isSplash]
* @property {boolean} [isAttack]
* @property {boolean} [isProjectile]
* @property {string[]} [tags]
* @property {boolean} [cancel] set by a `hit` hook to drop the instance
* @property {boolean} [noSp]
* @property {boolean} [ignoreSleep]
* @property {boolean} [ignoreSelect] reaches an airborne ally; the hit selected nobody
* @property {boolean} [sourceless] 无来源: source stats add nothing
* @property {number} [attackId] shared by every instance of one normal attack; 0 otherwise
* @property {Unit|null} [traitAlly]
*/
/**
* One living unit in a snapshot.
* `[id, x, y, hp, maxHp, sp, spMax, flags, anim]` (SIM §9, DESIGN §8.2).
* `flags` is the UF bitmask. `anim` is the ANIM code.
*
* @typedef {[number, number, number, number, number, number, number, number, number]} UnitSnap
*/
/**
* A knocked-out operator waiting to redeploy.
* `[id, respawnAt, respawnTime, state, row, col]`.
* `state`: 0 counting, 1 timer done and DP short, 2 timer done and the tile is taken.
*
* @typedef {[number, number, number, number, number, number]} DownSnap
*/
/**
* Element gauge or burst cooldown on one unit.
* `[id, element, fill, cooldownEnd, cooldown]`.
*
* @typedef {[number, string, number, number, number]} ElemSnap
*/
/**
* `snapshot()` (SIM §9). On the wire this object is the payload of `b.snap`,
* and `t` is sent as `gt` because the frame's `t` is the message type (DESIGN §8.2).
* `dp` is one number. `down` and `elem` are omitted when empty.
*
* @typedef {Object} FieldSnapshot
* @property {string} fieldId
* @property {number} t game seconds
* @property {UnitSnap[]} units
* @property {number} dp
* @property {number} killed
* @property {number} total
* @property {Object<string, number>} [dps]
* @property {Object} [boss]
* @property {DownSnap[]} [down]
* @property {ElemSnap[]} [elem]
*/
/**
* One WebSocket JSON frame (DESIGN §8). Client requests may carry `rid`.
* The server's `ok` or `error` echoes it. Server pushes omit it.
* Other own fields are the payload. Inbound frames are capped at 64 KB.
*
* @typedef {Object} ProtocolMessage
* @property {string} t message type
* @property {number} [rid] request id
*/
export {};