diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76c4b46..52c9baf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: - run: npm ci --no-audit --no-fund - run: npm test - run: npm run typecheck + - run: npm run lint # --check builds twice and fails when the two outputs differ - run: node scripts/build.mjs --check - name: Package contents diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f05969..6c6d6eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,6 +31,7 @@ jobs: - run: npm ci --no-audit --no-fund - run: npm test - run: npm run typecheck + - run: npm run lint - run: node scripts/build.mjs --check - name: Check the tag against package.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3be30a..da9d80d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,12 +12,14 @@ data is outside this repository. ## Development -Node.js 22 or later. There are no runtime dependencies; the development dependencies are esbuild and TypeScript. +Node.js 22 or later. There are no runtime dependencies; the development dependencies are esbuild, TypeScript and +ESLint. ```sh npm ci npm test # unit tests: node --test tests/unit/*.test.mjs npm run typecheck # type-checks tests/types/ against types/ +npm run lint # ESLint's recommended rules (eslint.config.js) npm run build # dist/: ESM, minified ESM and IIFE bundles with source maps node scripts/build.mjs --check # builds twice and fails if the outputs differ ``` diff --git a/README.en.md b/README.en.md index 27608ec..d7f04cb 100644 --- a/README.en.md +++ b/README.en.md @@ -232,6 +232,7 @@ npm ci npm test # unit tests (node --test, synthetic inputs only) npm run build # browser bundles in dist/ npm run typecheck # checks the type declarations in types/ +npm run lint # ESLint (eslint.config.js) npm run validate-data -- [chart id ...] # validates a site against the data format node scripts/story-author.mjs help # the commands of the story script tool (experimental) OURNOTES_DATA= npm run test:data # opt-in: runs the player in Node on real chart data diff --git a/README.md b/README.md index 018ebc1..3c15568 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ npm ci npm test # 单元测试(node --test,仅使用合成输入) npm run build # 构建 dist/ 下的浏览器包 npm run typecheck # 检查 types/ 下的类型声明 +npm run lint # ESLint 检查(eslint.config.js) npm run validate-data -- <站点目录> [谱面 id ...] # 按数据格式校验一个站点 node scripts/story-author.mjs help # 自写剧情工具(实验功能)的命令 OURNOTES_DATA=<站点目录> npm run test:data # 可选:用真实谱面数据在 Node 中运行播放器 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..bd2a456 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,25 @@ +// ESLint: the recommended rules over the sources, scripts, examples and tests (`npm run lint`). The player runs in +// browsers, the scripts and tests in Node.js; the Live2D and Spine runtimes are page scripts that define globals. +// The ports keep the statement order of the code they reproduce, including assignments a later branch overwrites, so +// no-useless-assignment is off. +import js from "@eslint/js"; +import globals from "globals"; + +export default [ + { ignores: ["dist/"] }, + js.configs.recommended, + { + languageOptions: { + ecmaVersion: 2024, + sourceType: "module", + globals: { + ...globals.browser, ...globals.node, + Live2DCubismCore: "readonly", Live2DCubismMotionSyncCore: "readonly", spine: "readonly", + }, + }, + rules: { + "no-unused-vars": ["error", { args: "none", caughtErrors: "none", ignoreRestSiblings: true }], + "no-useless-assignment": "off", + }, + }, +]; diff --git a/examples/songs/songs.js b/examples/songs/songs.js index 7c4fc52..c3319ff 100644 --- a/examples/songs/songs.js +++ b/examples/songs/songs.js @@ -283,8 +283,7 @@ const main = async () => { // the route heading: the view's title and what it answers const renderHead = () => { const t = u(); - const [title, lead] = S.view === "guide" ? [g().title, g().lead] - : [t.heads[S.view], t.lead[S.view](songs.songs.length, rows.length)]; + const title = S.view === "guide" ? g().title : t.heads[S.view]; put(head, h("div", { class: "page-head" }, h("div", { class: "page-head-title" }, icon("star", "ic star"), h("h1", {}, title), betaBadge("page-beta-note"), h("span", { class: "track", "aria-hidden": "true" })), S.view === "guide" && g().reminder ? h("aside", {id:"page-beta-note",class:"guide-reminder",role:"note"}, h("strong",{},g().reminder.title),g().reminder.priority ? h("p",{},h("strong",{},g().reminder.priority)) : null, h("p",{},modelText(g().reminder.text))) : h("span", {id:"page-beta-note",class:"sr-only"},t.betaNote))); }; diff --git a/examples/songs/text.js b/examples/songs/text.js index 6a10f49..af565c6 100644 --- a/examples/songs/text.js +++ b/examples/songs/text.js @@ -12,10 +12,6 @@ export const UI = { jackets: "封面", jacketsHint: "显示或隐藏封面", theme: "切换浅色 / 深色", swap: "交换横纵轴", band: "乐队", difficulty: "难度", heads: { rank: "谱面排行", charts: "谱面图表" }, - lead: { - rank: (songs, charts) => `${songs} 首歌、${charts} 张谱面的效率、活动评级、速度、等级与时长。得分数值按所选游玩场景计算:默认撃奏ライブ、三个区间都是第 1 名(最佳情况),也可切到不开激走的自由 Live,见说明。点任意一行看谱面详情;作词作曲、音频和谱面预览见 moenotes。`, - charts: (songs, charts) => `${songs} 首歌、${charts} 张谱面的分布。散点图的两轴可任选,点任意一点看谱面详情。`, - }, empty: "没有符合条件的谱面", emptyHint: "当前的乐队、难度、搜索词或“只看前沿”把所有谱面都排除了。", clear: "清除筛选", diff: { easy: "Easy", normal: "Normal", hard: "Hard", expert: "Expert" }, rankBy: { @@ -132,10 +128,6 @@ export const UI = { jackets: "Jackets", jacketsHint: "Show or hide the jackets", theme: "Light / dark", swap: "Swap the axes", band: "Band", difficulty: "Difficulty", heads: { rank: "Chart rankings", charts: "Chart figures" }, - lead: { - rank: (songs, charts) => `Efficiency, event ranks, speed, level and length of ${songs} songs and ${charts} charts. Score figures follow the play scenario chosen: by default Gekisou Live at rank 1 in all three ranges (the best case), or Free Live without Gekisou; see the guide. Open any row for the chart's details; credits, audio and chart previews are on moenotes.`, - charts: (songs, charts) => `How ${songs} songs and ${charts} charts spread. Pick any two figures for the scatter; open any point for the chart's details.`, - }, empty: "No chart matches", emptyHint: "The band, difficulties, search or “frontier only” leave out every chart.", clear: "Clear filters", diff: { easy: "Easy", normal: "Normal", hard: "Hard", expert: "Expert" }, rankBy: { @@ -354,7 +346,6 @@ export const REPLAY_UI = { export const GUIDE = { "zh": { "title": "定义、推导与成立条件", - "lead": "排行与适性展示理论最佳打法的统计参考;谱面详情还可逐音符计算给定输入的一局。新 chart-stats/3 使用标称概率期望与计算包围区间,旧 music-data/1 的采样统计仍可读取。两者使用同一套 ournotes-deck Rust 模型,原生逐帧核对的案例见下表。", "sections": [ { "title": "得分模型", @@ -639,7 +630,6 @@ export const GUIDE = { }, "en": { "title": "Definitions, derivations and conditions", - "lead": "Rankings and aptitude show statistical references for theoretical best play. Chart details can also calculate one play from per-note inputs. New chart-stats/3 data uses nominal expectations and numerical enclosures; legacy music-data/1 samples remain readable. Both use the same ournotes-deck Rust model; the native frame checks are listed below.", "sections": [ { "title": "Score model", diff --git a/package-lock.json b/package-lock.json index 1b197c6..9a35155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,40 @@ "version": "0.1.7", "license": "AGPL-3.0-only", "devDependencies": { + "@eslint/js": "10.0.1", "esbuild": "0.25.10", + "eslint": "10.12.0", + "globals": "17.13.0", "typescript": "5.9.3" }, "engines": { "node": ">=22" } }, + "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": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "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": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", @@ -458,6 +485,362 @@ "node": ">=18" } }, + "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" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "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" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/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/@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/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/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", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "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/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": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "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/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/@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/@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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "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/@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/@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/@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/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "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/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/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/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": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "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": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -500,6 +883,567 @@ "@esbuild/win32-x64": "0.25.10" } }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "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" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/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/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/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/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/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/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/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/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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "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/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/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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/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/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/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/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/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/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/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/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/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/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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/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": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/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/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/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/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/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/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/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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -513,6 +1457,55 @@ "engines": { "node": ">=14.17" } + }, + "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/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/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/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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 93fdc1f..455e769 100644 --- a/package.json +++ b/package.json @@ -111,11 +111,15 @@ "test": "node --test tests/unit/*.test.mjs", "test:data": "node --test tests/data/*.test.mjs", "typecheck": "tsc -p tests/types", + "lint": "eslint .", "validate-data": "node scripts/validate-data.mjs", "read-set": "node scripts/read-set.mjs" }, "devDependencies": { + "@eslint/js": "10.0.1", "esbuild": "0.25.10", + "eslint": "10.12.0", + "globals": "17.13.0", "typescript": "5.9.3" } } diff --git a/scripts/story-author.mjs b/scripts/story-author.mjs index 6d3ab18..1213400 100644 --- a/scripts/story-author.mjs +++ b/scripts/story-author.mjs @@ -31,7 +31,7 @@ import fs from "node:fs"; import path from "node:path"; import { Browser, findBrowser } from "./lib/story-author/chrome.js"; import { - FONT_PRESETS, SCRIPT_FORMAT, checkScript, compileScript, fontFamily, motionStem, motionStems, + FONT_PRESETS, SCRIPT_FORMAT, checkScript, compileScript, fontFamily, motionStems, parseScript, scriptTexts, } from "./lib/story-author/script.js"; import { CUBISM_CORE_URL, startServer } from "./lib/story-author/server.js"; diff --git a/scripts/validate-data.mjs b/scripts/validate-data.mjs index e4efdbe..765673b 100644 --- a/scripts/validate-data.mjs +++ b/scripts/validate-data.mjs @@ -52,7 +52,6 @@ const S = { storyFonts: schema("story-fonts"), storyLanguage: schema("story-language"), }; -const TEXT_FILE = /\.(json|glsl)$/; const STORY_HOST_FORMAT = "ournotes.story-host/1"; const STORY_MANIFEST_SITED = "ournotes.story-manifest/2"; const extOf = (p) => (p.match(/\.([A-Za-z0-9]+)$/) || [])[1]?.toLowerCase() ?? ""; diff --git a/src/data/assets.js b/src/data/assets.js index b49595d..aa11efe 100644 --- a/src/data/assets.js +++ b/src/data/assets.js @@ -104,7 +104,7 @@ export class AssetStore { if (j.stored !== undefined && raw.byteLength === j.stored) { const encoding = assetEncoding(asset); if (!encoding) throw new Error(`${j.path}: ${asset} has a stored size but no .gz / .br name`); - try { buf = u8(await decode(raw, encoding)); } catch (e) { throw new Error(`${j.path}: ${asset}: ${e.message}`); } + try { buf = u8(await decode(raw, encoding)); } catch (e) { throw new Error(`${j.path}: ${asset}: ${e.message}`, { cause: e }); } requestSignal.throwIfAborted(); if (buf.byteLength !== j.size) throw new Error(`${j.path}: ${buf.byteLength} bytes decoded, manifest ${j.size}`); } else if (raw.byteLength !== j.size) { diff --git a/src/live/canvas.js b/src/live/canvas.js index 52fb1f4..ba3a67b 100644 --- a/src/live/canvas.js +++ b/src/live/canvas.js @@ -82,7 +82,7 @@ export class LiveRenderCanvas { const atlas = await GLTex.load(gl, this.r.base, sp.texture); this.shadowSprite = new UISprite(sp.sprite, { ...sp, pixelsPerUnit: sp.pixelsToUnits }, atlas); this.buf = { vao: gl.createVertexArray(), vbo: gl.createBuffer(), ibo: gl.createBuffer() }; - for (const [node, e] of this.graphics) e.mat = this._material(e.g.m_Material); + for (const e of this.graphics.values()) e.mat = this._material(e.g.m_Material); // AspectRatioFitter.aspectRatio = (float)tex.width / (float)tex.height of the composite (FullInitialize) this.aspect = F(F(this.bg.target.width) / F(this.bg.target.height)); this.copy = LiveCopyProgram.create(gl); diff --git a/src/live2d/crilips/index.js b/src/live2d/crilips/index.js index c1f4722..ac746a8 100644 --- a/src/live2d/crilips/index.js +++ b/src/live2d/crilips/index.js @@ -1,4 +1,3 @@ -import { CriLipsData } from "./data.js"; import { CriLipsPipeline } from "./pipeline.js"; // The CRI Lips analyzer object the story hands to Live2DLipSyncController.setLipsAnalyzer for a model diff --git a/src/live2d/crilips/pipeline.js b/src/live2d/crilips/pipeline.js index 44093d2..1863048 100644 --- a/src/live2d/crilips/pipeline.js +++ b/src/live2d/crilips/pipeline.js @@ -1,4 +1,3 @@ -import { F } from "../../engine/core.js"; import { CriBiquadLpf } from "./biquad.js"; import { CriResampler } from "./resample.js"; import { CriLipsCore } from "./lipscore.js"; diff --git a/src/story/features/dotween-core.js b/src/story/features/dotween-core.js index 15d5b40..d854bbc 100644 --- a/src/story/features/dotween-core.js +++ b/src/story/features/dotween-core.js @@ -358,9 +358,11 @@ export class DTSequence extends DTween { return true; } - // Sequence.Insert -> DoInsert: the tween's delay becomes an offset, loops -1 become int.MaxValue, autoKill off + // Sequence.Insert -> DoInsert: the tween's delay becomes an offset, loops -1 become int.MaxValue, autoKill off; a + // nested Sequence is not implemented insert(at, t) { if (!this._validate(t)) return this; + if (t.tweenType === DT_TYPE.Sequence) unsupported("nested Sequences"); this.mgr._remove(t); at = F(at + t.delay); this.lastTweenInsertTime = at; @@ -425,7 +427,7 @@ export class DTSequence extends DTween { if (fired > 0) from = to; else if (isInverse && !this.isBackwards) from = F(this.duration - from); to = isInverse ? 0 : this.duration; - if (this.cycle(from, to, mode, useInverse, isInverse, true)) return true; + if (this.cycle(from, to, mode, useInverse, isInverse)) return true; if (this.loopType === DT_LOOP.Yoyo) isInverse = !isInverse; } if (cycles !== this.completedLoops || cyclePos !== this.position) return !this.active; @@ -437,12 +439,13 @@ export class DTSequence extends DTween { if (newCompletedSteps === 1 && this.isComplete) return false; if (newCompletedSteps > 0 && !this.isComplete) { from = useInverse ? this.duration : 0; - if (this.loopType === DT_LOOP.Restart && to > 0) this.cycle(this.duration, 0, DT_MODE.Goto, false, false, false); + if (this.loopType === DT_LOOP.Restart && to > 0) this.cycle(this.duration, 0, DT_MODE.Goto, false, false); } else from = useInverse ? F(this.duration - prevPos) : prevPos; return this.cycle(from, useInverse ? F(this.duration - newPos) : newPos, mode, useInverse, isInverse); } - // Sequence.ApplyInternalCycle: true when the Sequence must be killed + // Sequence.ApplyInternalCycle: true when the Sequence must be killed. Its multiCycleStep argument only places nested + // Sequences, which insert refuses. cycle(fromPos, toPos, mode, useInverse, prevPosIsInverse) { const wasPlaying = this.isPlaying; const o = this.objs; diff --git a/src/story/features/video.js b/src/story/features/video.js index f41944f..f1c3ad2 100644 --- a/src/story/features/video.js +++ b/src/story/features/video.js @@ -37,11 +37,6 @@ const FAST_FORWARD_MAX_FRAME_DROP = 3, SEEK_RESPEED_FREEZE_ALPHA = F(0.7), SEEK_ // Mathf.Approximately export const approximately = (a, b) => Math.abs(b - a) < Math.max(1e-6 * Math.max(Math.abs(a), Math.abs(b)), 1.1210387714598537e-44); -const waitUntil = async (loop, pred, stop) => { - while (!pred()) { await loop.yield("Update"); if (stop && stop()) return false; } - return true; -}; - // Fwk.Video.VideoInfo with the CRI Mana player state export class VideoInfo { constructor(uid, masterId, rec, source) { @@ -254,7 +249,7 @@ export class StoryVideo { this._hook = (l) => this._update(l.deltaTime); ctx.loop.on("update", this._hook); // Preload: prepare up to two, then the first becomes the current video - while (this._enqueue()) {} + while (this._enqueue()) { /* prepares one */ } if (this.tasks.length) this._setCurrent(this.tasks.shift()); } diff --git a/src/story/renderer.js b/src/story/renderer.js index ed77af4..1fb984c 100644 --- a/src/story/renderer.js +++ b/src/story/renderer.js @@ -157,7 +157,7 @@ export class StoryRenderer { for (const [k, [tw, th, o]] of Object.entries(specs)) { Object.defineProperty(rt, k, { enumerable: true, configurable: true, get: () => { const t = new GLTarget(gl, tw, th, o); own(k, t); return t; }, - set: (v) => own(k, v) }); + set: (v) => { own(k, v); } }); } void rt.color, void rt.post; // every frame draws into these two this.post.resizeBloom(w, h); @@ -314,9 +314,13 @@ export class StoryRenderer { return items.sort((a, b) => (a.sortingOrder - b.sortingOrder) || (b.dist - a.dist)); } - // `prev`: the item drawn just before (Live2DDrawing leaves out the state its previous draw set) + // `prev`: the item drawn just before (Live2DDrawing leaves out the state its previous draw set). A draw's perObject + // without a layer takes the item's. _drawList(items, frame) { - Live2DDrawing.drawItems(StoryRenderer.sortItems(items), (it, prev) => ({ ...frame, layer: it.layer ?? 12, prev })); + Live2DDrawing.drawItems(StoryRenderer.sortItems(items), (it, prev) => { + const layer = it.layer ?? 12; + return { ...frame, layer, perObject: (t, l) => frame.perObject(t, l ?? layer), prev }; + }); } _bindMesh(prog, bufs) { @@ -451,19 +455,20 @@ export class StoryRenderer { const gl = this.gl, w = vp.w, h = vp.h; for (const c of this.characters) c.gl.renderMasks(); // CubismMaskCommandBuffer (before cameras) const globals = { ...this._cameraGlobals(w, h), ...this._lightSheet() }; + // perObject takes the Transform and the layer the renderer is on; the per-object light count is culled by the + // light's culling mask against that layer. _drawList gives each item its own layer, effect draws pass theirs. const frame = { globals, perObject: (t, layer) => this._perObject(t, layer) }; - const liveFrame = { globals, perObject: (t) => this._perObject(t, 6) }; const fr = this.scene.fieldRenderer; // main camera this.rt.color.bind(); gl.disable(gl.SCISSOR_TEST); gl.colorMask(true, true, true, true); const cc = this.scene.camera.clearColor; gl.clearColor(cc[0], cc[1], cc[2], cc[3]); gl.clear(gl.COLOR_BUFFER_BIT); - if (fr.needsOffscreen()) this._offscreen(liveFrame); + if (fr.needsOffscreen()) this._offscreen(frame); else { this.rt.color.bind(); this._drawList([...this._backgroundItems(), ...this._characterItems(), - ...StoryRenderer.mainCameraItems(this._extraItems(liveFrame))], liveFrame); + ...StoryRenderer.mainCameraItems(this._extraItems(frame))], frame); } const stack = this.scene.volume.stack; this.post.buildLut(stack); @@ -476,7 +481,7 @@ export class StoryRenderer { if (fr.capture.pending) { const res = fr.capture.pending; fr.capture.pending = null; this.rt.capture.bind(); gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); - this._drawList([...this._backgroundItems(), ...this._extraItems(liveFrame).filter((it) => it.layer === 12)], liveFrame); + this._drawList([...this._backgroundItems(), ...this._extraItems(frame).filter((it) => it.layer === 12)], frame); this.loop.yield("Update").then(() => res(this.rt.capture)); } // UI overlay camera onto the same target diff --git a/src/ui/renderer.js b/src/ui/renderer.js index af149d6..4655701 100644 --- a/src/ui/renderer.js +++ b/src/ui/renderer.js @@ -149,7 +149,7 @@ export class Renderer { if(forced)return forced; const nativeCanvas=comp(n,'Canvas'); if(nativeCanvas&&nativeCanvas.m_RenderMode!==2)return {x:0,y:0,w:pw,h:ph,pivot:{x:0,y:1}}; - const r=n.rect||{},min=r.m_AnchorMin||{x:.5,y:.5},max=r.m_AnchorMax||min,pos=r.m_AnchoredPosition||{x:0,y:0},sz=r.m_SizeDelta||{x:0,y:0},pivot=r.m_Pivot||{x:.5,y:.5}; + const r=n.rect||{},min=r.m_AnchorMin||{x:.5,y:.5},max=r.m_AnchorMax||min,sz=r.m_SizeDelta||{x:0,y:0},pivot=r.m_Pivot||{x:.5,y:.5}; let w=pw*(max.x-min.x)+sz.x,h=ph*(max.y-min.y)+sz.y; const fit=comp(n,'ContentSizeFitter');if(fit&&fit.m_Enabled!==0){const input=this.layoutInput(n);if(fit.m_HorizontalFit)w=(fit.m_HorizontalFit===1?input.min:input.preferred)[0];if(fit.m_VerticalFit)h=(fit.m_VerticalFit===1?input.min:input.preferred)[1];} const ar=comp(n,'AspectRatioFitter');let aspect=ar?.m_AspectRatio||1; diff --git a/tests/unit/browser-font.test.mjs b/tests/unit/browser-font.test.mjs index d8376b8..4ce2a4f 100644 --- a/tests/unit/browser-font.test.mjs +++ b/tests/unit/browser-font.test.mjs @@ -2,7 +2,6 @@ // inputs only: the rasterizer draws filled boxes. import assert from "node:assert/strict"; import { test } from "node:test"; -import { F } from "../../src/engine/core.js"; import { TMPText } from "../../src/engine/uitext.js"; import { BrowserGlyphFont, oversampleOf, pixelBox, sdfTexels, signedDistance } from "../../src/story/browser-font.js"; import { StoryFontAssets } from "../../src/story/ui-fonts.js"; diff --git a/tests/unit/story-core.test.mjs b/tests/unit/story-core.test.mjs index 350fd59..b18604a 100644 --- a/tests/unit/story-core.test.mjs +++ b/tests/unit/story-core.test.mjs @@ -254,8 +254,7 @@ test("playback speed divides the auto-advance timing", async () => { test("manual advance: a line waits for a tap; a tap while typing reveals it and needs a second tap", async () => { const s = makeStory([talk(0, 1), talk(1, 1)], { auto: false }); const taps = { 30: 1, 40: 1, 45: 1, 200: 1, 260: 1 }; - let n = 0; - await run(s, { each: (k) => { n = k; if (taps[k]) s.core.tap(); } }); + await run(s, { each: (k) => { if (taps[k]) s.core.tap(); } }); const f = (k) => Number(s.trace.find((t) => t.includes(` #${k} `)).split(" ")[0]); // line 0 shows at frame 1; the tap at 30 (typing over) advances it; line 1's first tap (40) comes while typing assert.ok(f(1) >= 31 && f(1) <= 32, `line 1 at ${f(1)}`); diff --git a/tests/unit/story-feature-still.test.mjs b/tests/unit/story-feature-still.test.mjs index c35dc4a..e95c4be 100644 --- a/tests/unit/story-feature-still.test.mjs +++ b/tests/unit/story-feature-still.test.mjs @@ -90,6 +90,7 @@ test("DOTween Sequence: delays become offsets, callbacks at their time, restart assert.equal(mgr.kill("a"), 1); assert.equal(p1.active, false); assert.equal(mgr.play(tx), 0); // nested tweens are never reached + assert.throws(() => new DTSequence(mgr).insert(0, new DTSequence(mgr)), /nested Sequences not implemented/); mgr.dispose(); }); diff --git a/tests/unit/story-renderer.test.mjs b/tests/unit/story-renderer.test.mjs index bec83ba..3f78865 100644 --- a/tests/unit/story-renderer.test.mjs +++ b/tests/unit/story-renderer.test.mjs @@ -116,3 +116,20 @@ test("FxSpriteRenderer: a program with unity_SpriteColor / unity_SpriteProps tak assert.deepEqual(b.color0, [128 / 255, 64 / 255, 1, 128 / 255].map(F)); assert.equal(a.n, 4); assert.equal(b.n, 4); }); + +test("StoryRenderer: the per-object light count follows the layer of each drawn item", () => { + const loop = new PlayerLoop(30); + const camera = new AdvCamera(loop, cameraNode()); + const r = new StoryRenderer(headlessGL(), null, { camera, session: { stage: null } }, { unityLighting: false }, loop); + // two point lights at the origin that light the character layers Camera1..Camera5 (6..10) + r.addLights = [0, 1].map(() => ({ pos: [0, 0, 0, 1], range: 5, layers: 0b11111000000 })); + const t = new Transform("Item"); + const lightCount = (frame, layer) => frame.perObject(t, layer).unity_LightData[1]; + const frame = { globals: {}, perObject: (tr, layer) => r._perObject(tr, layer) }; + const seen = []; + const item = (layer, ownLayer) => ({ sortingOrder: 0, dist: 0, layer, draw: (f) => seen.push(lightCount(f, ownLayer)) }); + // a character on Camera1 (6) and one off the five slots (Default, 0), effects on AdvBack (12) and AdvFront (13), and + // a draw that names its own layer + r._drawList([item(6), item(0), item(12), item(13), item(12, 8)], frame); + assert.deepEqual(seen, [2, 0, 0, 0, 2]); +}); diff --git a/tests/unit/story-schema.test.mjs b/tests/unit/story-schema.test.mjs index 0aba975..ad7fc53 100644 --- a/tests/unit/story-schema.test.mjs +++ b/tests/unit/story-schema.test.mjs @@ -5,7 +5,6 @@ // No game data. import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; diff --git a/tests/unit/ui.test.mjs b/tests/unit/ui.test.mjs index e83d51e..978130f 100644 --- a/tests/unit/ui.test.mjs +++ b/tests/unit/ui.test.mjs @@ -4,7 +4,7 @@ import {test} from 'node:test'; import {UILibrary,UISession} from '../../src/ui/index.js'; import {resourceURLs} from '../../src/ui/library.js'; import {applyClip,sampleClip,sampleObjectCurves,referenceNode,resolveNodeIndex,mergeResources,assignField} from '../../src/ui/runtime.js'; -import {sequencePlan,SequenceRuntime} from '../../src/ui/tween.js'; +import {sequencePlan} from '../../src/ui/tween.js'; import {rectBox,affine,linearGroupLayout,layoutProperty} from '../../src/ui/layout.js'; import {visibleLines,textHeight,wrappingEnabled} from '../../src/ui/text-layout.js'; import {loadFonts} from '../../src/ui/renderer.js';