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
|
curl -fsS -o /dev/null http://127.0.0.1:3000/vendor/pixi.min.js
|
||||||
node tools/doctor.mjs || true
|
node tools/doctor.mjs || true
|
||||||
kill $pid || 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"
|
"ws": "^8.22.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
@@ -1401,6 +1405,950 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"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",
|
"postinstall": "node tools/vendor.mjs",
|
||||||
"setup": "node tools/setup.mjs",
|
"setup": "node tools/setup.mjs",
|
||||||
"doctor": "node tools/doctor.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": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
@@ -48,6 +53,10 @@
|
|||||||
"ws": "^8.22.0"
|
"ws": "^8.22.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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(); });
|
const unsub = data.subscribe((n) => { if (alive && names.includes(n)) force(); });
|
||||||
for (const n of names) data.load(n);
|
for (const n of names) data.load(n);
|
||||||
return () => { alive = false; unsub(); };
|
return () => { alive = false; unsub(); };
|
||||||
}, [key]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [key]);
|
||||||
return names.every((n) => {
|
return names.every((n) => {
|
||||||
const s = data.status(n);
|
const s = data.status(n);
|
||||||
return s === 'ready' || s === 'missing';
|
return s === 'ready' || s === 'missing';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
// plays 战场#01, 险境 draws one of 8, 绝境 / 终极 one of 7 (m01 excluded).
|
// plays 战场#01, 险境 draws one of 8, 绝境 / 终极 one of 7 (m01 excluded).
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from '../../vendor/hooks.module.js';
|
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 { html, Button, Icon, MicroLabel, Panel, TextField, PingPill, AvatarFrame, Tooltip, Spinner, DifficultyIcon, doctorNo } from '../ui/components.js';
|
||||||
import { toast, toastError } from '../ui/toasts.js';
|
import { toast, toastError } from '../ui/toasts.js';
|
||||||
import { GuideButton } from '../ui/guide.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[] }}
|
* @returns {{ lines: string[], pages: AtlasPage[] }}
|
||||||
*/
|
*/
|
||||||
export function parseAtlas(text) {
|
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[]} */
|
/** @type {AtlasPage[]} */
|
||||||
const pages = [];
|
const pages = [];
|
||||||
let page = null;
|
let page = null;
|
||||||
@@ -108,7 +108,7 @@ export function normalizeAtlas(text, { pageSize, pma = false, renamePage = null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const result = out.join('\n');
|
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 };
|
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