From 46a1e6fd6afcb1f4644756956025a8300a5f4a94 Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Wed, 14 Oct 2026 16:00:45 +0800 Subject: [PATCH 1/4] fix(story): cull per-object lights against the layer of each draw The story renderer counted the additional lights of every draw (unity_LightData.y) as if the renderer were on the Camera1 layer. Each item of the field's draw list now uses its own layer: a Live2D character the layer of its stage slot (Default when it stands off the five slots) and an effect renderer its own, so a light's culling mask decides which of them it lights, as the engine's per-object light culling does. With additional vertex lights on (Best quality), effects on the AdvBack and AdvFront layers and characters off the five slots no longer take lights whose culling mask leaves out their layer. A unit test draws items on several layers under two lights. Co-Authored-By: Claude Opus 5.5 --- src/story/renderer.js | 17 +++++++++++------ tests/unit/story-renderer.test.mjs | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/story/renderer.js b/src/story/renderer.js index ed77af4..4a31f92 100644 --- a/src/story/renderer.js +++ b/src/story/renderer.js @@ -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/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]); +}); From 278641afeb4c5a0b92ce4dbb2e58fe683ea8c60a Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Wed, 14 Oct 2026 16:01:03 +0800 Subject: [PATCH 2/4] refactor: drop unused bindings and the unused page lead texts Removes imports, variables and a helper nothing reads (in the CRI Lips pipeline, the live canvas, the story video feature, the UI renderer's layout, the story author and data validator scripts, and four unit tests), and the chart data page's lead texts, which the page heading no longer shows. The story renderer's render-target setter no longer returns a value, the video preload loop says what its empty body does, and an asset that fails to decode keeps the decoder's error as the cause of the error it raises. No behaviour changes. Co-Authored-By: Claude Opus 5.5 --- examples/songs/songs.js | 3 +-- examples/songs/text.js | 10 ---------- scripts/story-author.mjs | 2 +- scripts/validate-data.mjs | 1 - src/data/assets.js | 2 +- src/live/canvas.js | 2 +- src/live2d/crilips/index.js | 1 - src/live2d/crilips/pipeline.js | 1 - src/story/features/video.js | 7 +------ src/story/renderer.js | 2 +- src/ui/renderer.js | 2 +- tests/unit/browser-font.test.mjs | 1 - tests/unit/story-core.test.mjs | 3 +-- tests/unit/story-schema.test.mjs | 1 - tests/unit/ui.test.mjs | 2 +- 15 files changed, 9 insertions(+), 31 deletions(-) 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/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/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 4a31f92..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); 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-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'; From eac5fb4a62979ab8b62420ff945d319e1e00e3b9 Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Wed, 14 Oct 2026 16:01:21 +0800 Subject: [PATCH 3/4] ci: lint the sources, scripts, examples and tests with ESLint Adds ESLint with its recommended rules (eslint.config.js) as a development dependency and `npm run lint`, which CI and the release verification job run after the type check. The config knows the page globals of the Live2D and Spine runtimes, ignores dist/, reports unused variables other than arguments and caught errors, and leaves out no-useless-assignment so ports keep the statement order of the code they reproduce. The published package is unchanged. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 1 + CONTRIBUTING.md | 4 +- README.en.md | 1 + README.md | 1 + eslint.config.js | 25 + package-lock.json | 993 ++++++++++++++++++++++++++++++++++ package.json | 4 + 8 files changed, 1029 insertions(+), 1 deletion(-) create mode 100644 eslint.config.js 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/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" } } From 3780859addd6c4e61dc92cba8e0c94b80a5298b0 Mon Sep 17 00:00:00 2001 From: nichinichisou Date: Wed, 14 Oct 2026 16:07:47 +0800 Subject: [PATCH 4/4] fix(story): refuse nested DOTween Sequences Sequence.ApplyInternalCycle places a nested Sequence's position by its multiCycleStep argument, which the player's Sequence cycle does not reproduce. Inserting a Sequence into a Sequence now raises "nested Sequences not implemented" (a StoryCommandError) instead of playing it with positions the game would not reach, as the DOTween Pro sequence component already does for its nested sequences. The unused argument is removed from the cycle's callers. Sequences of tweens and callbacks play as before. Co-Authored-By: Claude Opus 5.5 --- src/story/features/dotween-core.js | 11 +++++++---- tests/unit/story-feature-still.test.mjs | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) 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/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(); });