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:
commit
3288afc787
14 files changed
+2174
-7
No files matched your search
@@ -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
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Generated
+949
-1
@@ -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
@@ -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
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
@@ -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)));
|
||||
@@ -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
@@ -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 {};
|
||||
Reference in new issue
Block a user