From 9177ed8f810c1410209eb347df85ee3abb395ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 13:16:11 +0800 Subject: [PATCH 01/26] feat: report remodel recipe v3 facts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/reporters/remodel-recipe.ts | 426 +++++++++++++++++++++++-- tests/reporters/remodel-recipe.test.ts | 341 +++++++++++++++++--- 2 files changed, 706 insertions(+), 61 deletions(-) diff --git a/src/reporters/remodel-recipe.ts b/src/reporters/remodel-recipe.ts index d52bb2c..3b93549 100644 --- a/src/reporters/remodel-recipe.ts +++ b/src/reporters/remodel-recipe.ts @@ -1,4 +1,3 @@ -import moment from 'moment-timezone' import _ from 'lodash' import BaseReporter from './base' import type { APIReqKousyouRemodelSlotRequest } from 'kcsapi/api_req_kousyou/remodel_slot/request' @@ -15,7 +14,7 @@ import type { type RemodelRecipeListItem = Pick< APIReqKousyouRemodelSlotlistResponse, - 'api_id' | 'api_req_bauxite' | 'api_req_bull' | 'api_req_fuel' | 'api_req_steel' + 'api_id' | 'api_req_bauxite' | 'api_req_bull' | 'api_req_fuel' | 'api_req_steel' | 'api_slot_id' > & Partial> @@ -33,7 +32,15 @@ type RemodelRecipeDetailBody = Partial< | 'api_req_slot_id' | 'api_req_slot_num' > -> +> & { + api_change_flag?: number + api_req_slot_id2?: number + api_req_slot_num2?: number + api_req_useitem_id?: number + api_req_useitem_num?: number + api_req_useitem_id2?: number + api_req_useitem_num2?: number +} type RemodelRecipeSlotPostBody = { api_id: string | number @@ -44,11 +51,78 @@ type RemodelRecipeSlotBody = Pick< 'api_remodel_id' | 'api_voice_ship_id' > & { api_after_slot?: { + api_slotitem_id?: number api_level?: number } api_remodel_flag: boolean | number } +interface RequiredItem { + id: number + count: number +} + +interface RemodelRecipeFleetContext { + observedSecondShipId: number + observedFlagshipId: number +} + +interface ItemImprovementAvailabilityPayload { + schemaVersion: 1 + source: 'list' + clientObservedAt: number + recipeId: number + itemId: number + day: number + observedSecondShipId: number + observedFlagshipId: number + detailObserved: false +} + +interface ItemImprovementCostPayload { + schemaVersion: 1 + source: 'detail' + clientObservedAt: number + recipeId: number + itemId: number + itemLevel: number + stage: number + day: number + observedSecondShipId: number + observedFlagshipId: number + fuel: number + ammo: number + steel: number + bauxite: number + buildkit: number + remodelkit: number + certainBuildkit: number + certainRemodelkit: number + reqSlotItems: RequiredItem[] + reqUseItems: RequiredItem[] + changeFlag: number + detailObserved: true +} + +interface ItemImprovementUpdatePayload { + schemaVersion: 1 + source: 'execution' + clientObservedAt: number + recipeId: number + itemId: number + itemLevel: number + day: number + observedSecondShipId: number + observedFlagshipId: number + upgradeObserved: true + upgradeToItemId: number + upgradeToItemLevel: number +} + +type CurrentDetail = ItemImprovementCostPayload & { + slotId: string | number +} + interface RemodelRecipeReportPayload { recipeId: number itemId: number @@ -70,10 +144,79 @@ interface RemodelRecipeReportPayload { key: string } +const ITEM_IMPROVEMENT_RECIPE_REPORT_PATH = '/api/report/v3/item_improvement_recipe' + +const hasOwn = (record: object, key: string): boolean => + Object.prototype.hasOwnProperty.call(record, key) + +const parseInt10 = (value: string | number): number => parseInt(String(value), 10) + +const getJstDay = (time: number): number => { + const date = new Date(time) + const utcDay = date.getUTCDay() + const utcHour = date.getUTCHours() + return utcHour >= 15 ? (utcDay + 1) % 7 : utcDay +} + +const normalizeRequiredPairs = ( + ...pairs: Array<{ + id: number | undefined + count: number | undefined + present: boolean + }> +): RequiredItem[] => { + const counts = new Map() + + for (const { id, count, present } of pairs) { + if (!present || (id === 0 && count === 0)) { + continue + } + if ( + !Number.isInteger(id) || + !Number.isInteger(count) || + id == null || + count == null || + id <= 0 || + count <= 0 + ) { + throw new Error(`Invalid required item pair: ${String(id)}/${String(count)}`) + } + counts.set(id, (counts.get(id) || 0) + count) + } + + return [...counts.entries()].map(([id, count]) => ({ id, count })).sort((a, b) => a.id - b.id) +} + +const toCostPayload = (detail: CurrentDetail): ItemImprovementCostPayload => ({ + schemaVersion: detail.schemaVersion, + source: detail.source, + clientObservedAt: detail.clientObservedAt, + recipeId: detail.recipeId, + itemId: detail.itemId, + itemLevel: detail.itemLevel, + stage: detail.stage, + day: detail.day, + observedSecondShipId: detail.observedSecondShipId, + observedFlagshipId: detail.observedFlagshipId, + fuel: detail.fuel, + ammo: detail.ammo, + steel: detail.steel, + bauxite: detail.bauxite, + buildkit: detail.buildkit, + remodelkit: detail.remodelkit, + certainBuildkit: detail.certainBuildkit, + certainRemodelkit: detail.certainRemodelkit, + reqSlotItems: detail.reqSlotItems, + reqUseItems: detail.reqUseItems, + changeFlag: detail.changeFlag, + detailObserved: detail.detailObserved, +}) + // Collecting remodel recipes export default class RemodelRecipeReporter extends BaseReporter { id: number itemId: number + itemLevel: number recipeId: number recipes: Record day: number @@ -88,6 +231,7 @@ export default class RemodelRecipeReporter extends BaseReporter { remodelkit: number certainBuildkit: number certainRemodelkit: number + currentDetail: CurrentDetail | undefined // a recipe = // id -> /kcsapi/api_req_kousyou/remodel_slotlist_detail postBody.api_id, @@ -113,6 +257,7 @@ export default class RemodelRecipeReporter extends BaseReporter { super() this.id = -1 this.itemId = -1 + this.itemLevel = -1 this.recipeId = -1 this.recipes = {} this.day = -1 @@ -127,8 +272,12 @@ export default class RemodelRecipeReporter extends BaseReporter { this.remodelkit = 0 this.certainBuildkit = 0 this.certainRemodelkit = 0 + this.currentDetail = undefined } - getStage(level: number) { + getStage(level: number, changeFlag = 0) { + if (changeFlag) { + return 2 + } switch (true) { case level >= 0 && level < 6: return 0 @@ -140,50 +289,239 @@ export default class RemodelRecipeReporter extends BaseReporter { return -1 } } + + getFleetContext(): RemodelRecipeFleetContext | undefined { + const deck = window._decks[0] + const flagshipRosterId = deck?.api_ship[0] + if (flagshipRosterId == null || Number(flagshipRosterId) <= 0) { + return undefined + } + + const flagship = window._ships[flagshipRosterId] + const observedFlagshipId = flagship?.api_ship_id + if (!observedFlagshipId) { + return undefined + } + + const secondRosterId = deck?.api_ship[1] + if (secondRosterId == null || Number(secondRosterId) <= 0) { + return { + observedSecondShipId: 0, + observedFlagshipId, + } + } + + const secondShip = window._ships[secondRosterId] + const observedSecondShipId = secondShip?.api_ship_id + if (!observedSecondShipId) { + return undefined + } + + return { + observedSecondShipId, + observedFlagshipId, + } + } + + normalizeReqSlotItems(response: RemodelRecipeDetailBody): RequiredItem[] { + return normalizeRequiredPairs( + { + id: response.api_req_slot_id, + count: response.api_req_slot_num, + present: hasOwn(response, 'api_req_slot_id') || hasOwn(response, 'api_req_slot_num'), + }, + { + id: response.api_req_slot_id2, + count: response.api_req_slot_num2, + present: hasOwn(response, 'api_req_slot_id2') || hasOwn(response, 'api_req_slot_num2'), + }, + ) + } + + normalizeReqUseItems(response: RemodelRecipeDetailBody): RequiredItem[] { + return normalizeRequiredPairs( + { + id: response.api_req_useitem_id, + count: response.api_req_useitem_num, + present: hasOwn(response, 'api_req_useitem_id') || hasOwn(response, 'api_req_useitem_num'), + }, + { + id: response.api_req_useitem_id2, + count: response.api_req_useitem_num2, + present: + hasOwn(response, 'api_req_useitem_id2') || hasOwn(response, 'api_req_useitem_num2'), + }, + ) + } + + hasExactDetailCosts(response: RemodelRecipeDetailBody): boolean { + return ( + hasOwn(response, 'api_req_buildkit') && + hasOwn(response, 'api_req_remodelkit') && + hasOwn(response, 'api_certain_buildkit') && + hasOwn(response, 'api_certain_remodelkit') + ) + } + + resetExecutionState() { + this.currentDetail = undefined + this.fuel = undefined + } + handle( method: GameApiMethod, path: GameApiPath, body: GameApiResponseBody, postBody: GameApiPostBody, + time = Date.now(), ) { switch (path) { case '/kcsapi/api_req_kousyou/remodel_slotlist': { - this.recipes = _.keyBy(body as RemodelRecipeListItem[], 'api_id') + const response = body as RemodelRecipeListItem[] + const day = getJstDay(time) + const context = this.getFleetContext() + this.recipes = _.keyBy(response, 'api_id') + + if (!context) { + console.error('Invalid remodel recipe fleet context') + return + } + + const records = response + .map((recipe): ItemImprovementAvailabilityPayload | undefined => { + const recipeId = recipe.api_id + const itemId = recipe.api_slot_id + if (!Number.isInteger(recipeId) || !Number.isInteger(itemId)) { + return undefined + } + return { + schemaVersion: 1, + source: 'list', + clientObservedAt: time, + recipeId, + itemId, + day, + observedSecondShipId: context.observedSecondShipId, + observedFlagshipId: context.observedFlagshipId, + detailObserved: false, + } + }) + .filter((record): record is ItemImprovementAvailabilityPayload => record != null) + + if (records.length) { + void this.report(ITEM_IMPROVEMENT_RECIPE_REPORT_PATH, { records }) + } } break case '/kcsapi/api_req_kousyou/remodel_slotlist_detail': { const response = body as RemodelRecipeDetailBody const request = postBody as RemodelRecipeDetailPostBody + this.resetExecutionState() + if (Object.keys(this.recipes).length === 0) { return } - const utc = moment.utc() - const hour = utc.hour() - const day = utc.day() - // remodel list refreshes at 00:00 UTC+9 - this.day = hour >= 15 ? (day + 1) % 7 : day - this.recipeId = parseInt(request.api_id) + this.day = getJstDay(time) + this.recipeId = parseInt10(request.api_id) + if (!Number.isInteger(this.recipeId)) { + console.error(`Invalid remodel recipe id: ${String(request.api_id)}`) + return + } const itemSlotId = request.api_slot_id - this.itemId = (window._slotitems[itemSlotId] || {}).api_slotitem_id || -1 - const itemLevel = (window._slotitems[itemSlotId] || {}).api_level || -1 - this.stage = this.getStage(itemLevel) + const slotItem = window._slotitems[itemSlotId] + this.itemId = slotItem?.api_slotitem_id ?? -1 + this.itemLevel = slotItem?.api_level ?? -1 + const changeFlag = response.api_change_flag ?? 0 + this.stage = this.getStage(this.itemLevel, changeFlag) + if (this.itemId < 0 || this.stage < 0) { + console.error('Invalid remodel recipe slot item data') + return + } + const recipe: Partial = this.recipes[this.recipeId] || {} + const fuel = recipe.api_req_fuel + const ammo = recipe.api_req_bull + const steel = recipe.api_req_steel + const bauxite = recipe.api_req_bauxite + const buildkit = response.api_req_buildkit + const remodelkit = response.api_req_remodelkit + const certainBuildkit = response.api_certain_buildkit + const certainRemodelkit = response.api_certain_remodelkit + if ( + fuel == null || + ammo == null || + steel == null || + bauxite == null || + buildkit == null || + remodelkit == null || + certainBuildkit == null || + certainRemodelkit == null || + !this.hasExactDetailCosts(response) + ) { + console.error('Invalid remodel recipe detail data') + return + } - this.fuel = recipe.api_req_fuel || 0 - this.ammo = recipe.api_req_bull || 0 - this.steel = recipe.api_req_steel || 0 - this.bauxite = recipe.api_req_bauxite || 0 + this.fuel = fuel + this.ammo = ammo + this.steel = steel + this.bauxite = bauxite this.reqItemId = response.api_req_slot_id || -1 this.reqItemCount = response.api_req_slot_num || 0 - this.buildkit = response.api_req_buildkit || 0 - this.remodelkit = response.api_req_remodelkit || 0 - this.certainBuildkit = response.api_certain_buildkit || 0 - this.certainRemodelkit = response.api_certain_remodelkit || 0 + this.buildkit = buildkit + this.remodelkit = remodelkit + this.certainBuildkit = certainBuildkit + this.certainRemodelkit = certainRemodelkit + + const context = this.getFleetContext() + if (!context) { + console.error('Invalid remodel recipe fleet context') + return + } + + let reqSlotItems: RequiredItem[] + let reqUseItems: RequiredItem[] + try { + reqSlotItems = this.normalizeReqSlotItems(response) + reqUseItems = this.normalizeReqUseItems(response) + } catch (err) { + console.error(err instanceof Error ? err.message : err) + return + } + + const detail: CurrentDetail = { + schemaVersion: 1, + source: 'detail', + clientObservedAt: time, + recipeId: this.recipeId, + itemId: this.itemId, + itemLevel: this.itemLevel, + stage: this.stage, + day: this.day, + observedSecondShipId: context.observedSecondShipId, + observedFlagshipId: context.observedFlagshipId, + fuel: this.fuel, + ammo: this.ammo, + steel: this.steel, + bauxite: this.bauxite, + buildkit: this.buildkit, + remodelkit: this.remodelkit, + certainBuildkit: this.certainBuildkit, + certainRemodelkit: this.certainRemodelkit, + reqSlotItems, + reqUseItems, + changeFlag, + detailObserved: true, + slotId: itemSlotId, + } + this.currentDetail = detail + + void this.report(ITEM_IMPROVEMENT_RECIPE_REPORT_PATH, toCostPayload(detail)) } break case '/kcsapi/api_req_kousyou/remodel_slot': @@ -193,13 +531,27 @@ export default class RemodelRecipeReporter extends BaseReporter { if (typeof this.fuel === 'undefined') { return } + const currentDetail = this.currentDetail if (this.itemId != response.api_remodel_id[0]) { console.error(`Inconsistent remodel item data: ${this.itemId}, ${request.api_slot_id}`) + this.resetExecutionState() return } if (this.recipeId != request.api_id) { console.error(`Inconsistent remodel item data: ${this.recipeId}, ${request.api_id}`) + this.resetExecutionState() + return + } + if ( + currentDetail && + request.api_slot_id != null && + currentDetail.slotId != request.api_slot_id + ) { + console.error( + `Inconsistent remodel item data: ${currentDetail.slotId}, ${request.api_slot_id}`, + ) + this.resetExecutionState() return } @@ -208,12 +560,19 @@ export default class RemodelRecipeReporter extends BaseReporter { // stage == -1 because /port will not update slotitems with api_level, they are // updated only when restarting game if (!response.api_remodel_flag || this.stage == -1) { + this.resetExecutionState() return } - const upgradeToItemId = - response.api_remodel_id[1] != this.itemId ? response.api_remodel_id[1] : -1 const afterSlot = response.api_after_slot || {} + const afterSlotItemId = afterSlot.api_slotitem_id + const remodelTargetItemId = response.api_remodel_id[1] + const upgradeToItemId = + afterSlotItemId != null && afterSlotItemId !== this.itemId + ? afterSlotItemId + : remodelTargetItemId != null && remodelTargetItemId !== this.itemId + ? remodelTargetItemId + : -1 const upgradeToItemLevel = upgradeToItemId >= 0 ? (afterSlot.api_level ?? -1) : -1 const secretary = response.api_voice_ship_id || -1 @@ -239,6 +598,25 @@ export default class RemodelRecipeReporter extends BaseReporter { } void this.report('/api/report/v2/remodel_recipe', info) + + if (currentDetail && upgradeToItemId >= 0 && upgradeToItemLevel >= 0) { + const update: ItemImprovementUpdatePayload = { + schemaVersion: 1, + source: 'execution', + clientObservedAt: currentDetail.clientObservedAt, + recipeId: currentDetail.recipeId, + itemId: currentDetail.itemId, + itemLevel: currentDetail.itemLevel, + day: currentDetail.day, + observedSecondShipId: currentDetail.observedSecondShipId, + observedFlagshipId: currentDetail.observedFlagshipId, + upgradeObserved: true, + upgradeToItemId, + upgradeToItemLevel, + } + void this.report(ITEM_IMPROVEMENT_RECIPE_REPORT_PATH, update) + } + this.resetExecutionState() } break } diff --git a/tests/reporters/remodel-recipe.test.ts b/tests/reporters/remodel-recipe.test.ts index 3079b04..a8044b6 100644 --- a/tests/reporters/remodel-recipe.test.ts +++ b/tests/reporters/remodel-recipe.test.ts @@ -9,36 +9,114 @@ import { beforeEach(resetReporterTestState) describe('RemodelRecipeReporter', () => { - it('reports successful remodel recipes with cached recipe cost and day data', () => { - window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } + const detailBody = { + api_req_slot_id: 90, + api_req_slot_num: 2, + api_req_buildkit: 3, + api_req_remodelkit: 4, + api_certain_buildkit: 5, + api_certain_remodelkit: 6, + } + + const listBody = [ + { + api_id: 33, + api_slot_id: 700, + api_req_fuel: 10, + api_req_bull: 20, + api_req_steel: 30, + api_req_bauxite: 40, + }, + ] + + it('reports v3 availability and detail facts without execution', () => { + window._slotitems[501] = { api_slotitem_id: 700, api_level: 0 } + window._decks[0] = { api_ship: [1, -1] } const reporter = new RemodelRecipeReporter() const report = attachReportSpy(reporter) - reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', [ - { - api_id: 33, - api_req_fuel: 10, - api_req_bull: 20, - api_req_steel: 30, - api_req_bauxite: 40, - }, - ]) + reporter.handle( + 'GET', + '/kcsapi/api_req_kousyou/remodel_slotlist', + listBody, + {}, + Date.UTC(2026, 6, 3, 14), + ) reporter.handle( 'POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', { - api_req_slot_id: 90, - api_req_slot_num: 2, - api_req_buildkit: 3, - api_req_remodelkit: 4, - api_certain_buildkit: 5, - api_certain_remodelkit: 6, + ...detailBody, + api_req_slot_id2: 90, + api_req_slot_num2: 1, + api_req_useitem_id: 65, + api_req_useitem_num: 1, + api_req_useitem_id2: 66, + api_req_useitem_num2: 2, + api_change_flag: 0, }, { api_id: '33', api_slot_id: 501, }, + Date.UTC(2026, 6, 3, 15), ) + + expect(report).toHaveBeenNthCalledWith(1, '/api/report/v3/item_improvement_recipe', { + records: [ + { + schemaVersion: 1, + source: 'list', + clientObservedAt: Date.UTC(2026, 6, 3, 14), + recipeId: 33, + itemId: 700, + day: 5, + observedSecondShipId: 0, + observedFlagshipId: 101, + detailObserved: false, + }, + ], + }) + expect(report).toHaveBeenNthCalledWith(2, '/api/report/v3/item_improvement_recipe', { + schemaVersion: 1, + source: 'detail', + clientObservedAt: Date.UTC(2026, 6, 3, 15), + recipeId: 33, + itemId: 700, + itemLevel: 0, + stage: 0, + day: 6, + observedSecondShipId: 0, + observedFlagshipId: 101, + fuel: 10, + ammo: 20, + steel: 30, + bauxite: 40, + buildkit: 3, + remodelkit: 4, + certainBuildkit: 5, + certainRemodelkit: 6, + reqSlotItems: [{ id: 90, count: 3 }], + reqUseItems: [ + { id: 65, count: 1 }, + { id: 66, count: 2 }, + ], + changeFlag: 0, + detailObserved: true, + }) + expect(report).toHaveBeenCalledTimes(2) + }) + + it('reports successful remodel recipes with cached recipe cost and day data', () => { + window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } + const reporter = new RemodelRecipeReporter() + const report = attachReportSpy(reporter) + + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle('POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', detailBody, { + api_id: '33', + api_slot_id: 501, + }) reporter.handle( 'POST', '/kcsapi/api_req_kousyou/remodel_slot', @@ -57,7 +135,7 @@ describe('RemodelRecipeReporter', () => { recipeId: 33, itemId: 700, stage: 1, - day: 3, + day: 6, secretary: 99, fuel: 10, ammo: 20, @@ -71,25 +149,34 @@ describe('RemodelRecipeReporter', () => { certainRemodelkit: 6, upgradeToItemId: 701, upgradeToItemLevel: 0, - key: 'r33-i700-s1-d3-s99', + key: 'r33-i700-s1-d6-s99', + }) + expect(report).toHaveBeenCalledWith('/api/report/v3/item_improvement_recipe', { + schemaVersion: 1, + source: 'execution', + clientObservedAt: expect.any(Number), + recipeId: 33, + itemId: 700, + itemLevel: 6, + day: 6, + observedSecondShipId: 102, + observedFlagshipId: 101, + upgradeObserved: true, + upgradeToItemId: 701, + upgradeToItemLevel: 0, }) }) - it('does not report failed remodel recipes or unknown item stages', () => { - window._slotitems[501] = { api_slotitem_id: 700, api_level: -1 } + it('does not report v3 update facts for failed or non-converting improvements', () => { + window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } const reporter = new RemodelRecipeReporter() const report = attachReportSpy(reporter) - reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', [{ api_id: 33 }]) - reporter.handle( - 'POST', - '/kcsapi/api_req_kousyou/remodel_slotlist_detail', - {}, - { - api_id: '33', - api_slot_id: 501, - }, - ) + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle('POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', detailBody, { + api_id: '33', + api_slot_id: 501, + }) reporter.handle( 'POST', '/kcsapi/api_req_kousyou/remodel_slot', @@ -113,7 +200,10 @@ describe('RemodelRecipeReporter', () => { }, ) - expect(report).not.toHaveBeenCalled() + expect(report).not.toHaveBeenCalledWith( + '/api/report/v3/item_improvement_recipe', + expect.objectContaining({ source: 'execution' }), + ) }) it('does not cache detail state before recipe list or when upgrade responses mismatch', () => { @@ -142,7 +232,93 @@ describe('RemodelRecipeReporter', () => { api_id: '33', }, ) - reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', [{ api_id: 33 }]) + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle('POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', detailBody, { + api_id: '33', + api_slot_id: 501, + }) + reporter.handle( + 'POST', + '/kcsapi/api_req_kousyou/remodel_slot', + { + api_remodel_flag: true, + api_remodel_id: [999, 701], + }, + { + api_id: '33', + }, + ) + reporter.handle( + 'POST', + '/kcsapi/api_req_kousyou/remodel_slot', + { + api_remodel_flag: true, + api_remodel_id: [700, 701], + }, + { + api_id: '34', + }, + ) + + expect(report).not.toHaveBeenCalledWith('/api/report/v2/remodel_recipe', expect.anything()) + expect(report).not.toHaveBeenCalledWith( + '/api/report/v3/item_improvement_recipe', + expect.objectContaining({ source: 'execution' }), + ) + expect(consoleError).toHaveBeenCalledTimes(1) + consoleError.mockRestore() + }) + + it('skips v3 reporting when fleet context is unknown', () => { + window._decks = [] + const reporter = new RemodelRecipeReporter() + const report = attachReportSpy(reporter) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + + expect(report).not.toHaveBeenCalled() + expect(consoleError).toHaveBeenCalledWith('Invalid remodel recipe fleet context') + consoleError.mockRestore() + }) + + it('rejects malformed required item pairs', () => { + window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } + const reporter = new RemodelRecipeReporter() + const report = attachReportSpy(reporter) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle( + 'POST', + '/kcsapi/api_req_kousyou/remodel_slotlist_detail', + { + ...detailBody, + api_req_useitem_id: 65, + api_req_useitem_num: 0, + }, + { + api_id: '33', + api_slot_id: 501, + }, + ) + + expect(report).toHaveBeenCalledTimes(1) + expect(consoleError).toHaveBeenCalledWith('Invalid required item pair: 65/0') + consoleError.mockRestore() + }) + + it('rejects detail observations without exact costs or slot item state', () => { + const reporter = new RemodelRecipeReporter() + const report = attachReportSpy(reporter) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle('POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', detailBody, { + api_id: '33', + api_slot_id: 501, + }) + window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } reporter.handle( 'POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', @@ -152,31 +328,122 @@ describe('RemodelRecipeReporter', () => { api_slot_id: 501, }, ) + + expect(report).toHaveBeenCalledTimes(1) + expect(consoleError).toHaveBeenCalledWith('Invalid remodel recipe slot item data') + expect(consoleError).toHaveBeenCalledWith('Invalid remodel recipe detail data') + consoleError.mockRestore() + }) + + it('rejects execution when the request slot mismatches cached detail state', () => { + window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } + const reporter = new RemodelRecipeReporter() + const report = attachReportSpy(reporter) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle('POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', detailBody, { + api_id: '33', + api_slot_id: 501, + }) reporter.handle( 'POST', '/kcsapi/api_req_kousyou/remodel_slot', { api_remodel_flag: true, - api_remodel_id: [999, 701], + api_remodel_id: [700, 701], + api_after_slot: { api_level: 0 }, + api_voice_ship_id: 99, }, { api_id: '33', + api_slot_id: 999, }, ) + + expect(report).not.toHaveBeenCalledWith('/api/report/v2/remodel_recipe', expect.anything()) + expect(consoleError).toHaveBeenCalledWith('Inconsistent remodel item data: 501, 999') + consoleError.mockRestore() + }) + + it('preserves v2 execution reporting when v3 fleet context is unavailable', () => { + window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } + window._decks = [] + const reporter = new RemodelRecipeReporter() + const report = attachReportSpy(reporter) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle('POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', detailBody, { + api_id: '33', + api_slot_id: 501, + }) reporter.handle( 'POST', '/kcsapi/api_req_kousyou/remodel_slot', { api_remodel_flag: true, api_remodel_id: [700, 701], + api_after_slot: { api_level: 0 }, + api_voice_ship_id: 99, }, { - api_id: '34', + api_id: '33', }, ) - expect(report).not.toHaveBeenCalled() - expect(consoleError).toHaveBeenCalledTimes(2) + expect(report).toHaveBeenCalledTimes(1) + expect(report).toHaveBeenCalledWith( + '/api/report/v2/remodel_recipe', + expect.objectContaining({ + recipeId: 33, + itemId: 700, + upgradeToItemId: 701, + }), + ) + expect(consoleError).toHaveBeenCalledWith('Invalid remodel recipe fleet context') consoleError.mockRestore() }) + + it('uses api_after_slot item id for conversion targets when remodel id is unchanged', () => { + window._slotitems[501] = { api_slotitem_id: 700, api_level: 10 } + const reporter = new RemodelRecipeReporter() + const report = attachReportSpy(reporter) + + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle( + 'POST', + '/kcsapi/api_req_kousyou/remodel_slotlist_detail', + { + ...detailBody, + api_change_flag: 1, + }, + { + api_id: '33', + api_slot_id: 501, + }, + ) + reporter.handle( + 'POST', + '/kcsapi/api_req_kousyou/remodel_slot', + { + api_remodel_flag: true, + api_remodel_id: [700, 700], + api_after_slot: { api_slotitem_id: 701, api_level: 0 }, + api_voice_ship_id: 99, + }, + { + api_id: '33', + }, + ) + + expect(report).toHaveBeenCalledWith( + '/api/report/v3/item_improvement_recipe', + expect.objectContaining({ + source: 'execution', + upgradeToItemId: 701, + upgradeToItemLevel: 0, + }), + ) + }) }) From 20a1d75d0e54393e71fc5169db801213861d24d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 13:40:11 +0800 Subject: [PATCH 02/26] feat: add opt-in remodel debug recorder Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 18 ++ src/index.ts | 6 + src/remodel-debug-recorder.ts | 328 +++++++++++++++++++++++++++ tests/remodel-debug-recorder.test.ts | 106 +++++++++ 4 files changed, 458 insertions(+) create mode 100644 src/remodel-debug-recorder.ts create mode 100644 tests/remodel-debug-recorder.test.ts diff --git a/README.md b/README.md index d738d29..35a6811 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,21 @@ Report ship creating info and drop info, and so on. - `pnpm run lint` checks JavaScript and TypeScript sources. - `pnpm run typecheck` runs TypeScript in strict mode. - `pnpm test` builds the plugin and runs the Vitest suite. + +## Remodel debug recorder + +For local remodel recipe validation, the plugin includes an opt-in in-memory recorder. It is off by default and only records the three Akashi remodel APIs. + +Enable it from the Poi devtools console, then reload the plugin: + +```js +localStorage.setItem('poi-plugin-report:remodel-debug-recorder', '1') +``` + +When enabled, a small local control appears with `Export` and `Clear`. Captures stay in memory and are not sent anywhere; allowlisted, sanitized data is written only when `Export` is clicked. + +Disable it with: + +```js +localStorage.removeItem('poi-plugin-report:remodel-debug-recorder') +``` diff --git a/src/index.ts b/src/index.ts index ebd9223..cde1af6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import semver from 'semver' import { init } from './sentry' import type { GameResponseEvent } from './types/game-api' import type { Reporter } from './types/reporter' +import { recordRemodelDebugEvent, startRemodelDebugRecorder } from './remodel-debug-recorder' import * as remote from '@electron/remote' @@ -39,11 +40,13 @@ import { } from './reporters' let reporters: Reporter[] = [] +let stopRemodelDebugRecorder: (() => void) | undefined const handleResponse = (e: GameResponseEvent) => { if (!(gameAPIBroadcaster.serverInfo.num >= 1)) { return } const { method, path, body, postBody, time = Date.now() } = e.detail + recordRemodelDebugEvent({ method, path, body, postBody, time }) for (const reporter of reporters) { try { reporter.handle(method, path, body, postBody, time) @@ -59,6 +62,7 @@ const handleResponse = (e: GameResponseEvent) => { export const show = false export const pluginDidLoad = (_e: unknown) => { + stopRemodelDebugRecorder = startRemodelDebugRecorder() reporters = [ new QuestReporter(), new CreateShipReporter(), @@ -74,4 +78,6 @@ export const pluginDidLoad = (_e: unknown) => { } export const pluginWillUnload = (_e: unknown) => { window.removeEventListener('game.response', handleResponse) + stopRemodelDebugRecorder?.() + stopRemodelDebugRecorder = undefined } diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts new file mode 100644 index 0000000..3586054 --- /dev/null +++ b/src/remodel-debug-recorder.ts @@ -0,0 +1,328 @@ +import type { GameResponseEventDetail } from './types/game-api' + +const ENABLED_KEY = 'poi-plugin-report:remodel-debug-recorder' +const CONTAINER_ID = 'poi-plugin-report-remodel-debug-recorder' + +const REMODEL_PATHS = new Set([ + '/kcsapi/api_req_kousyou/remodel_slotlist', + '/kcsapi/api_req_kousyou/remodel_slotlist_detail', + '/kcsapi/api_req_kousyou/remodel_slot', +]) + +interface SanitizedFleetContext { + firstFleet?: { + flagship?: { + api_ship_id?: number + } + secondShip?: { + api_ship_id?: number + } + } + selectedSlotItem?: { + api_slotitem_id?: number + api_level?: number + } +} + +export interface RemodelDebugRecord { + time: number + method: string + path: string + postBody: unknown + body: unknown + context: SanitizedFleetContext +} + +const records: RemodelDebugRecord[] = [] +let updateCounter: (() => void) | undefined + +const cloneJson = (value: unknown): unknown => { + if (value == null) { + return value + } + return JSON.parse(JSON.stringify(value)) as unknown +} + +export const isRemodelDebugRecorderEnabled = (): boolean => { + try { + return window.localStorage?.getItem(ENABLED_KEY) === '1' + } catch { + return false + } +} + +const getPostBodySlotId = (postBody: unknown): string | number | undefined => { + if (postBody == null || typeof postBody !== 'object') { + return undefined + } + const slotId = (postBody as Record).api_slot_id + return typeof slotId === 'string' || typeof slotId === 'number' ? slotId : undefined +} + +const pickNumber = (record: Record, key: string): number | undefined => { + const value = record[key] + return typeof value === 'number' ? value : undefined +} + +const pickStringOrNumber = ( + record: Record, + key: string, +): string | number | undefined => { + const value = record[key] + return typeof value === 'string' || typeof value === 'number' ? value : undefined +} + +const sanitizePostBody = (path: string, postBody: unknown): unknown => { + if (postBody == null || typeof postBody !== 'object') { + return {} + } + const record = postBody as Record + + switch (path) { + case '/kcsapi/api_req_kousyou/remodel_slotlist_detail': + return { + api_id: pickStringOrNumber(record, 'api_id'), + } + case '/kcsapi/api_req_kousyou/remodel_slot': + return { + api_id: pickStringOrNumber(record, 'api_id'), + api_certain_flag: pickStringOrNumber(record, 'api_certain_flag'), + } + default: + return {} + } +} + +const sanitizeListBody = (body: unknown): unknown => { + if (!Array.isArray(body)) { + return [] + } + + return body.map((entry: unknown) => { + const record = + entry != null && typeof entry === 'object' ? (entry as Record) : {} + return { + api_id: pickNumber(record, 'api_id'), + api_slot_id: pickNumber(record, 'api_slot_id'), + api_req_fuel: pickNumber(record, 'api_req_fuel'), + api_req_bull: pickNumber(record, 'api_req_bull'), + api_req_steel: pickNumber(record, 'api_req_steel'), + api_req_bauxite: pickNumber(record, 'api_req_bauxite'), + } + }) +} + +const sanitizeDetailBody = (body: unknown): unknown => { + const record = body != null && typeof body === 'object' ? (body as Record) : {} + + return { + api_req_buildkit: pickNumber(record, 'api_req_buildkit'), + api_req_remodelkit: pickNumber(record, 'api_req_remodelkit'), + api_certain_buildkit: pickNumber(record, 'api_certain_buildkit'), + api_certain_remodelkit: pickNumber(record, 'api_certain_remodelkit'), + api_req_slot_id: pickNumber(record, 'api_req_slot_id'), + api_req_slot_num: pickNumber(record, 'api_req_slot_num'), + api_req_slot_id2: pickNumber(record, 'api_req_slot_id2'), + api_req_slot_num2: pickNumber(record, 'api_req_slot_num2'), + api_req_useitem_id: pickNumber(record, 'api_req_useitem_id'), + api_req_useitem_num: pickNumber(record, 'api_req_useitem_num'), + api_req_useitem_id2: pickNumber(record, 'api_req_useitem_id2'), + api_req_useitem_num2: pickNumber(record, 'api_req_useitem_num2'), + api_change_flag: pickNumber(record, 'api_change_flag'), + } +} + +const sanitizeSlotBody = (body: unknown): unknown => { + const record = body != null && typeof body === 'object' ? (body as Record) : {} + const afterSlot = + record.api_after_slot != null && typeof record.api_after_slot === 'object' + ? (record.api_after_slot as Record) + : {} + + return { + api_remodel_flag: pickNumber(record, 'api_remodel_flag'), + api_remodel_id: Array.isArray(record.api_remodel_id) + ? record.api_remodel_id.filter((value) => typeof value === 'number') + : undefined, + api_after_slot: { + api_slotitem_id: pickNumber(afterSlot, 'api_slotitem_id'), + api_level: pickNumber(afterSlot, 'api_level'), + api_alv: pickNumber(afterSlot, 'api_alv'), + }, + api_voice_ship_id: pickNumber(record, 'api_voice_ship_id'), + } +} + +const sanitizeBody = (path: string, body: unknown): unknown => { + switch (path) { + case '/kcsapi/api_req_kousyou/remodel_slotlist': + return sanitizeListBody(body) + case '/kcsapi/api_req_kousyou/remodel_slotlist_detail': + return sanitizeDetailBody(body) + case '/kcsapi/api_req_kousyou/remodel_slot': + return sanitizeSlotBody(body) + default: + return {} + } +} + +const createSanitizedContext = (slotId: string | number | undefined): SanitizedFleetContext => { + const deckShipIds = window._decks[0]?.api_ship.slice(0, 2) || [] + const flagship = deckShipIds[0] == null ? undefined : window._ships[deckShipIds[0]] + const secondShip = deckShipIds[1] == null ? undefined : window._ships[deckShipIds[1]] + if (slotId != null) { + const slotitem = window._slotitems[slotId] + if (slotitem) { + return { + firstFleet: { + flagship: flagship ? { api_ship_id: flagship.api_ship_id } : undefined, + secondShip: secondShip ? { api_ship_id: secondShip.api_ship_id } : undefined, + }, + selectedSlotItem: { + api_slotitem_id: slotitem.api_slotitem_id, + api_level: slotitem.api_level, + }, + } + } + } + + return { + firstFleet: { + flagship: flagship ? { api_ship_id: flagship.api_ship_id } : undefined, + secondShip: secondShip ? { api_ship_id: secondShip.api_ship_id } : undefined, + }, + } +} + +const stripUndefined = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map(stripUndefined) + } + if (value != null && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value) + .filter(([, entry]) => entry !== undefined) + .map(([key, entry]) => [key, stripUndefined(entry)]), + ) + } + return value +} + +const createSanitizedRecordBody = (path: string, body: unknown): unknown => + stripUndefined(sanitizeBody(path, body)) + +const createSanitizedRecordPostBody = (path: string, postBody: unknown): unknown => + stripUndefined(sanitizePostBody(path, postBody)) + +const createSanitizedRecordContext = (slotId: string | number | undefined): SanitizedFleetContext => + stripUndefined(createSanitizedContext(slotId)) as SanitizedFleetContext + +export const createRemodelDebugRecord = ( + event: GameResponseEventDetail, +): RemodelDebugRecord | undefined => { + if (!REMODEL_PATHS.has(event.path)) { + return undefined + } + + return { + time: event.time, + method: event.method, + path: event.path, + postBody: cloneJson(createSanitizedRecordPostBody(event.path, event.postBody)), + body: cloneJson(createSanitizedRecordBody(event.path, event.body)), + context: createSanitizedRecordContext(getPostBodySlotId(event.postBody)), + } +} + +export const recordRemodelDebugEvent = (event: GameResponseEventDetail): void => { + if (!isRemodelDebugRecorderEnabled()) { + return + } + + try { + const record = createRemodelDebugRecord(event) + if (!record) { + return + } + + records.push(record) + updateCounter?.() + } catch (err) { + console.error(err) + } +} + +export const clearRemodelDebugRecords = (): void => { + records.length = 0 + updateCounter?.() +} + +export const getRemodelDebugRecords = (): readonly RemodelDebugRecord[] => records + +const exportRecords = (): void => { + const blob = new Blob([JSON.stringify({ records }, null, 2)], { + type: 'application/json', + }) + const href = URL.createObjectURL(blob) + const anchor = document.createElement('a') + anchor.href = href + anchor.download = `plugin-report-remodel-debug-${new Date() + .toISOString() + .replace(/[:.]/g, '-')}.json` + anchor.click() + URL.revokeObjectURL(href) +} + +export const startRemodelDebugRecorder = (): (() => void) => { + if (!isRemodelDebugRecorderEnabled() || typeof document === 'undefined') { + return () => {} + } + + const existing = document.getElementById(CONTAINER_ID) + if (existing) { + existing.remove() + } + + const container = document.createElement('div') + container.id = CONTAINER_ID + container.style.cssText = [ + 'position:fixed', + 'right:12px', + 'bottom:12px', + 'z-index:2147483647', + 'display:flex', + 'gap:6px', + 'padding:6px', + 'background:rgba(0,0,0,.75)', + 'color:#fff', + 'font:12px sans-serif', + 'border-radius:4px', + ].join(';') + + const count = document.createElement('span') + const exportButton = document.createElement('button') + const clearButton = document.createElement('button') + + updateCounter = () => { + count.textContent = `Remodel debug: ${records.length}` + } + updateCounter() + + exportButton.type = 'button' + exportButton.textContent = 'Export' + exportButton.addEventListener('click', exportRecords) + + clearButton.type = 'button' + clearButton.textContent = 'Clear' + clearButton.addEventListener('click', clearRemodelDebugRecords) + + container.append(count, exportButton, clearButton) + document.body.append(container) + + return () => { + exportButton.removeEventListener('click', exportRecords) + clearButton.removeEventListener('click', clearRemodelDebugRecords) + container.remove() + updateCounter = undefined + } +} diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts new file mode 100644 index 0000000..8a94428 --- /dev/null +++ b/tests/remodel-debug-recorder.test.ts @@ -0,0 +1,106 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import { + clearRemodelDebugRecords, + createRemodelDebugRecord, + getRemodelDebugRecords, + recordRemodelDebugEvent, +} from '../src/remodel-debug-recorder' +import type { GameResponseEventDetail } from '../src/types/game-api' + +const storage = new Map() + +const createWindow = () => + ({ + localStorage: { + getItem(key: string) { + return storage.get(key) ?? null + }, + setItem(key: string, value: string) { + storage.set(key, value) + }, + removeItem(key: string) { + storage.delete(key) + }, + }, + _decks: [{ api_ship: [1, 2, 3] }], + _ships: { + 1: { api_ship_id: 101, api_lv: 80 }, + 2: { api_ship_id: 102, api_lv: 70 }, + 3: { api_ship_id: 103, api_lv: 60 }, + }, + _slotitems: { + 501: { api_slotitem_id: 700, api_level: 6, api_locked: 1 }, + }, + }) as unknown as Window & typeof globalThis + +const remodelDetailEvent: GameResponseEventDetail = { + time: 1710000000000, + method: 'POST', + path: '/kcsapi/api_req_kousyou/remodel_slotlist_detail', + postBody: { + api_id: '33', + api_slot_id: '501', + api_token: 'secret-token', + }, + body: { + api_req_buildkit: 3, + api_token: 'secret-token', + }, +} + +beforeEach(() => { + storage.clear() + globalThis.window = createWindow() + clearRemodelDebugRecords() +}) + +describe('remodel debug recorder', () => { + it('does not record unless explicitly enabled', () => { + recordRemodelDebugEvent(remodelDetailEvent) + + expect(getRemodelDebugRecords()).toHaveLength(0) + }) + + it('records only remodel APIs when enabled', () => { + window.localStorage.setItem('poi-plugin-report:remodel-debug-recorder', '1') + + recordRemodelDebugEvent({ + ...remodelDetailEvent, + path: '/kcsapi/api_get_member/ship2', + }) + recordRemodelDebugEvent(remodelDetailEvent) + + expect(getRemodelDebugRecords()).toHaveLength(1) + }) + + it('sanitizes fleet and slot context for captured remodel records', () => { + const record = createRemodelDebugRecord(remodelDetailEvent) + + expect(record).toEqual({ + time: 1710000000000, + method: 'POST', + path: '/kcsapi/api_req_kousyou/remodel_slotlist_detail', + postBody: { + api_id: '33', + }, + body: { + api_req_buildkit: 3, + }, + context: { + firstFleet: { + flagship: { + api_ship_id: 101, + }, + secondShip: { + api_ship_id: 102, + }, + }, + selectedSlotItem: { + api_slotitem_id: 700, + api_level: 6, + }, + }, + }) + }) +}) From 9befa8c8980d9f7e532679f2f2f6c5d850c67704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 14:05:33 +0800 Subject: [PATCH 03/26] feat: expose remodel recorder settings UI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 14 +--- scripts/smoke-load.cjs | 28 ++++++++ src/index.ts | 8 +-- src/remodel-debug-recorder.ts | 69 +++---------------- src/remodel-debug-settings.ts | 120 ++++++++++++++++++++++++++++++++++ src/types/vendor.d.ts | 27 ++++++++ tsdown.config.ts | 1 + 7 files changed, 191 insertions(+), 76 deletions(-) create mode 100644 src/remodel-debug-settings.ts diff --git a/README.md b/README.md index 35a6811..8e212ab 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,6 @@ Report ship creating info and drop info, and so on. For local remodel recipe validation, the plugin includes an opt-in in-memory recorder. It is off by default and only records the three Akashi remodel APIs. -Enable it from the Poi devtools console, then reload the plugin: +Enable it from the plugin settings panel. The settings panel also shows the in-memory record count and provides `Export` and `Clear` actions. -```js -localStorage.setItem('poi-plugin-report:remodel-debug-recorder', '1') -``` - -When enabled, a small local control appears with `Export` and `Clear`. Captures stay in memory and are not sent anywhere; allowlisted, sanitized data is written only when `Export` is clicked. - -Disable it with: - -```js -localStorage.removeItem('poi-plugin-report:remodel-debug-recorder') -``` +Captures stay in memory and are not sent anywhere; allowlisted, sanitized data is written only when `Export` is clicked. Disable the setting after collecting the local validation data. diff --git a/scripts/smoke-load.cjs b/scripts/smoke-load.cjs index a1fb38b..798a95d 100644 --- a/scripts/smoke-load.cjs +++ b/scripts/smoke-load.cjs @@ -75,6 +75,31 @@ const sentryStub = { }, } +class ReactComponent { + constructor(props) { + this.props = props + this.state = {} + } + + setState(state) { + this.state = { + ...this.state, + ...state, + } + } +} + +const reactStub = { + Component: ReactComponent, + createElement(type, props, ...children) { + return { + type, + props, + children, + } + }, +} + const lodashImplementations = { compact(values) { return values.filter(Boolean) @@ -203,6 +228,8 @@ Module._load = function smokeLoad(request, parent, isMain) { return sentryStub case 'node-fetch': return fetchStub + case 'react': + return reactStub case 'lodash': return lodashStub case 'moment-timezone': @@ -229,6 +256,7 @@ async function main() { const plugin = require(entryPath) assert.strictEqual(plugin.show, false) + assert.strictEqual(typeof plugin.settingClass, 'function') assert.strictEqual(typeof plugin.pluginDidLoad, 'function') assert.strictEqual(typeof plugin.pluginWillUnload, 'function') diff --git a/src/index.ts b/src/index.ts index cde1af6..589384a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,8 @@ import semver from 'semver' import { init } from './sentry' import type { GameResponseEvent } from './types/game-api' import type { Reporter } from './types/reporter' -import { recordRemodelDebugEvent, startRemodelDebugRecorder } from './remodel-debug-recorder' +import { recordRemodelDebugEvent } from './remodel-debug-recorder' +import RemodelDebugSettings from './remodel-debug-settings' import * as remote from '@electron/remote' @@ -40,7 +41,6 @@ import { } from './reporters' let reporters: Reporter[] = [] -let stopRemodelDebugRecorder: (() => void) | undefined const handleResponse = (e: GameResponseEvent) => { if (!(gameAPIBroadcaster.serverInfo.num >= 1)) { return @@ -61,8 +61,8 @@ const handleResponse = (e: GameResponseEvent) => { } export const show = false +export const settingClass = RemodelDebugSettings export const pluginDidLoad = (_e: unknown) => { - stopRemodelDebugRecorder = startRemodelDebugRecorder() reporters = [ new QuestReporter(), new CreateShipReporter(), @@ -78,6 +78,4 @@ export const pluginDidLoad = (_e: unknown) => { } export const pluginWillUnload = (_e: unknown) => { window.removeEventListener('game.response', handleResponse) - stopRemodelDebugRecorder?.() - stopRemodelDebugRecorder = undefined } diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index 3586054..4561aef 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -1,7 +1,6 @@ import type { GameResponseEventDetail } from './types/game-api' const ENABLED_KEY = 'poi-plugin-report:remodel-debug-recorder' -const CONTAINER_ID = 'poi-plugin-report-remodel-debug-recorder' const REMODEL_PATHS = new Set([ '/kcsapi/api_req_kousyou/remodel_slotlist', @@ -34,7 +33,6 @@ export interface RemodelDebugRecord { } const records: RemodelDebugRecord[] = [] -let updateCounter: (() => void) | undefined const cloneJson = (value: unknown): unknown => { if (value == null) { @@ -51,6 +49,15 @@ export const isRemodelDebugRecorderEnabled = (): boolean => { } } +export const setRemodelDebugRecorderEnabled = (enabled: boolean): void => { + if (enabled) { + window.localStorage?.setItem(ENABLED_KEY, '1') + return + } + + window.localStorage?.removeItem(ENABLED_KEY) +} + const getPostBodySlotId = (postBody: unknown): string | number | undefined => { if (postBody == null || typeof postBody !== 'object') { return undefined @@ -246,7 +253,6 @@ export const recordRemodelDebugEvent = (event: GameResponseEventDetail): void => } records.push(record) - updateCounter?.() } catch (err) { console.error(err) } @@ -254,12 +260,11 @@ export const recordRemodelDebugEvent = (event: GameResponseEventDetail): void => export const clearRemodelDebugRecords = (): void => { records.length = 0 - updateCounter?.() } export const getRemodelDebugRecords = (): readonly RemodelDebugRecord[] => records -const exportRecords = (): void => { +export const exportRemodelDebugRecords = (): void => { const blob = new Blob([JSON.stringify({ records }, null, 2)], { type: 'application/json', }) @@ -272,57 +277,3 @@ const exportRecords = (): void => { anchor.click() URL.revokeObjectURL(href) } - -export const startRemodelDebugRecorder = (): (() => void) => { - if (!isRemodelDebugRecorderEnabled() || typeof document === 'undefined') { - return () => {} - } - - const existing = document.getElementById(CONTAINER_ID) - if (existing) { - existing.remove() - } - - const container = document.createElement('div') - container.id = CONTAINER_ID - container.style.cssText = [ - 'position:fixed', - 'right:12px', - 'bottom:12px', - 'z-index:2147483647', - 'display:flex', - 'gap:6px', - 'padding:6px', - 'background:rgba(0,0,0,.75)', - 'color:#fff', - 'font:12px sans-serif', - 'border-radius:4px', - ].join(';') - - const count = document.createElement('span') - const exportButton = document.createElement('button') - const clearButton = document.createElement('button') - - updateCounter = () => { - count.textContent = `Remodel debug: ${records.length}` - } - updateCounter() - - exportButton.type = 'button' - exportButton.textContent = 'Export' - exportButton.addEventListener('click', exportRecords) - - clearButton.type = 'button' - clearButton.textContent = 'Clear' - clearButton.addEventListener('click', clearRemodelDebugRecords) - - container.append(count, exportButton, clearButton) - document.body.append(container) - - return () => { - exportButton.removeEventListener('click', exportRecords) - clearButton.removeEventListener('click', clearRemodelDebugRecords) - container.remove() - updateCounter = undefined - } -} diff --git a/src/remodel-debug-settings.ts b/src/remodel-debug-settings.ts new file mode 100644 index 0000000..804897b --- /dev/null +++ b/src/remodel-debug-settings.ts @@ -0,0 +1,120 @@ +import React from 'react' +import { + clearRemodelDebugRecords, + exportRemodelDebugRecords, + getRemodelDebugRecords, + isRemodelDebugRecorderEnabled, + setRemodelDebugRecorderEnabled, +} from './remodel-debug-recorder' + +interface RemodelDebugSettingsState { + enabled: boolean + count: number +} + +const createStyle = (style: Record) => style + +export default class RemodelDebugSettings extends React.Component< + Record, + RemodelDebugSettingsState +> { + refreshTimer: ReturnType | undefined + + constructor(props: Record) { + super(props) + this.state = { + enabled: isRemodelDebugRecorderEnabled(), + count: getRemodelDebugRecords().length, + } + } + + componentDidMount() { + this.refreshTimer = setInterval(() => { + this.setState({ + enabled: isRemodelDebugRecorderEnabled(), + count: getRemodelDebugRecords().length, + }) + }, 1000) + } + + componentWillUnmount() { + if (this.refreshTimer) { + clearInterval(this.refreshTimer) + } + } + + toggleRecorder = () => { + const enabled = !isRemodelDebugRecorderEnabled() + setRemodelDebugRecorderEnabled(enabled) + this.setState({ enabled }) + } + + clearRecords = () => { + clearRemodelDebugRecords() + this.setState({ count: 0 }) + } + + exportRecords = () => { + exportRemodelDebugRecords() + } + + render() { + const { enabled, count } = this.state + + return React.createElement( + 'div', + { + style: createStyle({ + display: 'grid', + gap: 8, + lineHeight: 1.5, + maxWidth: 640, + }), + }, + React.createElement('h4', null, 'Remodel recipe debug recorder'), + React.createElement( + 'p', + null, + 'Opt-in local recorder for validating Akashi remodel API sequences. It captures only allowlisted remodel fields, keeps records in memory, and writes a file only when Export is clicked.', + ), + React.createElement( + 'label', + null, + React.createElement('input', { + checked: enabled, + onChange: this.toggleRecorder, + type: 'checkbox', + }), + ' Enable remodel debug recorder', + ), + React.createElement('p', null, `Captured records: ${count}`), + React.createElement( + 'div', + { + style: createStyle({ + display: 'flex', + gap: 8, + }), + }, + React.createElement( + 'button', + { + disabled: count === 0, + onClick: this.exportRecords, + type: 'button', + }, + 'Export', + ), + React.createElement( + 'button', + { + disabled: count === 0, + onClick: this.clearRecords, + type: 'button', + }, + 'Clear', + ), + ), + ) + } +} diff --git a/src/types/vendor.d.ts b/src/types/vendor.d.ts index 1aa5c38..dd0ec27 100644 --- a/src/types/vendor.d.ts +++ b/src/types/vendor.d.ts @@ -38,6 +38,33 @@ declare module 'moment-timezone' { export default moment } +declare module 'react' { + export type ReactNode = unknown + + export class Component

, S = Record> { + constructor(props: P) + props: Readonly

+ state: Readonly + setState(state: Partial): void + componentDidMount?(): void + componentWillUnmount?(): void + render(): ReactNode + } + + export function createElement( + type: unknown, + props?: Record | null, + ...children: unknown[] + ): ReactNode + + const React: { + Component: typeof Component + createElement: typeof createElement + } + + export default React +} + declare module 'views/utils/selectors' { export function shipDataSelectorFactory(shipId: unknown): (state: unknown) => unknown export function shipEquipDataSelectorFactory(shipId: unknown): (state: unknown) => unknown diff --git a/tsdown.config.ts b/tsdown.config.ts index 31dd629..f2dd7a3 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -29,6 +29,7 @@ module.exports = defineConfig({ 'lodash', 'moment-timezone', 'node-fetch', + 'react', 'semver', /^views\//, ], From af343c189420bf6a975e4ab35124fbb2c42dee41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 14:07:34 +0800 Subject: [PATCH 04/26] fix: type recorder settings as react component Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package.json | 1 + pnpm-lock.yaml | 15 +++++++++++++++ src/remodel-debug-settings.ts | 8 ++++---- src/types/vendor.d.ts | 27 --------------------------- 4 files changed, 20 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index d4f60a4..5232994 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/lodash": "^4.17.21", "@types/node": "^24.0.0", "@types/node-fetch": "^2.6.13", + "@types/react": "^19.2.17", "@types/semver": "^7.7.1", "@vitest/coverage-v8": "^4.0.0", "eslint": "^10.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6063906..69ba7ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@types/node-fetch': specifier: ^2.6.13 version: 2.6.13 + '@types/react': + specifier: ^19.2.17 + version: 19.2.17 '@types/semver': specifier: ^7.7.1 version: 7.7.1 @@ -549,6 +552,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -1028,6 +1034,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + currently-unhandled@0.4.1: resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} engines: {node: '>=0.10.0'} @@ -3354,6 +3363,10 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + '@types/semver@7.7.1': {} '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0)(typescript@5.9.3)': @@ -3850,6 +3863,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.2.3: {} + currently-unhandled@0.4.1: dependencies: array-find-index: 1.0.2 diff --git a/src/remodel-debug-settings.ts b/src/remodel-debug-settings.ts index 804897b..6cc1e95 100644 --- a/src/remodel-debug-settings.ts +++ b/src/remodel-debug-settings.ts @@ -1,4 +1,4 @@ -import React from 'react' +import React, { Component, type CSSProperties, type ReactNode } from 'react' import { clearRemodelDebugRecords, exportRemodelDebugRecords, @@ -12,9 +12,9 @@ interface RemodelDebugSettingsState { count: number } -const createStyle = (style: Record) => style +const createStyle = (style: CSSProperties): CSSProperties => style -export default class RemodelDebugSettings extends React.Component< +export default class RemodelDebugSettings extends Component< Record, RemodelDebugSettingsState > { @@ -58,7 +58,7 @@ export default class RemodelDebugSettings extends React.Component< exportRemodelDebugRecords() } - render() { + render(): ReactNode { const { enabled, count } = this.state return React.createElement( diff --git a/src/types/vendor.d.ts b/src/types/vendor.d.ts index dd0ec27..1aa5c38 100644 --- a/src/types/vendor.d.ts +++ b/src/types/vendor.d.ts @@ -38,33 +38,6 @@ declare module 'moment-timezone' { export default moment } -declare module 'react' { - export type ReactNode = unknown - - export class Component

, S = Record> { - constructor(props: P) - props: Readonly

- state: Readonly - setState(state: Partial): void - componentDidMount?(): void - componentWillUnmount?(): void - render(): ReactNode - } - - export function createElement( - type: unknown, - props?: Record | null, - ...children: unknown[] - ): ReactNode - - const React: { - Component: typeof Component - createElement: typeof createElement - } - - export default React -} - declare module 'views/utils/selectors' { export function shipDataSelectorFactory(shipId: unknown): (state: unknown) => unknown export function shipEquipDataSelectorFactory(shipId: unknown): (state: unknown) => unknown From 4686cca269d2bd84c4abb4d03061e28a40decab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 14:25:37 +0800 Subject: [PATCH 05/26] test: add recorder storybook preview Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 1 + .storybook/main.ts | 11 + .storybook/preview.ts | 14 + README.md | 1 + eslint.config.mjs | 1 + package.json | 7 + pnpm-lock.yaml | 1798 +++++++++++++++++++++++- src/remodel-debug-recorder.ts | 32 +- src/remodel-debug-settings.stories.ts | 62 + src/reporters/remodel-recipe.ts | 8 +- tests/remodel-debug-recorder.test.ts | 74 +- tests/reporters/remodel-recipe.test.ts | 37 +- tsdown.config.ts | 2 +- 13 files changed, 1953 insertions(+), 95 deletions(-) create mode 100644 .storybook/main.ts create mode 100644 .storybook/preview.ts create mode 100644 src/remodel-debug-settings.stories.ts diff --git a/.gitignore b/.gitignore index 96c4569..de75306 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ chunks/*.js index.js reporters/*.js sentry.js +storybook-static diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..73d6f4b --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,11 @@ +import type { StorybookConfig } from '@storybook/react-vite' + +const config: StorybookConfig = { + stories: ['../src/**/*.stories.@(ts|tsx)'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, +} + +export default config diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..0795624 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,14 @@ +import type { Preview } from '@storybook/react-vite' + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +} + +export default preview diff --git a/README.md b/README.md index 8e212ab..30bfd90 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Report ship creating info and drop info, and so on. - `pnpm run lint` checks JavaScript and TypeScript sources. - `pnpm run typecheck` runs TypeScript in strict mode. +- `pnpm run storybook` previews React plugin UI surfaces. - `pnpm test` builds the plugin and runs the Vitest suite. ## Remodel debug recorder diff --git a/eslint.config.mjs b/eslint.config.mjs index 97c40c5..111508b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,6 +15,7 @@ export default tseslint.config( 'node_modules/**', 'reporters/**', 'sentry.js', + 'storybook-static/**', ], }, js.configs.recommended, diff --git a/package.json b/package.json index 5232994..ca10899 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "prepack": "tsdown", "postpublish": "git clean -fd -- index.js sentry.js reporters chunks && git checkout .", "smoke": "node scripts/smoke-load.cjs", + "storybook": "storybook dev -p 6006", + "storybook:build": "storybook build", "test": "tsdown && vitest run", "test:coverage": "tsdown && vitest run --coverage", "typecheck": "tsc --noEmit", @@ -51,10 +53,12 @@ "@commitlint/cli": "^8.2.0", "@commitlint/config-conventional": "^8.2.0", "@eslint/js": "^10.0.1", + "@storybook/react-vite": "^10.4.6", "@types/lodash": "^4.17.21", "@types/node": "^24.0.0", "@types/node-fetch": "^2.6.13", "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "@vitest/coverage-v8": "^4.0.0", "eslint": "^10.6.0", @@ -67,6 +71,9 @@ "kcsapi": "^1.260604.0", "lint-staged": "^9.4.2", "prettier": "^3.9.4", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "storybook": "^10.4.6", "tsdown": "^0.22.0", "typescript": "^5.9.0", "typescript-eslint": "^8.62.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69ba7ad..abd0abf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.6.0) + '@storybook/react-vite': + specifier: ^10.4.6 + version: 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7))(typescript@5.9.3)(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1)) '@types/lodash': specifier: ^4.17.21 version: 4.17.24 @@ -29,6 +32,9 @@ importers: '@types/react': specifier: ^19.2.17 version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) '@types/semver': specifier: ^7.7.1 version: 7.7.1 @@ -65,9 +71,18 @@ importers: prettier: specifier: ^3.9.4 version: 3.9.4 + react: + specifier: ^19.2.7 + version: 19.2.7 + react-dom: + specifier: ^19.2.7 + version: 19.2.7(react@19.2.7) + storybook: + specifier: ^10.4.6 + version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7) tsdown: specifier: ^0.22.0 - version: 0.22.3(typescript@5.9.3)(unrun@0.2.39(synckit@0.11.13)) + version: 0.22.3(oxc-resolver@11.23.0)(typescript@5.9.3)(unrun@0.2.39(synckit@0.11.13)) typescript: specifier: ^5.9.0 version: 5.9.3 @@ -76,18 +91,51 @@ importers: version: 8.62.1(eslint@10.6.0)(typescript@5.9.3) vitest: specifier: ^4.0.0 - version: 4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(vite@8.1.1(@types/node@24.13.2)) + version: 4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1)) packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + '@babel/generator@8.0.0': resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -104,6 +152,14 @@ packages: resolution: {integrity: sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==} engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} @@ -114,6 +170,18 @@ packages: engines: {node: ^22.18.0 || >=24.11.0} hasBin: true + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -192,18 +260,180 @@ packages: '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -263,9 +493,21 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': + resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} + peerDependencies: + typescript: '>= 4.3.x' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -297,12 +539,226 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-parser/binding-android-arm-eabi@0.127.0': + resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.127.0': + resolution: {integrity: sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.127.0': + resolution: {integrity: sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.127.0': + resolution: {integrity: sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.127.0': + resolution: {integrity: sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + resolution: {integrity: sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + resolution: {integrity: sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + resolution: {integrity: sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + resolution: {integrity: sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + resolution: {integrity: sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + resolution: {integrity: sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + resolution: {integrity: sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + resolution: {integrity: sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + resolution: {integrity: sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + resolution: {integrity: sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + resolution: {integrity: sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + resolution: {integrity: sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + resolution: {integrity: sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + resolution: {integrity: sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + resolution: {integrity: sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} '@oxc-project/types@0.137.0': resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + '@oxc-resolver/binding-android-arm-eabi@11.23.0': + resolution: {integrity: sha512-8IJyWRLVAyhTfe9/TIEbQqSQnl5rUqYJrUOS6Dkr+Mq9FGHMxDGeiEmwkBqCvDP5KckpPh/GYSgbag66O6JsCw==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.23.0': + resolution: {integrity: sha512-pprVojnNhHxupwTT2gdeUlkxll6XEvWWBk3oVicOSNVWQC99OBnDhMQDoirqnzrE1bScQSMS2JgPpqdlrhz/Fg==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.23.0': + resolution: {integrity: sha512-mbIrWIMAJeytyee36OyUP5XH92TP7FaKaQ2m5AjokKy7STgjrhRt7SMXqpqLjhGm6Xn721Xmsg6H3Rtd9YQETw==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.23.0': + resolution: {integrity: sha512-UnIphmZ1LazUCr9DXWaKYWtKDefPMbgLsywaoYxRqVCNHhq4MM6d2q1Nz1i9Vzxt5i+cE2nRUYpAUHr/lijNYA==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.23.0': + resolution: {integrity: sha512-aaZ/cSEYFkSxgS2hOrobT6RQcsWNviOX8dW6CEkVx2/UYkAf9MeHbjl3W0usWV53rVV//ndBdn2nb1y7jsu4lw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.23.0': + resolution: {integrity: sha512-IoJLvO5SjLSVMaq83BNTrPCb1FppvoJc1IhZ5CoUVl3PykUBku7D+LK1j0GSurhJcIc6zfjghsvaZNpq5ev6Mg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.23.0': + resolution: {integrity: sha512-vskFpwg44T/LFsfjSCnVZ5ygcuqzPC1yUzVEiKa8BgHAQz0+QLQQW3EGWLPVi8EXFghzjR4EtgPBtOhCjU4jdw==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.23.0': + resolution: {integrity: sha512-//TcHVhrChyw5RYtgts6WO7KcWq9387c1Z5Zvhqpk/ktAbyaRYgBZrpSY1GDCFq50ASt6B6jhh+JxB1rB45IAg==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.23.0': + resolution: {integrity: sha512-ZFqlwiTf7CXLLSGyAR9tYiO33LiaeIEXW+xm42d8mnUGpDgPltyrCGYtQezyMMEXvjhOgCz1X+i7sbDTJEx+bg==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.23.0': + resolution: {integrity: sha512-oZ5LeN5+H1R19dRjTAxKrxQguH+AsemHcnthEfFxf4OjmBSty2doHLeSmMunKy3zpTHJQ3lh3Af+dNS+W6dYeA==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.23.0': + resolution: {integrity: sha512-O4ciFDyX5ebQd0qkb1bjAIg8IEfiLT03GbSeylwlwlUMK9KwBWaALwrxSbc0Msaz4U6iPj+T9eRXpD5mxBfmvA==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.23.0': + resolution: {integrity: sha512-P3o8Y9kISYjcxadmbO+94ThRwLhwGuDAbA7dcdd4+YLpfeF+mmobz8fXf4NmSdfSqjyRSkceJDBRZha9NVYkiQ==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.23.0': + resolution: {integrity: sha512-oj03m1E3RmTFczKhcKJDzHaEDKJnPIsDcQFVxBJsSdXGSuIPdt5TvcM332FfMQgzI6yDJqyl4InrnFfXrmUTKQ==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.23.0': + resolution: {integrity: sha512-BqJxbSC8FdP7mSuSpRePTGHm0hXWV+dfz//f7SjsteZncLaBgWTBmi/OZNv7sX6CyG/Pt/eJkPorP+DkMOhMwQ==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.23.0': + resolution: {integrity: sha512-utmw+VmUrW4K8LI5/6jhg4aGYKJHOIjQ9syYOOA6pF3w7haKu4r4enTe2U0C04/HbUvkq/Zif43xFsKW1Pnq9w==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-openharmony-arm64@11.23.0': + resolution: {integrity: sha512-V6lbRrthHa4TbvsLjPtg+EkXT1tRY+s4I8rYLXUfiHlZzGx3sLv1EH9CEOOevjvUYHLsbe/gqCIc73XnQfPb9A==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.23.0': + resolution: {integrity: sha512-gRoOxQPdnAmIAjxcuQNBxfihvx+wjTaQM/9/eP12xwnGNawOG/+Zz9RHN4WNSxT45b5CrscK4NB8aPh+oZQXAQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.23.0': + resolution: {integrity: sha512-CgTGMYsJVe1eUiCdJTpGw21svXw79ITsemN1h0hcNkiswasDbN5MoibSLY+gRMWP5syfEz5iffrjZnwEP8xeUA==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.23.0': + resolution: {integrity: sha512-gUGJpr+Rn6zMxm5juApV0K3U845i8t47o8k+rbO0BHbi4PoJIfSPeQmrE2dgohQm2g5k6iviNFyXCGqvmaYUpw==} + cpu: [x64] + os: [win32] + '@pkgr/core@0.3.6': resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -494,6 +950,15 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@samverschueren/stream-to-observable@0.3.1': resolution: {integrity: sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==} engines: {node: '>=6'} @@ -509,15 +974,118 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@storybook/builder-vite@10.4.6': + resolution: {integrity: sha512-BHBtD81HiXUiDQz/CaFynLtWmm7AFUQn8VnXuHipZ8KlnUANopa4yqdVuy/Gwz8ub254uFI5NMZsW/KlgWNgNg==} + peerDependencies: + storybook: ^10.4.6 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/csf-plugin@10.4.6': + resolution: {integrity: sha512-NILLxDqpA/JR/AazGWpsz+4fadJwRU4uhHephGtYpVOWnQA/DkJfKT6zpcJVq8+QA8A2zKMLX3GVKsXIrxjuDA==} + peerDependencies: + esbuild: '*' + rollup: '*' + storybook: ^10.4.6 + vite: '*' + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@2.1.0': + resolution: {integrity: sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@storybook/react-dom-shim@10.4.6': + resolution: {integrity: sha512-iGNmKzrq9vgl2PDrYAnZKI+yvac3Ym+lJXXuQaqlFRS23zA5MNm4EBX+rAG7WulqchoK6NaZ0KQOs2mAgEpTMg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.6 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@storybook/react-vite@10.4.6': + resolution: {integrity: sha512-0arEQtybqGYXHbXpTot+Wv9YtG+V5Vp43QayXavPKQ20M8mpEzhyCPKd0EhqMGSC1Z1UEt0hm365WUBhI9LfKA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.6 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/react@10.4.6': + resolution: {integrity: sha512-9Y7YecrVFe1/01KYjfOLxVqTg2Aq+IO6TEv6sC2U0PfD0AWCSCmQ91QqgBpN/XW4aFFWoiZNinyXMUlU8zxy2w==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.4.6 + typescript: '>= 4.9.x' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + typescript: + optional: true + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -552,9 +1120,17 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -736,6 +1312,9 @@ packages: '@vitest/browser': optional: true + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.1.9': resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} @@ -750,6 +1329,9 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.1.9': resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} @@ -759,12 +1341,21 @@ packages: '@vitest/snapshot@4.1.9': resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.1.9': resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.1.9': resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@webcontainer/env@1.1.1': + resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -798,6 +1389,10 @@ packages: resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} engines: {node: '>=4'} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-styles@2.2.1: resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} engines: {node: '>=0.10.0'} @@ -806,6 +1401,10 @@ packages: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansis@4.3.1: resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} @@ -825,6 +1424,13 @@ packages: argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -860,6 +1466,10 @@ packages: resolution: {integrity: sha512-8OG92q3R35qjC/4i6BLBMg8IB+fClWu/1PEwg2Z9Rn+BuNaiEgJzpzn+pxWOdHJWDCAwu2JP0wCDTozAM4QirQ==} engines: {node: ^22.18.0 || >=24.11.0} + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + ast-v8-to-istanbul@1.0.4: resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} @@ -887,6 +1497,11 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.41: + resolution: {integrity: sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==} + engines: {node: '>=6.0.0'} + hasBin: true + birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} @@ -901,6 +1516,15 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} @@ -949,6 +1573,13 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + caniuse-lite@1.0.30001800: + resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -961,6 +1592,10 @@ packages: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -1034,6 +1669,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1088,13 +1726,29 @@ packages: dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -1110,6 +1764,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1118,6 +1776,16 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dot-prop@3.0.0: resolution: {integrity: sha512-k4ELWeEU3uCcwub7+dWydqQBRjAjkV9L33HjVRG5Xo2QybI6ja/v+4W73SRi8ubCqJz0l9XsTP1NbewfyqaSlw==} engines: {node: '>=0.10.0'} @@ -1135,6 +1803,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + electron-to-chromium@1.5.387: + resolution: {integrity: sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==} + elegant-spinner@1.0.1: resolution: {integrity: sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==} engines: {node: '>=0.10.0'} @@ -1184,6 +1855,15 @@ packages: resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} engines: {node: '>= 0.4'} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -1293,6 +1973,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1405,6 +2088,10 @@ packages: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1453,6 +2140,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -1625,6 +2316,11 @@ packages: resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} engines: {node: '>=0.10.0'} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-document.all@1.0.0: resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} engines: {node: '>= 0.4'} @@ -1653,6 +2349,11 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -1744,6 +2445,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -1792,6 +2497,11 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} @@ -1943,10 +2653,24 @@ packages: resolution: {integrity: sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==} engines: {node: '>=0.10.0'} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2030,6 +2754,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -2057,6 +2785,10 @@ packages: resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} engines: {node: '>= 0.4'} + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + engines: {node: '>=18'} + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -2115,6 +2847,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + opencollective-postinstall@2.0.3: resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} hasBin: true @@ -2127,6 +2863,13 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.127.0: + resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.23.0: + resolution: {integrity: sha512-f0+l598CJMOLnYPXsXxttJALH0ljtivdRMKtvHhxRuWa5FYmw5+qODARl8oYjMC/brpzKcrpdORsOBrTqhBZ9A==} + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -2210,6 +2953,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} @@ -2221,6 +2968,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2264,6 +3015,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -2293,6 +3048,27 @@ packages: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@8.0.3: + resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} + engines: {node: ^20.9.0 || >=22} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + read-pkg-up@3.0.0: resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} engines: {node: '>=4'} @@ -2313,6 +3089,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + recast@0.23.12: + resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} + engines: {node: '>= 4'} + redent@2.0.0: resolution: {integrity: sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==} engines: {node: '>=4'} @@ -2411,6 +3191,10 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-node@1.0.0: resolution: {integrity: sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==} engines: {node: '>=4'} @@ -2438,6 +3222,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver-compare@1.0.0: resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} @@ -2520,6 +3307,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -2552,6 +3343,21 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + storybook@10.4.6: + resolution: {integrity: sha512-6wkA6LxfDSSilloITsrFOJfsnw0mDUP2h8Ls+lRt8oRsudtz2RWFhLv+Toiwg6NW7hUpdTDc2hzR7DztJid6+A==} + hasBin: true + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + prettier: ^2 || ^3 + vite-plus: ^0.1.15 + peerDependenciesMeta: + '@types/react': + optional: true + prettier: + optional: true + vite-plus: + optional: true + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -2611,6 +3417,10 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} engines: {node: '>=0.8.0'} @@ -2645,6 +3455,9 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2656,10 +3469,18 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2682,6 +3503,14 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + tsdown@0.22.3: resolution: {integrity: sha512-louqbfA8Qf//B9jTTL0FPtXTNpjCWv1VPkbcmQMph2pTpzs+LnB1tbe4tDDRVpo2BjF5SgUXaTZe45SxB8pWHg==} engines: {node: ^22.18.0 || >=24.11.0} @@ -2776,6 +3605,10 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + unrs-resolver@1.12.2: resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} @@ -2789,9 +3622,20 @@ packages: synckit: optional: true + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2882,6 +3726,9 @@ packages: jsdom: optional: true + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2923,6 +3770,25 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -2939,12 +3805,44 @@ packages: snapshots: + '@adobe/css-tools@4.5.0': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/generator@8.0.0': dependencies: '@babel/parser': 8.0.0 @@ -2954,6 +3852,32 @@ snapshots: '@types/jsesc': 2.5.1 jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.29.7': {} '@babel/helper-string-parser@8.0.0': {} @@ -2962,6 +3886,13 @@ snapshots: '@babel/helper-validator-identifier@8.0.2': {} + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 @@ -2970,6 +3901,26 @@ snapshots: dependencies: '@babel/types': 8.0.0 + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -3079,6 +4030,12 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 @@ -3089,6 +4046,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -3099,6 +4061,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0)': dependencies: eslint: 10.6.0 @@ -3122,83 +4162,227 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.6.0)': - optionalDependencies: - eslint: 10.6.0 + '@eslint/js@10.0.1(eslint@10.6.0)': + optionalDependencies: + eslint: 10.6.0 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@5.9.3)(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1))': + dependencies: + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + vite: 8.1.1(@types/node@24.13.2)(esbuild@0.28.1) + optionalDependencies: + typescript: 5.9.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@marionebl/sander@0.6.1': + dependencies: + graceful-fs: 4.2.11 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-parser/binding-android-arm-eabi@0.127.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.127.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.127.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.127.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.127.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.127.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.127.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.127.0': + optional: true + + '@oxc-project/types@0.127.0': {} + + '@oxc-project/types@0.137.0': {} - '@eslint/object-schema@3.0.5': {} + '@oxc-resolver/binding-android-arm-eabi@11.23.0': + optional: true - '@eslint/plugin-kit@0.7.2': - dependencies: - '@eslint/core': 1.2.1 - levn: 0.4.1 + '@oxc-resolver/binding-android-arm64@11.23.0': + optional: true - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 + '@oxc-resolver/binding-darwin-arm64@11.23.0': + optional: true - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 + '@oxc-resolver/binding-darwin-x64@11.23.0': + optional: true - '@humanfs/types@0.15.0': {} + '@oxc-resolver/binding-freebsd-x64@11.23.0': + optional: true - '@humanwhocodes/module-importer@1.0.1': {} + '@oxc-resolver/binding-linux-arm-gnueabihf@11.23.0': + optional: true - '@humanwhocodes/retry@0.4.3': {} + '@oxc-resolver/binding-linux-arm-musleabihf@11.23.0': + optional: true - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@oxc-resolver/binding-linux-arm64-gnu@11.23.0': + optional: true - '@jridgewell/resolve-uri@3.1.2': {} + '@oxc-resolver/binding-linux-arm64-musl@11.23.0': + optional: true - '@jridgewell/sourcemap-codec@1.5.5': {} + '@oxc-resolver/binding-linux-ppc64-gnu@11.23.0': + optional: true - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@oxc-resolver/binding-linux-riscv64-gnu@11.23.0': + optional: true - '@marionebl/sander@0.6.1': - dependencies: - graceful-fs: 4.2.11 - mkdirp: 0.5.6 - rimraf: 2.7.1 + '@oxc-resolver/binding-linux-riscv64-musl@11.23.0': + optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.3 + '@oxc-resolver/binding-linux-s390x-gnu@11.23.0': optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 + '@oxc-resolver/binding-linux-x64-gnu@11.23.0': optional: true - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 + '@oxc-resolver/binding-linux-x64-musl@11.23.0': + optional: true - '@nodelib/fs.stat@2.0.5': {} + '@oxc-resolver/binding-openharmony-arm64@11.23.0': + optional: true - '@nodelib/fs.walk@1.2.8': + '@oxc-resolver/binding-wasm32-wasi@11.23.0': dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true - '@oxc-project/types@0.127.0': + '@oxc-resolver/binding-win32-arm64-msvc@11.23.0': optional: true - '@oxc-project/types@0.137.0': {} + '@oxc-resolver/binding-win32-x64-msvc@11.23.0': + optional: true '@pkgr/core@0.3.6': {} @@ -3309,6 +4493,12 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/pluginutils@5.4.0': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + '@samverschueren/stream-to-observable@0.3.1(rxjs@6.6.7)': dependencies: any-observable: 0.3.0(rxjs@6.6.7) @@ -3319,11 +4509,132 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@storybook/builder-vite@10.4.6(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7))(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1))': + dependencies: + '@storybook/csf-plugin': 10.4.6(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7))(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1)) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7) + ts-dedent: 2.3.0 + vite: 8.1.1(@types/node@24.13.2)(esbuild@0.28.1) + transitivePeerDependencies: + - esbuild + - rollup + - webpack + + '@storybook/csf-plugin@10.4.6(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7))(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1))': + dependencies: + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.28.1 + vite: 8.1.1(@types/node@24.13.2)(esbuild@0.28.1) + + '@storybook/global@5.0.0': {} + + '@storybook/icons@2.1.0(react@19.2.7)': + dependencies: + react: 19.2.7 + + '@storybook/react-dom-shim@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7))': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@storybook/react-vite@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7))(typescript@5.9.3)(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1)) + '@rollup/pluginutils': 5.4.0 + '@storybook/builder-vite': 10.4.6(esbuild@0.28.1)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7))(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1)) + '@storybook/react': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7))(typescript@5.9.3) + empathic: 2.0.1 + magic-string: 0.30.21 + react: 19.2.7 + react-docgen: 8.0.3 + react-dom: 19.2.7(react@19.2.7) + resolve: 1.22.12 + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7) + tsconfig-paths: 4.2.0 + vite: 8.1.1(@types/node@24.13.2)(esbuild@0.28.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - esbuild + - rollup + - supports-color + - typescript + - webpack + + '@storybook/react@10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7))(typescript@5.9.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 10.4.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7)) + react: 19.2.7 + react-docgen: 8.0.3 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + react-dom: 19.2.7(react@19.2.7) + storybook: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -3331,6 +4642,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/doctrine@0.0.9': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} @@ -3363,10 +4676,16 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + '@types/react@19.2.17': dependencies: csstype: 3.2.3 + '@types/resolve@1.20.6': {} + '@types/semver@7.7.1': {} '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0)(typescript@5.9.3))(eslint@10.6.0)(typescript@5.9.3)': @@ -3542,7 +4861,15 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(vite@8.1.1(@types/node@24.13.2)) + vitest: 4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1)) + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 '@vitest/expect@4.1.9': dependencies: @@ -3553,13 +4880,17 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.9(vite@8.1.1(@types/node@24.13.2))': + '@vitest/mocker@4.1.9(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1))': dependencies: '@vitest/spy': 4.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.1(@types/node@24.13.2) + vite: 8.1.1(@types/node@24.13.2)(esbuild@0.28.1) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 '@vitest/pretty-format@4.1.9': dependencies: @@ -3577,14 +4908,26 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + '@vitest/spy@4.1.9': {} + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@vitest/utils@4.1.9': dependencies: '@vitest/pretty-format': 4.1.9 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@webcontainer/env@1.1.1': {} + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -3614,12 +4957,16 @@ snapshots: ansi-regex@3.0.1: {} + ansi-regex@5.0.1: {} + ansi-styles@2.2.1: {} ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 + ansi-styles@5.2.0: {} + ansis@4.3.1: {} any-observable@0.3.0(rxjs@6.6.7): @@ -3630,6 +4977,12 @@ snapshots: dependencies: sprintf-js: 1.0.3 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -3671,6 +5024,10 @@ snapshots: estree-walker: 3.0.3 pathe: 2.0.3 + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + ast-v8-to-istanbul@1.0.4: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -3702,6 +5059,8 @@ snapshots: balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.41: {} + birpc@4.0.0: {} brace-expansion@1.1.15: @@ -3717,6 +5076,18 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.4: + dependencies: + baseline-browser-mapping: 2.10.41 + caniuse-lite: 1.0.30001800 + electron-to-chromium: 1.5.387 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.4) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + cac@7.0.0: {} call-bind-apply-helpers@1.0.2: @@ -3766,6 +5137,16 @@ snapshots: camelcase@5.3.1: {} + caniuse-lite@1.0.30001800: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chai@6.2.2: {} chalk@1.1.3: @@ -3782,6 +5163,8 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 + check-error@2.1.3: {} + ci-info@2.0.0: {} clean-stack@2.2.0: {} @@ -3863,6 +5246,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css.escape@1.5.1: {} + csstype@3.2.3: {} currently-unhandled@0.4.1: @@ -3912,8 +5297,17 @@ snapshots: dedent@0.7.0: {} + deep-eql@5.0.2: {} + deep-is@0.1.4: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -3921,6 +5315,8 @@ snapshots: gopd: 1.2.0 optional: true + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -3943,17 +5339,29 @@ snapshots: delayed-stream@1.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} dir-glob@3.0.1: dependencies: path-type: 4.0.0 + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dot-prop@3.0.0: dependencies: is-obj: 1.0.1 - dts-resolver@3.0.0: {} + dts-resolver@3.0.0(oxc-resolver@11.23.0): + optionalDependencies: + oxc-resolver: 11.23.0 dunder-proto@1.0.1: dependencies: @@ -3961,6 +5369,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + electron-to-chromium@1.5.387: {} + elegant-spinner@1.0.1: {} empathic@2.0.1: {} @@ -4071,6 +5481,37 @@ snapshots: is-symbol: 1.1.1 optional: true + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -4201,6 +5642,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -4332,6 +5775,8 @@ snapshots: generator-function@2.0.1: optional: true + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4393,6 +5838,12 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -4579,6 +6030,8 @@ snapshots: is-directory@0.3.1: {} + is-docker@3.0.0: {} + is-document.all@1.0.0: dependencies: call-bound: 1.0.4 @@ -4610,6 +6063,10 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-map@2.0.3: optional: true @@ -4696,6 +6153,10 @@ snapshots: get-intrinsic: 1.3.0 optional: true + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isarray@2.0.5: optional: true @@ -4735,6 +6196,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + jsonparse@1.3.1: {} kcsapi@1.260604.0: {} @@ -4901,10 +6364,20 @@ snapshots: currently-unhandled: 0.4.1 signal-exit: 3.0.7 + loupe@3.2.1: {} + + lru-cache@11.5.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lru-cache@6.0.0: dependencies: yallist: 4.0.0 + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4995,6 +6468,8 @@ snapshots: minimist@1.2.8: {} + minipass@7.1.3: {} + mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -5017,6 +6492,8 @@ snapshots: semver: 6.3.1 optional: true + node-releases@2.0.50: {} + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -5083,6 +6560,13 @@ snapshots: dependencies: mimic-fn: 2.1.0 + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + opencollective-postinstall@2.0.3: {} optionator@0.9.4: @@ -5101,6 +6585,53 @@ snapshots: safe-push-apply: 1.0.0 optional: true + oxc-parser@0.127.0: + dependencies: + '@oxc-project/types': 0.127.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.127.0 + '@oxc-parser/binding-android-arm64': 0.127.0 + '@oxc-parser/binding-darwin-arm64': 0.127.0 + '@oxc-parser/binding-darwin-x64': 0.127.0 + '@oxc-parser/binding-freebsd-x64': 0.127.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.127.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.127.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.127.0 + '@oxc-parser/binding-linux-arm64-musl': 0.127.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.127.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.127.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-gnu': 0.127.0 + '@oxc-parser/binding-linux-x64-musl': 0.127.0 + '@oxc-parser/binding-openharmony-arm64': 0.127.0 + '@oxc-parser/binding-wasm32-wasi': 0.127.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.127.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.127.0 + '@oxc-parser/binding-win32-x64-msvc': 0.127.0 + + oxc-resolver@11.23.0: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.23.0 + '@oxc-resolver/binding-android-arm64': 11.23.0 + '@oxc-resolver/binding-darwin-arm64': 11.23.0 + '@oxc-resolver/binding-darwin-x64': 11.23.0 + '@oxc-resolver/binding-freebsd-x64': 11.23.0 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.23.0 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.23.0 + '@oxc-resolver/binding-linux-arm64-gnu': 11.23.0 + '@oxc-resolver/binding-linux-arm64-musl': 11.23.0 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.23.0 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.23.0 + '@oxc-resolver/binding-linux-riscv64-musl': 11.23.0 + '@oxc-resolver/binding-linux-s390x-gnu': 11.23.0 + '@oxc-resolver/binding-linux-x64-gnu': 11.23.0 + '@oxc-resolver/binding-linux-x64-musl': 11.23.0 + '@oxc-resolver/binding-openharmony-arm64': 11.23.0 + '@oxc-resolver/binding-wasm32-wasi': 11.23.0 + '@oxc-resolver/binding-win32-arm64-msvc': 11.23.0 + '@oxc-resolver/binding-win32-x64-msvc': 11.23.0 + p-finally@1.0.0: {} p-finally@2.0.1: {} @@ -5167,6 +6698,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + path-type@3.0.0: dependencies: pify: 3.0.0 @@ -5175,6 +6711,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -5208,6 +6746,12 @@ snapshots: prettier@3.9.4: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -5225,6 +6769,34 @@ snapshots: quick-lru@4.0.1: {} + react-docgen-typescript@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + react-docgen@8.0.3: + dependencies: + '@babel/core': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.12 + strip-indent: 4.1.1 + transitivePeerDependencies: + - supports-color + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react@19.2.7: {} + read-pkg-up@3.0.0: dependencies: find-up: 2.1.0 @@ -5255,6 +6827,14 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + recast@0.23.12: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + redent@2.0.0: dependencies: indent-string: 3.2.0 @@ -5335,14 +6915,14 @@ snapshots: dependencies: glob: 7.2.3 - rolldown-plugin-dts@0.26.0(rolldown@1.1.3)(typescript@5.9.3): + rolldown-plugin-dts@0.26.0(oxc-resolver@11.23.0)(rolldown@1.1.3)(typescript@5.9.3): dependencies: '@babel/generator': 8.0.0 '@babel/helper-validator-identifier': 8.0.2 '@babel/parser': 8.0.0 ast-kit: 3.0.0 birpc: 4.0.0 - dts-resolver: 3.0.0 + dts-resolver: 3.0.0(oxc-resolver@11.23.0) get-tsconfig: 5.0.0-beta.5 obug: 2.1.3 rolldown: 1.1.3 @@ -5394,6 +6974,8 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.3 '@rolldown/binding-win32-x64-msvc': 1.1.3 + run-applescript@7.1.0: {} + run-node@1.0.0: {} run-parallel@1.2.0: @@ -5428,14 +7010,15 @@ snapshots: is-regex: 1.2.1 optional: true + scheduler@0.27.0: {} + semver-compare@1.0.0: {} semver@5.7.2: {} semver@6.3.0: {} - semver@6.3.1: - optional: true + semver@6.3.1: {} semver@7.8.5: {} @@ -5518,6 +7101,8 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -5550,6 +7135,32 @@ snapshots: internal-slot: 1.1.0 optional: true + storybook@10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7): + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 2.1.0(react@19.2.7) + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + '@webcontainer/env': 1.1.1 + esbuild: 0.28.1 + open: 10.2.0 + oxc-parser: 0.127.0 + oxc-resolver: 11.23.0 + recast: 0.23.12 + semver: 7.8.5 + use-sync-external-store: 1.6.0(react@19.2.7) + ws: 8.21.0 + optionalDependencies: + '@types/react': 19.2.17 + prettier: 3.9.4 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - react + - utf-8-validate + string-argv@0.3.2: {} string-width@1.0.2: @@ -5620,6 +7231,8 @@ snapshots: dependencies: min-indent: 1.0.1 + strip-indent@4.1.1: {} + supports-color@2.0.0: {} supports-color@5.5.0: @@ -5646,6 +7259,8 @@ snapshots: through@2.3.8: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@1.2.4: {} @@ -5655,8 +7270,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyrainbow@2.0.0: {} + tinyrainbow@3.1.0: {} + tinyspy@4.0.4: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5671,7 +7290,15 @@ snapshots: dependencies: typescript: 5.9.3 - tsdown@0.22.3(typescript@5.9.3)(unrun@0.2.39(synckit@0.11.13)): + ts-dedent@2.3.0: {} + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsdown@0.22.3(oxc-resolver@11.23.0)(typescript@5.9.3)(unrun@0.2.39(synckit@0.11.13)): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -5682,7 +7309,7 @@ snapshots: obug: 2.1.3 picomatch: 4.0.4 rolldown: 1.1.3 - rolldown-plugin-dts: 0.26.0(rolldown@1.1.3)(typescript@5.9.3) + rolldown-plugin-dts: 0.26.0(oxc-resolver@11.23.0)(rolldown@1.1.3)(typescript@5.9.3) semver: 7.8.5 tinyexec: 1.2.4 tinyglobby: 0.2.17 @@ -5699,8 +7326,7 @@ snapshots: tslib@1.14.1: {} - tslib@2.8.1: - optional: true + tslib@2.8.1: {} type-check@0.4.0: dependencies: @@ -5777,6 +7403,13 @@ snapshots: undici-types@7.18.2: {} + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.17.0 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + unrs-resolver@1.12.2: dependencies: napi-postinstall: 0.3.4 @@ -5811,10 +7444,20 @@ snapshots: synckit: 0.11.13 optional: true + update-browserslist-db@1.2.3(browserslist@4.28.4): + dependencies: + browserslist: 4.28.4 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 + util-deprecate@1.0.2: {} validate-npm-package-license@3.0.4: @@ -5822,7 +7465,7 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite@8.1.1(@types/node@24.13.2): + vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -5831,12 +7474,13 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 + esbuild: 0.28.1 fsevents: 2.3.3 - vitest@4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(vite@8.1.1(@types/node@24.13.2)): + vitest@4.1.9(@types/node@24.13.2)(@vitest/coverage-v8@4.1.9)(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1)): dependencies: '@vitest/expect': 4.1.9 - '@vitest/mocker': 4.1.9(vite@8.1.1(@types/node@24.13.2)) + '@vitest/mocker': 4.1.9(vite@8.1.1(@types/node@24.13.2)(esbuild@0.28.1)) '@vitest/pretty-format': 4.1.9 '@vitest/runner': 4.1.9 '@vitest/snapshot': 4.1.9 @@ -5853,7 +7497,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.1(@types/node@24.13.2) + vite: 8.1.1(@types/node@24.13.2)(esbuild@0.28.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.2 @@ -5861,6 +7505,8 @@ snapshots: transitivePeerDependencies: - msw + webpack-virtual-modules@0.6.2: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -5928,6 +7574,14 @@ snapshots: wrappy@1.0.2: {} + ws@8.21.0: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + yallist@3.1.1: {} + yallist@4.0.0: {} yargs-parser@10.1.0: diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index 4561aef..8c63ab1 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -1,6 +1,7 @@ import type { GameResponseEventDetail } from './types/game-api' const ENABLED_KEY = 'poi-plugin-report:remodel-debug-recorder' +const MAX_RECORDS = 200 const REMODEL_PATHS = new Set([ '/kcsapi/api_req_kousyou/remodel_slotlist', @@ -50,12 +51,16 @@ export const isRemodelDebugRecorderEnabled = (): boolean => { } export const setRemodelDebugRecorderEnabled = (enabled: boolean): void => { - if (enabled) { - window.localStorage?.setItem(ENABLED_KEY, '1') - return - } + try { + if (enabled) { + window.localStorage?.setItem(ENABLED_KEY, '1') + return + } - window.localStorage?.removeItem(ENABLED_KEY) + window.localStorage?.removeItem(ENABLED_KEY) + } catch (err) { + console.error(err) + } } const getPostBodySlotId = (postBody: unknown): string | number | undefined => { @@ -79,6 +84,14 @@ const pickStringOrNumber = ( return typeof value === 'string' || typeof value === 'number' ? value : undefined } +const pickBooleanOrNumber = ( + record: Record, + key: string, +): boolean | number | undefined => { + const value = record[key] + return typeof value === 'boolean' || typeof value === 'number' ? value : undefined +} + const sanitizePostBody = (path: string, postBody: unknown): unknown => { if (postBody == null || typeof postBody !== 'object') { return {} @@ -147,7 +160,7 @@ const sanitizeSlotBody = (body: unknown): unknown => { : {} return { - api_remodel_flag: pickNumber(record, 'api_remodel_flag'), + api_remodel_flag: pickBooleanOrNumber(record, 'api_remodel_flag'), api_remodel_id: Array.isArray(record.api_remodel_id) ? record.api_remodel_id.filter((value) => typeof value === 'number') : undefined, @@ -174,7 +187,7 @@ const sanitizeBody = (path: string, body: unknown): unknown => { } const createSanitizedContext = (slotId: string | number | undefined): SanitizedFleetContext => { - const deckShipIds = window._decks[0]?.api_ship.slice(0, 2) || [] + const deckShipIds = window._decks[0]?.api_ship?.slice(0, 2) || [] const flagship = deckShipIds[0] == null ? undefined : window._ships[deckShipIds[0]] const secondShip = deckShipIds[1] == null ? undefined : window._ships[deckShipIds[1]] if (slotId != null) { @@ -253,6 +266,9 @@ export const recordRemodelDebugEvent = (event: GameResponseEventDetail): void => } records.push(record) + if (records.length > MAX_RECORDS) { + records.splice(0, records.length - MAX_RECORDS) + } } catch (err) { console.error(err) } @@ -275,5 +291,5 @@ export const exportRemodelDebugRecords = (): void => { .toISOString() .replace(/[:.]/g, '-')}.json` anchor.click() - URL.revokeObjectURL(href) + setTimeout(() => URL.revokeObjectURL(href), 0) } diff --git a/src/remodel-debug-settings.stories.ts b/src/remodel-debug-settings.stories.ts new file mode 100644 index 0000000..071c323 --- /dev/null +++ b/src/remodel-debug-settings.stories.ts @@ -0,0 +1,62 @@ +import React from 'react' + +import RemodelDebugSettings from './remodel-debug-settings' +import { + clearRemodelDebugRecords, + recordRemodelDebugEvent, + setRemodelDebugRecorderEnabled, +} from './remodel-debug-recorder' + +const meta = { + title: 'Plugin/RemodelDebugSettings', + component: RemodelDebugSettings, + decorators: [ + (Story: () => React.ReactElement) => + React.createElement( + 'div', + { + style: { + padding: 16, + maxWidth: 720, + }, + }, + React.createElement(Story), + ), + ], +} + +export default meta + +interface Story { + beforeEach(): void +} + +export const Empty: Story = { + beforeEach() { + setRemodelDebugRecorderEnabled(false) + clearRemodelDebugRecords() + }, +} + +export const EnabledWithCapture: Story = { + beforeEach() { + setRemodelDebugRecorderEnabled(true) + clearRemodelDebugRecords() + recordRemodelDebugEvent({ + time: Date.UTC(2026, 6, 3, 15), + method: 'POST', + path: '/kcsapi/api_req_kousyou/remodel_slotlist_detail', + postBody: { + api_id: '33', + api_slot_id: '501', + api_token: 'redacted by sanitizer', + }, + body: { + api_req_buildkit: 3, + api_req_remodelkit: 4, + api_certain_buildkit: 5, + api_certain_remodelkit: 6, + }, + }) + }, +} diff --git a/src/reporters/remodel-recipe.ts b/src/reporters/remodel-recipe.ts index 3b93549..275f5b6 100644 --- a/src/reporters/remodel-recipe.ts +++ b/src/reporters/remodel-recipe.ts @@ -292,7 +292,7 @@ export default class RemodelRecipeReporter extends BaseReporter { getFleetContext(): RemodelRecipeFleetContext | undefined { const deck = window._decks[0] - const flagshipRosterId = deck?.api_ship[0] + const flagshipRosterId = deck?.api_ship?.[0] if (flagshipRosterId == null || Number(flagshipRosterId) <= 0) { return undefined } @@ -303,7 +303,7 @@ export default class RemodelRecipeReporter extends BaseReporter { return undefined } - const secondRosterId = deck?.api_ship[1] + const secondRosterId = deck?.api_ship?.[1] if (secondRosterId == null || Number(secondRosterId) <= 0) { return { observedSecondShipId: 0, @@ -603,11 +603,11 @@ export default class RemodelRecipeReporter extends BaseReporter { const update: ItemImprovementUpdatePayload = { schemaVersion: 1, source: 'execution', - clientObservedAt: currentDetail.clientObservedAt, + clientObservedAt: time, recipeId: currentDetail.recipeId, itemId: currentDetail.itemId, itemLevel: currentDetail.itemLevel, - day: currentDetail.day, + day: getJstDay(time), observedSecondShipId: currentDetail.observedSecondShipId, observedFlagshipId: currentDetail.observedFlagshipId, upgradeObserved: true, diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index 8a94428..675ad3e 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -1,10 +1,11 @@ -import { beforeEach, describe, expect, it } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { clearRemodelDebugRecords, createRemodelDebugRecord, getRemodelDebugRecords, recordRemodelDebugEvent, + setRemodelDebugRecorderEnabled, } from '../src/remodel-debug-recorder' import type { GameResponseEventDetail } from '../src/types/game-api' @@ -103,4 +104,75 @@ describe('remodel debug recorder', () => { }, }) }) + + it('keeps boolean remodel flags in slot execution captures', () => { + const record = createRemodelDebugRecord({ + time: 1710000000001, + method: 'POST', + path: '/kcsapi/api_req_kousyou/remodel_slot', + postBody: { + api_id: '33', + api_slot_id: '501', + api_token: 'secret-token', + }, + body: { + api_remodel_flag: true, + api_remodel_id: [700, 701], + api_after_slot: { + api_slotitem_id: 701, + api_level: 0, + }, + }, + }) + + expect(record).toMatchObject({ + postBody: { + api_id: '33', + }, + body: { + api_remodel_flag: true, + }, + }) + }) + + it('handles missing deck ship arrays while recording', () => { + window._decks = [{} as (typeof window._decks)[number]] + + expect(() => recordRemodelDebugEvent(remodelDetailEvent)).not.toThrow() + }) + + it('caps in-memory captures to the latest 200 records', () => { + window.localStorage.setItem('poi-plugin-report:remodel-debug-recorder', '1') + + for (let index = 0; index < 205; index += 1) { + recordRemodelDebugEvent({ + ...remodelDetailEvent, + time: index, + }) + } + + expect(getRemodelDebugRecords()).toHaveLength(200) + expect(getRemodelDebugRecords()[0]?.time).toBe(5) + }) + + it('does not throw when localStorage writes fail', () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + globalThis.window = { + ...createWindow(), + localStorage: { + getItem: () => null, + setItem: () => { + throw new Error('blocked') + }, + removeItem: () => { + throw new Error('blocked') + }, + }, + } as unknown as Window & typeof globalThis + + expect(() => setRemodelDebugRecorderEnabled(true)).not.toThrow() + expect(() => setRemodelDebugRecorderEnabled(false)).not.toThrow() + expect(consoleError).toHaveBeenCalledTimes(2) + consoleError.mockRestore() + }) }) diff --git a/tests/reporters/remodel-recipe.test.ts b/tests/reporters/remodel-recipe.test.ts index a8044b6..ac1d756 100644 --- a/tests/reporters/remodel-recipe.test.ts +++ b/tests/reporters/remodel-recipe.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { attachReportSpy, @@ -6,7 +6,17 @@ import { resetReporterTestState, } from '../helpers/reporter-test-harness' -beforeEach(resetReporterTestState) +const fixedTestTime = Date.UTC(2026, 6, 3, 15) + +beforeEach(() => { + resetReporterTestState() + vi.useFakeTimers() + vi.setSystemTime(fixedTestTime) +}) + +afterEach(() => { + vi.useRealTimers() +}) describe('RemodelRecipeReporter', () => { const detailBody = { @@ -28,6 +38,8 @@ describe('RemodelRecipeReporter', () => { api_req_bauxite: 40, }, ] + const detailTime = fixedTestTime + const executionTime = Date.UTC(2026, 6, 4, 15) it('reports v3 availability and detail facts without execution', () => { window._slotitems[501] = { api_slotitem_id: 700, api_level: 0 } @@ -112,11 +124,17 @@ describe('RemodelRecipeReporter', () => { const reporter = new RemodelRecipeReporter() const report = attachReportSpy(reporter) - reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) - reporter.handle('POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', detailBody, { - api_id: '33', - api_slot_id: 501, - }) + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody, {}, detailTime) + reporter.handle( + 'POST', + '/kcsapi/api_req_kousyou/remodel_slotlist_detail', + detailBody, + { + api_id: '33', + api_slot_id: 501, + }, + detailTime, + ) reporter.handle( 'POST', '/kcsapi/api_req_kousyou/remodel_slot', @@ -129,6 +147,7 @@ describe('RemodelRecipeReporter', () => { { api_id: '33', }, + executionTime, ) expect(report).toHaveBeenCalledWith('/api/report/v2/remodel_recipe', { @@ -154,11 +173,11 @@ describe('RemodelRecipeReporter', () => { expect(report).toHaveBeenCalledWith('/api/report/v3/item_improvement_recipe', { schemaVersion: 1, source: 'execution', - clientObservedAt: expect.any(Number), + clientObservedAt: executionTime, recipeId: 33, itemId: 700, itemLevel: 6, - day: 6, + day: 0, observedSecondShipId: 102, observedFlagshipId: 101, upgradeObserved: true, diff --git a/tsdown.config.ts b/tsdown.config.ts index f2dd7a3..4339e63 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -22,6 +22,7 @@ module.exports = defineConfig({ outExtensions: () => ({ js: '.js' }), format: ['cjs'], deps: { + onlyBundle: ['react'], neverBundle: [ '@electron/remote', '@sentry/electron', @@ -29,7 +30,6 @@ module.exports = defineConfig({ 'lodash', 'moment-timezone', 'node-fetch', - 'react', 'semver', /^views\//, ], From f9f7a8f1b52847c718f2d0f9fe721f95fb4b6688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 14:36:42 +0800 Subject: [PATCH 06/26] refactor: simplify recorder settings component Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-recorder.ts | 22 +++- src/remodel-debug-settings.ts | 150 ++++++++++----------------- tests/remodel-debug-recorder.test.ts | 15 +++ 3 files changed, 91 insertions(+), 96 deletions(-) diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index 8c63ab1..26e0298 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -34,6 +34,13 @@ export interface RemodelDebugRecord { } const records: RemodelDebugRecord[] = [] +const listeners = new Set<() => void>() + +const notifyListeners = (): void => { + for (const listener of listeners) { + listener() + } +} const cloneJson = (value: unknown): unknown => { if (value == null) { @@ -54,13 +61,20 @@ export const setRemodelDebugRecorderEnabled = (enabled: boolean): void => { try { if (enabled) { window.localStorage?.setItem(ENABLED_KEY, '1') - return + } else { + window.localStorage?.removeItem(ENABLED_KEY) } - - window.localStorage?.removeItem(ENABLED_KEY) } catch (err) { console.error(err) } + notifyListeners() +} + +export const subscribeRemodelDebugRecorder = (listener: () => void): (() => void) => { + listeners.add(listener) + return () => { + listeners.delete(listener) + } } const getPostBodySlotId = (postBody: unknown): string | number | undefined => { @@ -269,6 +283,7 @@ export const recordRemodelDebugEvent = (event: GameResponseEventDetail): void => if (records.length > MAX_RECORDS) { records.splice(0, records.length - MAX_RECORDS) } + notifyListeners() } catch (err) { console.error(err) } @@ -276,6 +291,7 @@ export const recordRemodelDebugEvent = (event: GameResponseEventDetail): void => export const clearRemodelDebugRecords = (): void => { records.length = 0 + notifyListeners() } export const getRemodelDebugRecords = (): readonly RemodelDebugRecord[] => records diff --git a/src/remodel-debug-settings.ts b/src/remodel-debug-settings.ts index 6cc1e95..7910020 100644 --- a/src/remodel-debug-settings.ts +++ b/src/remodel-debug-settings.ts @@ -1,120 +1,84 @@ -import React, { Component, type CSSProperties, type ReactNode } from 'react' +import React, { type CSSProperties, type ReactElement, useEffect, useState } from 'react' import { clearRemodelDebugRecords, exportRemodelDebugRecords, getRemodelDebugRecords, isRemodelDebugRecorderEnabled, setRemodelDebugRecorderEnabled, + subscribeRemodelDebugRecorder, } from './remodel-debug-recorder' -interface RemodelDebugSettingsState { - enabled: boolean - count: number +const rootStyle: CSSProperties = { + display: 'grid', + gap: 8, + lineHeight: 1.5, + maxWidth: 640, } -const createStyle = (style: CSSProperties): CSSProperties => style +const buttonRowStyle: CSSProperties = { + display: 'flex', + gap: 8, +} -export default class RemodelDebugSettings extends Component< - Record, - RemodelDebugSettingsState -> { - refreshTimer: ReturnType | undefined +const getRecorderState = () => ({ + enabled: isRemodelDebugRecorderEnabled(), + count: getRemodelDebugRecords().length, +}) - constructor(props: Record) { - super(props) - this.state = { - enabled: isRemodelDebugRecorderEnabled(), - count: getRemodelDebugRecords().length, - } - } +export default function RemodelDebugSettings(): ReactElement { + const [recorderState, setRecorderState] = useState(getRecorderState) - componentDidMount() { - this.refreshTimer = setInterval(() => { - this.setState({ - enabled: isRemodelDebugRecorderEnabled(), - count: getRemodelDebugRecords().length, - }) - }, 1000) - } + useEffect(() => subscribeRemodelDebugRecorder(() => setRecorderState(getRecorderState())), []) - componentWillUnmount() { - if (this.refreshTimer) { - clearInterval(this.refreshTimer) - } + const toggleRecorder = () => { + setRemodelDebugRecorderEnabled(!isRemodelDebugRecorderEnabled()) } - toggleRecorder = () => { - const enabled = !isRemodelDebugRecorderEnabled() - setRemodelDebugRecorderEnabled(enabled) - this.setState({ enabled }) - } - - clearRecords = () => { + const clearRecords = () => { clearRemodelDebugRecords() - this.setState({ count: 0 }) - } - - exportRecords = () => { - exportRemodelDebugRecords() } - render(): ReactNode { - const { enabled, count } = this.state - - return React.createElement( + return React.createElement( + 'div', + { style: rootStyle }, + React.createElement('h4', null, 'Remodel recipe debug recorder'), + React.createElement( + 'p', + null, + 'Opt-in local recorder for validating Akashi remodel API sequences. It captures only allowlisted remodel fields, keeps records in memory, and writes a file only when Export is clicked.', + ), + React.createElement( + 'label', + null, + React.createElement('input', { + checked: recorderState.enabled, + onChange: toggleRecorder, + type: 'checkbox', + }), + ' Enable remodel debug recorder', + ), + React.createElement('p', null, `Captured records: ${recorderState.count}`), + React.createElement( 'div', - { - style: createStyle({ - display: 'grid', - gap: 8, - lineHeight: 1.5, - maxWidth: 640, - }), - }, - React.createElement('h4', null, 'Remodel recipe debug recorder'), - React.createElement( - 'p', - null, - 'Opt-in local recorder for validating Akashi remodel API sequences. It captures only allowlisted remodel fields, keeps records in memory, and writes a file only when Export is clicked.', - ), + { style: buttonRowStyle }, React.createElement( - 'label', - null, - React.createElement('input', { - checked: enabled, - onChange: this.toggleRecorder, - type: 'checkbox', - }), - ' Enable remodel debug recorder', + 'button', + { + disabled: recorderState.count === 0, + onClick: exportRemodelDebugRecords, + type: 'button', + }, + 'Export', ), - React.createElement('p', null, `Captured records: ${count}`), React.createElement( - 'div', + 'button', { - style: createStyle({ - display: 'flex', - gap: 8, - }), + disabled: recorderState.count === 0, + onClick: clearRecords, + type: 'button', }, - React.createElement( - 'button', - { - disabled: count === 0, - onClick: this.exportRecords, - type: 'button', - }, - 'Export', - ), - React.createElement( - 'button', - { - disabled: count === 0, - onClick: this.clearRecords, - type: 'button', - }, - 'Clear', - ), + 'Clear', ), - ) - } + ), + ) } diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index 675ad3e..96d7e17 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -6,6 +6,7 @@ import { getRemodelDebugRecords, recordRemodelDebugEvent, setRemodelDebugRecorderEnabled, + subscribeRemodelDebugRecorder, } from '../src/remodel-debug-recorder' import type { GameResponseEventDetail } from '../src/types/game-api' @@ -155,6 +156,20 @@ describe('remodel debug recorder', () => { expect(getRemodelDebugRecords()[0]?.time).toBe(5) }) + it('notifies subscribers when setting or records change', () => { + const listener = vi.fn() + const unsubscribe = subscribeRemodelDebugRecorder(listener) + + setRemodelDebugRecorderEnabled(true) + recordRemodelDebugEvent(remodelDetailEvent) + clearRemodelDebugRecords() + + expect(listener).toHaveBeenCalledTimes(3) + unsubscribe() + setRemodelDebugRecorderEnabled(false) + expect(listener).toHaveBeenCalledTimes(3) + }) + it('does not throw when localStorage writes fail', () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) globalThis.window = { From f7d1b15c22b6905980209bfc6b2a26a9db377a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 14:41:29 +0800 Subject: [PATCH 07/26] refactor: write recorder settings in tsx Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eslint.config.mjs | 5 +- ....ts => remodel-debug-settings.stories.tsx} | 23 +++-- src/remodel-debug-settings.ts | 84 ------------------- src/remodel-debug-settings.tsx | 68 +++++++++++++++ tsconfig.json | 3 +- 5 files changed, 84 insertions(+), 99 deletions(-) rename src/{remodel-debug-settings.stories.ts => remodel-debug-settings.stories.tsx} (80%) delete mode 100644 src/remodel-debug-settings.ts create mode 100644 src/remodel-debug-settings.tsx diff --git a/eslint.config.mjs b/eslint.config.mjs index 111508b..cc36af5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,7 +22,7 @@ export default tseslint.config( ...tseslint.configs.recommended, importX.flatConfigs.recommended, { - files: ['**/*.{js,cjs,mjs,ts}'], + files: ['**/*.{js,cjs,mjs,ts,tsx}'], languageOptions: { ecmaVersion: 2022, sourceType: 'module', @@ -61,6 +61,7 @@ export default tseslint.config( js: 'never', mjs: 'never', ts: 'never', + tsx: 'never', }, ], 'import-x/no-unresolved': [ @@ -78,7 +79,7 @@ export default tseslint.config( }, }, { - files: ['src/**/*.ts'], + files: ['src/**/*.{ts,tsx}'], extends: [tseslint.configs.recommendedTypeChecked], languageOptions: { parserOptions: { diff --git a/src/remodel-debug-settings.stories.ts b/src/remodel-debug-settings.stories.tsx similarity index 80% rename from src/remodel-debug-settings.stories.ts rename to src/remodel-debug-settings.stories.tsx index 071c323..6589e78 100644 --- a/src/remodel-debug-settings.stories.ts +++ b/src/remodel-debug-settings.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import type { ReactElement } from 'react' import RemodelDebugSettings from './remodel-debug-settings' import { @@ -11,17 +11,16 @@ const meta = { title: 'Plugin/RemodelDebugSettings', component: RemodelDebugSettings, decorators: [ - (Story: () => React.ReactElement) => - React.createElement( - 'div', - { - style: { - padding: 16, - maxWidth: 720, - }, - }, - React.createElement(Story), - ), + (Story: () => ReactElement) => ( +

+ +
+ ), ], } diff --git a/src/remodel-debug-settings.ts b/src/remodel-debug-settings.ts deleted file mode 100644 index 7910020..0000000 --- a/src/remodel-debug-settings.ts +++ /dev/null @@ -1,84 +0,0 @@ -import React, { type CSSProperties, type ReactElement, useEffect, useState } from 'react' -import { - clearRemodelDebugRecords, - exportRemodelDebugRecords, - getRemodelDebugRecords, - isRemodelDebugRecorderEnabled, - setRemodelDebugRecorderEnabled, - subscribeRemodelDebugRecorder, -} from './remodel-debug-recorder' - -const rootStyle: CSSProperties = { - display: 'grid', - gap: 8, - lineHeight: 1.5, - maxWidth: 640, -} - -const buttonRowStyle: CSSProperties = { - display: 'flex', - gap: 8, -} - -const getRecorderState = () => ({ - enabled: isRemodelDebugRecorderEnabled(), - count: getRemodelDebugRecords().length, -}) - -export default function RemodelDebugSettings(): ReactElement { - const [recorderState, setRecorderState] = useState(getRecorderState) - - useEffect(() => subscribeRemodelDebugRecorder(() => setRecorderState(getRecorderState())), []) - - const toggleRecorder = () => { - setRemodelDebugRecorderEnabled(!isRemodelDebugRecorderEnabled()) - } - - const clearRecords = () => { - clearRemodelDebugRecords() - } - - return React.createElement( - 'div', - { style: rootStyle }, - React.createElement('h4', null, 'Remodel recipe debug recorder'), - React.createElement( - 'p', - null, - 'Opt-in local recorder for validating Akashi remodel API sequences. It captures only allowlisted remodel fields, keeps records in memory, and writes a file only when Export is clicked.', - ), - React.createElement( - 'label', - null, - React.createElement('input', { - checked: recorderState.enabled, - onChange: toggleRecorder, - type: 'checkbox', - }), - ' Enable remodel debug recorder', - ), - React.createElement('p', null, `Captured records: ${recorderState.count}`), - React.createElement( - 'div', - { style: buttonRowStyle }, - React.createElement( - 'button', - { - disabled: recorderState.count === 0, - onClick: exportRemodelDebugRecords, - type: 'button', - }, - 'Export', - ), - React.createElement( - 'button', - { - disabled: recorderState.count === 0, - onClick: clearRecords, - type: 'button', - }, - 'Clear', - ), - ), - ) -} diff --git a/src/remodel-debug-settings.tsx b/src/remodel-debug-settings.tsx new file mode 100644 index 0000000..8814cad --- /dev/null +++ b/src/remodel-debug-settings.tsx @@ -0,0 +1,68 @@ +import { type CSSProperties, type ReactElement, useEffect, useState } from 'react' +import { + clearRemodelDebugRecords, + exportRemodelDebugRecords, + getRemodelDebugRecords, + isRemodelDebugRecorderEnabled, + setRemodelDebugRecorderEnabled, + subscribeRemodelDebugRecorder, +} from './remodel-debug-recorder' + +const rootStyle: CSSProperties = { + display: 'grid', + gap: 8, + lineHeight: 1.5, + maxWidth: 640, +} + +const buttonRowStyle: CSSProperties = { + display: 'flex', + gap: 8, +} + +const getRecorderState = () => ({ + enabled: isRemodelDebugRecorderEnabled(), + count: getRemodelDebugRecords().length, +}) + +export default function RemodelDebugSettings(): ReactElement { + const [recorderState, setRecorderState] = useState(getRecorderState) + + useEffect(() => subscribeRemodelDebugRecorder(() => setRecorderState(getRecorderState())), []) + + const toggleRecorder = () => { + setRemodelDebugRecorderEnabled(!isRemodelDebugRecorderEnabled()) + } + + const clearRecords = () => { + clearRemodelDebugRecords() + } + + return ( +
+

Remodel recipe debug recorder

+

+ Opt-in local recorder for validating Akashi remodel API sequences. It captures only + allowlisted remodel fields, keeps records in memory, and writes a file only when Export is + clicked. +

+ +

Captured records: {recorderState.count}

+
+ + +
+
+ ) +} diff --git a/tsconfig.json b/tsconfig.json index 92988fa..97450cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,9 +8,10 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, + "jsx": "react-jsx", "baseUrl": ".", "types": ["node"] }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "tests/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "tests/**/*.ts", "tests/**/*.tsx"], "exclude": ["node_modules"] } From 3fae4ae445f864ad5fcd3c667f5e929432ebf7e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 15:14:39 +0800 Subject: [PATCH 08/26] perf: skip recorder storage reads for unrelated APIs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-recorder.ts | 3 +++ tests/remodel-debug-recorder.test.ts | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index 26e0298..ba2393a 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -269,6 +269,9 @@ export const createRemodelDebugRecord = ( } export const recordRemodelDebugEvent = (event: GameResponseEventDetail): void => { + if (!REMODEL_PATHS.has(event.path)) { + return + } if (!isRemodelDebugRecorderEnabled()) { return } diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index 96d7e17..54ce35d 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -64,6 +64,26 @@ describe('remodel debug recorder', () => { expect(getRemodelDebugRecords()).toHaveLength(0) }) + it('ignores unrelated APIs before checking localStorage', () => { + const getItem = vi.fn(() => '1') + globalThis.window = { + ...createWindow(), + localStorage: { + getItem, + setItem: vi.fn(), + removeItem: vi.fn(), + }, + } as unknown as Window & typeof globalThis + + recordRemodelDebugEvent({ + ...remodelDetailEvent, + path: '/kcsapi/api_get_member/ship2', + }) + + expect(getItem).not.toHaveBeenCalled() + expect(getRemodelDebugRecords()).toHaveLength(0) + }) + it('records only remodel APIs when enabled', () => { window.localStorage.setItem('poi-plugin-report:remodel-debug-recorder', '1') From 3e8060948c1fe9a6f7e7d15a990d55406709fbc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 15:16:59 +0800 Subject: [PATCH 09/26] fix: address recorder review follow-ups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-recorder.ts | 12 ++++++++++-- src/remodel-debug-settings.tsx | 5 ++++- src/reporters/remodel-recipe.ts | 1 + tests/remodel-debug-recorder.test.ts | 24 ++++++++++++++++++++++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index ba2393a..fe2424f 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -35,6 +35,7 @@ export interface RemodelDebugRecord { const records: RemodelDebugRecord[] = [] const listeners = new Set<() => void>() +let enabledCache: boolean | undefined const notifyListeners = (): void => { for (const listener of listeners) { @@ -50,14 +51,21 @@ const cloneJson = (value: unknown): unknown => { } export const isRemodelDebugRecorderEnabled = (): boolean => { + if (enabledCache != null) { + return enabledCache + } + try { - return window.localStorage?.getItem(ENABLED_KEY) === '1' + enabledCache = window.localStorage?.getItem(ENABLED_KEY) === '1' } catch { - return false + enabledCache = false } + + return enabledCache } export const setRemodelDebugRecorderEnabled = (enabled: boolean): void => { + enabledCache = enabled try { if (enabled) { window.localStorage?.setItem(ENABLED_KEY, '1') diff --git a/src/remodel-debug-settings.tsx b/src/remodel-debug-settings.tsx index 8814cad..dc5077a 100644 --- a/src/remodel-debug-settings.tsx +++ b/src/remodel-debug-settings.tsx @@ -28,7 +28,10 @@ const getRecorderState = () => ({ export default function RemodelDebugSettings(): ReactElement { const [recorderState, setRecorderState] = useState(getRecorderState) - useEffect(() => subscribeRemodelDebugRecorder(() => setRecorderState(getRecorderState())), []) + useEffect(() => { + const unsubscribe = subscribeRemodelDebugRecorder(() => setRecorderState(getRecorderState())) + return unsubscribe + }, []) const toggleRecorder = () => { setRemodelDebugRecorderEnabled(!isRemodelDebugRecorderEnabled()) diff --git a/src/reporters/remodel-recipe.ts b/src/reporters/remodel-recipe.ts index 275f5b6..51a4f45 100644 --- a/src/reporters/remodel-recipe.ts +++ b/src/reporters/remodel-recipe.ts @@ -276,6 +276,7 @@ export default class RemodelRecipeReporter extends BaseReporter { } getStage(level: number, changeFlag = 0) { if (changeFlag) { + // api_change_flag marks an update/conversion detail, which belongs to the +10 stage. return 2 } switch (true) { diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index 54ce35d..54b01eb 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -54,6 +54,7 @@ const remodelDetailEvent: GameResponseEventDetail = { beforeEach(() => { storage.clear() globalThis.window = createWindow() + setRemodelDebugRecorderEnabled(false) clearRemodelDebugRecords() }) @@ -85,7 +86,7 @@ describe('remodel debug recorder', () => { }) it('records only remodel APIs when enabled', () => { - window.localStorage.setItem('poi-plugin-report:remodel-debug-recorder', '1') + setRemodelDebugRecorderEnabled(true) recordRemodelDebugEvent({ ...remodelDetailEvent, @@ -96,6 +97,25 @@ describe('remodel debug recorder', () => { expect(getRemodelDebugRecords()).toHaveLength(1) }) + it('caches enabled state after the first storage read', () => { + const getItem = vi.fn(() => '1') + globalThis.window = { + ...createWindow(), + localStorage: { + getItem, + setItem: vi.fn(), + removeItem: vi.fn(), + }, + } as unknown as Window & typeof globalThis + setRemodelDebugRecorderEnabled(true) + + recordRemodelDebugEvent(remodelDetailEvent) + recordRemodelDebugEvent(remodelDetailEvent) + + expect(getItem).not.toHaveBeenCalled() + expect(getRemodelDebugRecords()).toHaveLength(2) + }) + it('sanitizes fleet and slot context for captured remodel records', () => { const record = createRemodelDebugRecord(remodelDetailEvent) @@ -163,7 +183,7 @@ describe('remodel debug recorder', () => { }) it('caps in-memory captures to the latest 200 records', () => { - window.localStorage.setItem('poi-plugin-report:remodel-debug-recorder', '1') + setRemodelDebugRecorderEnabled(true) for (let index = 0; index < 205; index += 1) { recordRemodelDebugEvent({ From 354d4a5cc8558abc4c2e25aaeb90c1ed2287b302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 16:13:29 +0800 Subject: [PATCH 10/26] fix: use decorators for recorder stories Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-settings.stories.tsx | 64 +++++++++++++++----------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/remodel-debug-settings.stories.tsx b/src/remodel-debug-settings.stories.tsx index 6589e78..606dfc9 100644 --- a/src/remodel-debug-settings.stories.tsx +++ b/src/remodel-debug-settings.stories.tsx @@ -7,11 +7,25 @@ import { setRemodelDebugRecorderEnabled, } from './remodel-debug-recorder' +type StoryRender = () => ReactElement + +const resetRecorder = (enabled: boolean): void => { + setRemodelDebugRecorderEnabled(enabled) + clearRemodelDebugRecords() +} + +const withRecorderState = + (setup: () => void) => + (Story: StoryRender): ReactElement => { + setup() + return + } + const meta = { title: 'Plugin/RemodelDebugSettings', component: RemodelDebugSettings, decorators: [ - (Story: () => ReactElement) => ( + (Story: StoryRender) => (
ReactElement> } export const Empty: Story = { - beforeEach() { - setRemodelDebugRecorderEnabled(false) - clearRemodelDebugRecords() - }, + decorators: [withRecorderState(() => resetRecorder(false))], } export const EnabledWithCapture: Story = { - beforeEach() { - setRemodelDebugRecorderEnabled(true) - clearRemodelDebugRecords() - recordRemodelDebugEvent({ - time: Date.UTC(2026, 6, 3, 15), - method: 'POST', - path: '/kcsapi/api_req_kousyou/remodel_slotlist_detail', - postBody: { - api_id: '33', - api_slot_id: '501', - api_token: 'redacted by sanitizer', - }, - body: { - api_req_buildkit: 3, - api_req_remodelkit: 4, - api_certain_buildkit: 5, - api_certain_remodelkit: 6, - }, - }) - }, + decorators: [ + withRecorderState(() => { + resetRecorder(true) + recordRemodelDebugEvent({ + time: Date.UTC(2026, 6, 3, 15), + method: 'POST', + path: '/kcsapi/api_req_kousyou/remodel_slotlist_detail', + postBody: { + api_id: '33', + api_slot_id: '501', + api_token: 'redacted by sanitizer', + }, + body: { + api_req_buildkit: 3, + api_req_remodelkit: 4, + api_certain_buildkit: 5, + api_certain_remodelkit: 6, + }, + }) + }), + ], } From 6ee19ebfe694fc362645f35d6b847a617d164613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 16:39:14 +0800 Subject: [PATCH 11/26] fix: align react bundling with plugin config Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/smoke-load.cjs | 19 +++++++++++++++++++ src/remodel-debug-recorder.ts | 2 +- tests/remodel-debug-recorder.test.ts | 10 ++++++++++ tsdown.config.ts | 5 ++++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/scripts/smoke-load.cjs b/scripts/smoke-load.cjs index 798a95d..de46ab6 100644 --- a/scripts/smoke-load.cjs +++ b/scripts/smoke-load.cjs @@ -100,6 +100,22 @@ const reactStub = { }, } +const reactJsxRuntimeStub = { + Fragment: Symbol.for('react.fragment'), + jsx(type, props) { + return { + type, + props, + } + }, + jsxs(type, props) { + return { + type, + props, + } + }, +} + const lodashImplementations = { compact(values) { return values.filter(Boolean) @@ -230,6 +246,9 @@ Module._load = function smokeLoad(request, parent, isMain) { return fetchStub case 'react': return reactStub + case 'react/jsx-runtime': + case 'react/jsx-dev-runtime': + return reactJsxRuntimeStub case 'lodash': return lodashStub case 'moment-timezone': diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index fe2424f..9cdae99 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -305,7 +305,7 @@ export const clearRemodelDebugRecords = (): void => { notifyListeners() } -export const getRemodelDebugRecords = (): readonly RemodelDebugRecord[] => records +export const getRemodelDebugRecords = (): readonly RemodelDebugRecord[] => [...records] export const exportRemodelDebugRecords = (): void => { const blob = new Blob([JSON.stringify({ records }, null, 2)], { diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index 54b01eb..d12a3ef 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -196,6 +196,16 @@ describe('remodel debug recorder', () => { expect(getRemodelDebugRecords()[0]?.time).toBe(5) }) + it('returns a copy of captured records', () => { + setRemodelDebugRecorderEnabled(true) + recordRemodelDebugEvent(remodelDetailEvent) + + const snapshot = getRemodelDebugRecords() as Array + snapshot.length = 0 + + expect(getRemodelDebugRecords()).toHaveLength(1) + }) + it('notifies subscribers when setting or records change', () => { const listener = vi.fn() const unsubscribe = subscribeRemodelDebugRecorder(listener) diff --git a/tsdown.config.ts b/tsdown.config.ts index 4339e63..8edfffc 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -22,7 +22,6 @@ module.exports = defineConfig({ outExtensions: () => ({ js: '.js' }), format: ['cjs'], deps: { - onlyBundle: ['react'], neverBundle: [ '@electron/remote', '@sentry/electron', @@ -30,6 +29,10 @@ module.exports = defineConfig({ 'lodash', 'moment-timezone', 'node-fetch', + 'react', + 'react-dom', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', 'semver', /^views\//, ], From 53f22a99b0009850712afa9181631c15985ffa5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 17:02:12 +0800 Subject: [PATCH 12/26] fix: address final recorder review nits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-recorder.ts | 2 +- src/reporters/remodel-recipe.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index 9cdae99..c9e61b0 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -318,5 +318,5 @@ export const exportRemodelDebugRecords = (): void => { .toISOString() .replace(/[:.]/g, '-')}.json` anchor.click() - setTimeout(() => URL.revokeObjectURL(href), 0) + setTimeout(() => URL.revokeObjectURL(href), 1000) } diff --git a/src/reporters/remodel-recipe.ts b/src/reporters/remodel-recipe.ts index 51a4f45..3327448 100644 --- a/src/reporters/remodel-recipe.ts +++ b/src/reporters/remodel-recipe.ts @@ -11,6 +11,7 @@ import type { GameApiPostBody, GameApiResponseBody, } from '../types/game-api' +import { parseInt10 } from '../types/window-state' type RemodelRecipeListItem = Pick< APIReqKousyouRemodelSlotlistResponse, @@ -149,8 +150,6 @@ const ITEM_IMPROVEMENT_RECIPE_REPORT_PATH = '/api/report/v3/item_improvement_rec const hasOwn = (record: object, key: string): boolean => Object.prototype.hasOwnProperty.call(record, key) -const parseInt10 = (value: string | number): number => parseInt(String(value), 10) - const getJstDay = (time: number): number => { const date = new Date(time) const utcDay = date.getUTCDay() From 574ceb4bb3600b1227450833282f08cbc9d3e5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 17:33:28 +0800 Subject: [PATCH 13/26] build: bundle reporters into plugin output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package.json | 5 +- scripts/smoke-load.cjs | 28 +-- src/remodel-debug-recorder.ts | 2 + src/reporters/base.ts | 9 +- src/types/vendor.d.ts | 2 + tests/helpers/reporter-test-harness.ts | 271 ++++++++++++++----------- tests/remodel-debug-recorder.test.ts | 114 +++++++++++ tests/reporter-utils.test.ts | 20 +- tsdown.config.ts | 20 +- vitest.config.mjs | 4 +- 10 files changed, 300 insertions(+), 175 deletions(-) diff --git a/package.json b/package.json index ca10899..f8dfe7d 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,8 @@ "main": "index.js", "packageManager": "pnpm@10.28.0", "files": [ - "chunks", "i18n", "index.js", - "reporters", - "sentry.js", "package.json", "README.md", "LICENSE" @@ -18,7 +15,7 @@ "build": "tsdown", "lint": "eslint .", "prepack": "tsdown", - "postpublish": "git clean -fd -- index.js sentry.js reporters chunks && git checkout .", + "postpublish": "git clean -fd -- index.js && git checkout .", "smoke": "node scripts/smoke-load.cjs", "storybook": "storybook dev -p 6006", "storybook:build": "storybook build", diff --git a/scripts/smoke-load.cjs b/scripts/smoke-load.cjs index de46ab6..6824735 100644 --- a/scripts/smoke-load.cjs +++ b/scripts/smoke-load.cjs @@ -5,16 +5,13 @@ const path = require('path') const projectRoot = path.resolve(__dirname, '..') const entryPath = path.join(projectRoot, 'index.js') -const baseReporterPath = path.join(projectRoot, 'reporters', 'base.js') if (!fs.existsSync(entryPath)) { console.log('[smoke-load] index.js not found; skipping until built output exists.') process.exit(0) } -const packageMeta = require(path.join(projectRoot, 'package.json')) const listeners = new Map() -const fetchCalls = [] const startupFetchCalls = [] global.window = { @@ -203,8 +200,9 @@ const lodashStub = new Proxy(lodashImplementations, { }) const fetchStub = async (requestUrl, options = {}) => { - const calls = requestUrl.endsWith('/api/report/v3/known_quests') ? startupFetchCalls : fetchCalls - calls.push({ requestUrl, options }) + if (requestUrl.endsWith('/api/report/v3/known_quests')) { + startupFetchCalls.push({ requestUrl, options }) + } return { ok: true, status: 200, @@ -268,8 +266,6 @@ Module._load = function smokeLoad(request, parent, isMain) { } } -const loadExport = (mod) => mod && (mod.default || mod) - async function main() { try { const plugin = require(entryPath) @@ -292,23 +288,7 @@ async function main() { 'https://example.invalid/api/report/v3/known_quests', ) - const BaseReporter = loadExport(require(baseReporterPath)) - const reporter = new BaseReporter() - assert.strictEqual(reporter.SERVER_HOSTNAME, 'example.invalid') - assert.strictEqual(reporter.USERAGENT, `Reporter/${packageMeta.version} poi/10.7.0`) - - const json = await reporter.getJson('/api/smoke') - assert.deepStrictEqual(json, { ok: true }) - assert.strictEqual(fetchCalls[0].requestUrl, 'https://example.invalid/api/smoke') - assert.strictEqual(fetchCalls[0].options.headers['User-Agent'], reporter.USERAGENT) - assert.strictEqual(fetchCalls[0].options.headers['X-Reporter'], reporter.USERAGENT) - - await reporter.report('/api/report/smoke', { ok: true }) - assert.strictEqual(fetchCalls[1].requestUrl, 'https://example.invalid/api/report/smoke') - assert.strictEqual(fetchCalls[1].options.method, 'POST') - assert.deepStrictEqual(JSON.parse(fetchCalls[1].options.body), { data: { ok: true } }) - - console.log('[smoke-load] built plugin loaded and reporter base exercised.') + console.log('[smoke-load] built plugin loaded.') } finally { Module._load = originalLoad } diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index c9e61b0..28159c3 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -203,6 +203,7 @@ const sanitizeBody = (path: string, body: unknown): unknown => { return sanitizeDetailBody(body) case '/kcsapi/api_req_kousyou/remodel_slot': return sanitizeSlotBody(body) + /* c8 ignore next 2 -- createRemodelDebugRecord filters paths before sanitizing. */ default: return {} } @@ -286,6 +287,7 @@ export const recordRemodelDebugEvent = (event: GameResponseEventDetail): void => try { const record = createRemodelDebugRecord(event) + /* c8 ignore next 3 -- createRemodelDebugRecord can only return undefined for paths filtered above. */ if (!record) { return } diff --git a/src/reporters/base.ts b/src/reporters/base.ts index 1f4eb0f..e4e8ab8 100644 --- a/src/reporters/base.ts +++ b/src/reporters/base.ts @@ -2,7 +2,6 @@ import url from 'url' import * as Sentry from '@sentry/electron' import fetch, { type RequestInit } from 'node-fetch' import https from 'https' -import packageMeta from '../../package.json' import type { ReportPayload } from '../types/reporter' // Because let's encrypt has switched to a new root cert which is not supported in older version of Electron, @@ -12,6 +11,8 @@ const insecureAgent = new https.Agent({ }) const { SERVER_HOSTNAME, POI_VERSION } = window +const REPORTER_VERSION = + typeof __REPORTER_VERSION__ === 'string' ? __REPORTER_VERSION__ : '0.0.0-dev' export default class BaseReporter { SERVER_HOSTNAME: string @@ -19,7 +20,7 @@ export default class BaseReporter { constructor() { this.SERVER_HOSTNAME = SERVER_HOSTNAME - this.USERAGENT = `Reporter/${packageMeta.version} poi/${POI_VERSION}` + this.USERAGENT = `Reporter/${REPORTER_VERSION} poi/${POI_VERSION}` } getJson = async (path: string): Promise> => { @@ -42,7 +43,7 @@ export default class BaseReporter { path, }) Sentry.setContext('versions', { - reporter: packageMeta.version, + reporter: REPORTER_VERSION, poi: POI_VERSION, }) Sentry.captureException(err) @@ -79,7 +80,7 @@ export default class BaseReporter { path, }) Sentry.setContext('versions', { - reporter: packageMeta.version, + reporter: REPORTER_VERSION, poi: POI_VERSION, }) Sentry.setContext('data', info) diff --git a/src/types/vendor.d.ts b/src/types/vendor.d.ts index 1aa5c38..c0a86ec 100644 --- a/src/types/vendor.d.ts +++ b/src/types/vendor.d.ts @@ -20,6 +20,8 @@ declare module 'electron' { export = electron } +declare const __REPORTER_VERSION__: string | undefined + declare module 'moment-timezone' { interface Moment { day(): number diff --git a/tests/helpers/reporter-test-harness.ts b/tests/helpers/reporter-test-harness.ts index 5c22c9f..2b8c9a0 100644 --- a/tests/helpers/reporter-test-harness.ts +++ b/tests/helpers/reporter-test-harness.ts @@ -1,49 +1,164 @@ import { createHash } from 'node:crypto' -import Module, { createRequire } from 'node:module' +import Module from 'node:module' import { vi } from 'vitest' import type { WindowSlotItem } from '../../src/types/window-state' +import SourceAACIReporter from '../../src/reporters/aaci' +import SourceBaseReporter from '../../src/reporters/base' +import SourceCreateItemReporter from '../../src/reporters/create-item' +import SourceCreateShipReporter from '../../src/reporters/create-ship' +import SourceDropShipReporter from '../../src/reporters/drop-ship' +import SourceNightBattleCIReporter from '../../src/reporters/night-battle-ci' +import SourceNightContactReportor from '../../src/reporters/night-contact' +import SourceQuestReporter from '../../src/reporters/quest' +import SourceRemodelItemReporter from '../../src/reporters/remodel-item' +import SourceRemodelRecipeReporter from '../../src/reporters/remodel-recipe' +import SourceShipStatReporter from '../../src/reporters/ship-stat' + +const state = vi.hoisted(() => { + const ship = (overrides: Record = {}): any => ({ + api_ship_id: 100, + api_lv: 50, + api_cond: 49, + api_slot: [], + api_onslot: [], + ...overrides, + }) + + const selectorState = { + store: {} as any, + ships: new Map(), + equips: new Map(), + } + + const aaciState: { getShipAACIs: any } = { + getShipAACIs: vi.fn(() => [] as number[]), + } + + const momentState = { + hour: 16, + day: 2, + } + + const fetchState = { + calls: [] as any[][], + implementation: async (..._args: any[]): Promise => ({ + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({}), + text: async () => '', + }), + } + + const sentryState = { + captured: [] as unknown[], + contexts: [] as Array<{ name: string; context: unknown }>, + tags: [] as unknown[], + } + + const createWindow = () => + ({ + POI_VERSION: '10.7.0', + SERVER_HOSTNAME: 'example.invalid', + _decks: [{ api_ship: [1, 2] }], + _ships: { + 1: ship({ api_ship_id: 101, api_lv: 80, api_cond: 53 }), + 2: ship({ api_ship_id: 102, api_lv: 70, api_cond: 49 }), + }, + $ships: { + 101: { api_id: 101, api_yomi: 'alpha' }, + 102: { api_id: 102, api_yomi: 'bravo' }, + }, + _slotitems: {}, + _teitokuId: 12345, + _teitokuLv: 120, + _nickName: 'Admiral', + _nickNameId: 99, + getStore: () => selectorState.store, + }) as unknown as Window & typeof globalThis + + globalThis.window = createWindow() + ;(globalThis as unknown as { __REPORTER_VERSION__: string }).__REPORTER_VERSION__ = '8.1.0' + ;(globalThis as any).__reporterTestHarnessState = { + aaciState, + selectorState, + } + + return { + aaciState, + createWindow, + fetchState, + momentState, + selectorState, + sentryState, + } +}) + +export const { aaciState, fetchState, momentState, selectorState, sentryState } = state declare global { var __reporterTestHarnessPatched: boolean | undefined } -const require = createRequire(__filename) type ModuleWithLoad = typeof Module & { _load(request: string, parent: NodeJS.Module | null | undefined, isMain: boolean): unknown } + const moduleWithLoad = Module as ModuleWithLoad const originalLoad = moduleWithLoad._load -export const selectorState = { - store: {} as any, - ships: new Map(), - equips: new Map(), -} +vi.mock('@sentry/electron', () => ({ + captureException(error: unknown) { + sentryState.captured.push(error) + }, + setContext(name: string, context: unknown) { + sentryState.contexts.push({ name, context }) + }, + withScope(callback: (scope: { setTags(tags: unknown): void }) => void) { + callback({ + setTags(tags: unknown) { + sentryState.tags.push(tags) + }, + }) + }, +})) -export const aaciState: { getShipAACIs: any } = { - getShipAACIs: vi.fn(() => [] as number[]), -} +vi.mock('node-fetch', () => ({ + default: async (...args: any[]) => { + fetchState.calls.push(args) + return fetchState.implementation(...args) + }, +})) + +vi.mock('moment-timezone', () => ({ + default: { + utc: () => ({ + hour: () => momentState.hour, + day: () => momentState.day, + }), + }, +})) -export const momentState = { - hour: 16, - day: 2, -} +vi.mock('views/utils/selectors', () => ({ + shipDataSelectorFactory: (shipId: number) => () => selectorState.ships.get(shipId), + shipEquipDataSelectorFactory: (shipId: number) => () => selectorState.equips.get(shipId), +})) -export const fetchState = { - calls: [] as any[][], - implementation: async (..._args: any[]): Promise => ({ - ok: true, - status: 200, - statusText: 'OK', - json: async () => ({}), - text: async () => '', - }), -} +vi.mock('views/utils/aaci', () => aaciState) -export const sentryState = { - captured: [] as unknown[], - contexts: [] as Array<{ name: string; context: unknown }>, - tags: [] as unknown[], +if (!globalThis.__reporterTestHarnessPatched) { + moduleWithLoad._load = function loadReporterTestStub( + request: string, + parent: NodeJS.Module | null | undefined, + isMain: boolean, + ) { + if (request === 'views/utils/aaci') { + return aaciState + } + + return originalLoad.call(this, request, parent, isMain) + } + globalThis.__reporterTestHarnessPatched = true } export const ship = (overrides: Record = {}): any => ({ @@ -68,96 +183,20 @@ export const equip = ({ api_houm: houm, }) -const createWindow = () => - ({ - POI_VERSION: '10.7.0', - SERVER_HOSTNAME: 'example.invalid', - _decks: [{ api_ship: [1, 2] }], - _ships: { - 1: ship({ api_ship_id: 101, api_lv: 80, api_cond: 53 }), - 2: ship({ api_ship_id: 102, api_lv: 70, api_cond: 49 }), - }, - $ships: { - 101: { api_id: 101, api_yomi: 'alpha' }, - 102: { api_id: 102, api_yomi: 'bravo' }, - }, - _slotitems: {}, - _teitokuId: 12345, - _teitokuLv: 120, - _nickName: 'Admiral', - _nickNameId: 99, - getStore: () => selectorState.store, - }) as unknown as Window & typeof globalThis - -globalThis.window = createWindow() - -const sentryStub = { - captureException(error: unknown) { - sentryState.captured.push(error) - }, - setContext(name: string, context: unknown) { - sentryState.contexts.push({ name, context }) - }, - withScope(callback: (scope: { setTags(tags: unknown): void }) => void) { - callback({ - setTags(tags: unknown) { - sentryState.tags.push(tags) - }, - }) - }, -} - -if (!globalThis.__reporterTestHarnessPatched) { - moduleWithLoad._load = function loadReporterTestStub( - request: string, - parent: NodeJS.Module | null | undefined, - isMain: boolean, - ) { - switch (request) { - case '@sentry/electron': - return sentryStub - case 'node-fetch': - return async (...args: any[]) => { - fetchState.calls.push(args) - return fetchState.implementation(...args) - } - case 'moment-timezone': - return { - utc: () => ({ - hour: () => momentState.hour, - day: () => momentState.day, - }), - } - case 'views/utils/selectors': - return { - shipDataSelectorFactory: (shipId: number) => () => selectorState.ships.get(shipId), - shipEquipDataSelectorFactory: (shipId: number) => () => selectorState.equips.get(shipId), - } - case 'views/utils/aaci': - return aaciState - default: - return originalLoad.call(this, request, parent, isMain) - } - } - globalThis.__reporterTestHarnessPatched = true -} - -const loadDefault = (mod: any) => mod.default || mod - -export const AACIReporter = loadDefault(require('../../reporters/aaci.js')) -export const BaseReporter = loadDefault(require('../../reporters/base.js')) -export const CreateItemReporter = loadDefault(require('../../reporters/create-item.js')) -export const CreateShipReporter = loadDefault(require('../../reporters/create-ship.js')) -export const DropShipReporter = loadDefault(require('../../reporters/drop-ship.js')) -export const NightBattleCIReporter = loadDefault(require('../../reporters/night-battle-ci.js')) -export const NightContactReportor = loadDefault(require('../../reporters/night-contact.js')) -export const QuestReporter = loadDefault(require('../../reporters/quest.js')) -export const RemodelItemReporter = loadDefault(require('../../reporters/remodel-item.js')) -export const RemodelRecipeReporter = loadDefault(require('../../reporters/remodel-recipe.js')) -export const ShipStatReporter = loadDefault(require('../../reporters/ship-stat.js')) +export const AACIReporter: any = SourceAACIReporter +export const BaseReporter: any = SourceBaseReporter +export const CreateItemReporter: any = SourceCreateItemReporter +export const CreateShipReporter: any = SourceCreateShipReporter +export const DropShipReporter: any = SourceDropShipReporter +export const NightBattleCIReporter: any = SourceNightBattleCIReporter +export const NightContactReportor: any = SourceNightContactReportor +export const QuestReporter: any = SourceQuestReporter +export const RemodelItemReporter: any = SourceRemodelItemReporter +export const RemodelRecipeReporter: any = SourceRemodelRecipeReporter +export const ShipStatReporter: any = SourceShipStatReporter export const resetReporterTestState = () => { - globalThis.window = createWindow() + globalThis.window = state.createWindow() selectorState.store = { sortie: { sortieMapId: 1 }, battle: { _status: { result: { deckHp: [] } } }, diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index d12a3ef..6d306fe 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -7,6 +7,7 @@ import { recordRemodelDebugEvent, setRemodelDebugRecorderEnabled, subscribeRemodelDebugRecorder, + exportRemodelDebugRecords, } from '../src/remodel-debug-recorder' import type { GameResponseEventDetail } from '../src/types/game-api' @@ -85,6 +86,15 @@ describe('remodel debug recorder', () => { expect(getRemodelDebugRecords()).toHaveLength(0) }) + it('does not create records for unrelated APIs', () => { + expect( + createRemodelDebugRecord({ + ...remodelDetailEvent, + path: '/kcsapi/api_get_member/ship2', + }), + ).toBeUndefined() + }) + it('records only remodel APIs when enabled', () => { setRemodelDebugRecorderEnabled(true) @@ -176,12 +186,81 @@ describe('remodel debug recorder', () => { }) }) + it('sanitizes malformed list and slot response shapes', () => { + expect( + createRemodelDebugRecord({ + time: 1, + method: 'GET', + path: '/kcsapi/api_req_kousyou/remodel_slotlist', + postBody: null as unknown as GameResponseEventDetail['postBody'], + body: [ + { + api_id: 33, + api_slot_id: 700, + api_req_fuel: 10, + api_req_bull: 20, + api_req_steel: 30, + api_req_bauxite: 40, + api_token: 'secret-token', + }, + null, + ], + }), + ).toMatchObject({ + body: [ + { + api_id: 33, + api_slot_id: 700, + api_req_fuel: 10, + api_req_bull: 20, + api_req_steel: 30, + api_req_bauxite: 40, + }, + {}, + ], + postBody: {}, + }) + expect( + createRemodelDebugRecord({ + time: 2, + method: 'POST', + path: '/kcsapi/api_req_kousyou/remodel_slot', + postBody: { + api_id: '33', + }, + body: { + api_after_slot: null, + api_remodel_flag: 1, + api_remodel_id: ['bad', 701], + }, + }), + ).toMatchObject({ + body: { + api_after_slot: {}, + api_remodel_flag: 1, + api_remodel_id: [701], + }, + }) + }) + it('handles missing deck ship arrays while recording', () => { window._decks = [{} as (typeof window._decks)[number]] expect(() => recordRemodelDebugEvent(remodelDetailEvent)).not.toThrow() }) + it('logs and skips malformed circular captures', () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + window._decks = undefined as unknown as typeof window._decks + setRemodelDebugRecorderEnabled(true) + + recordRemodelDebugEvent(remodelDetailEvent) + + expect(getRemodelDebugRecords()).toHaveLength(0) + expect(consoleError).toHaveBeenCalled() + consoleError.mockRestore() + }) + it('caps in-memory captures to the latest 200 records', () => { setRemodelDebugRecorderEnabled(true) @@ -240,4 +319,39 @@ describe('remodel debug recorder', () => { expect(consoleError).toHaveBeenCalledTimes(2) consoleError.mockRestore() }) + + it('exports captures as a delayed-revoked JSON blob', () => { + vi.useFakeTimers() + setRemodelDebugRecorderEnabled(true) + recordRemodelDebugEvent(remodelDetailEvent) + const click = vi.fn() + const anchor = { + click, + download: '', + href: '', + } + const createElement = vi.fn(() => anchor as unknown as HTMLElement) + const originalDocument = globalThis.document + globalThis.document = { + createElement, + } as unknown as Document + const createObjectUrl = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:debug') + const revokeObjectUrl = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}) + + exportRemodelDebugRecords() + + expect(createElement).toHaveBeenCalledWith('a') + expect(createObjectUrl).toHaveBeenCalledWith(expect.any(Blob)) + expect(anchor.href).toBe('blob:debug') + expect(anchor.download).toMatch(/^plugin-report-remodel-debug-/) + expect(click).toHaveBeenCalled() + expect(revokeObjectUrl).not.toHaveBeenCalled() + vi.advanceTimersByTime(1000) + expect(revokeObjectUrl).toHaveBeenCalledWith('blob:debug') + + globalThis.document = originalDocument + createObjectUrl.mockRestore() + revokeObjectUrl.mockRestore() + vi.useRealTimers() + }) }) diff --git a/tests/reporter-utils.test.ts b/tests/reporter-utils.test.ts index 7a55690..e3ca392 100644 --- a/tests/reporter-utils.test.ts +++ b/tests/reporter-utils.test.ts @@ -1,20 +1,20 @@ -import { createRequire } from 'node:module' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' -globalThis.window = { - _teitokuId: 1, - _ships: {}, - $ships: {}, -} as unknown as Window & typeof globalThis +vi.hoisted(() => { + globalThis.window = { + _teitokuId: 1, + _ships: {}, + $ships: {}, + } as unknown as Window & typeof globalThis +}) -const require = createRequire(__filename) -const { +import { getFirstPlaneCounts, getHpStyle, getNightBattleCVCIType, getNightBattleDDCIType, getNightBattleSSCIType, -} = require('../reporters/utils.js') +} from '../src/reporters/utils' const equip = ({ id = 0, type2 = 0, type3 = 0, houm = 0 } = {}) => ({ api_slotitem_id: id, diff --git a/tsdown.config.ts b/tsdown.config.ts index 8edfffc..c71a5ea 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,26 +1,16 @@ const { defineConfig } = require('tsdown') +const packageMeta = require('./package.json') module.exports = defineConfig({ entry: { index: 'src/index.ts', - 'reporters/aaci': 'src/reporters/aaci.ts', - 'reporters/base': 'src/reporters/base.ts', - 'reporters/create-item': 'src/reporters/create-item.ts', - 'reporters/create-ship': 'src/reporters/create-ship.ts', - 'reporters/drop-ship': 'src/reporters/drop-ship.ts', - 'reporters/index': 'src/reporters/index.ts', - 'reporters/night-battle-ci': 'src/reporters/night-battle-ci.ts', - 'reporters/night-contact': 'src/reporters/night-contact.ts', - 'reporters/quest': 'src/reporters/quest.ts', - 'reporters/remodel-item': 'src/reporters/remodel-item.ts', - 'reporters/remodel-recipe': 'src/reporters/remodel-recipe.ts', - 'reporters/ship-stat': 'src/reporters/ship-stat.ts', - 'reporters/utils': 'src/reporters/utils.ts', - sentry: 'src/sentry.ts', }, outDir: '.', outExtensions: () => ({ js: '.js' }), format: ['cjs'], + define: { + __REPORTER_VERSION__: JSON.stringify(packageMeta.version), + }, deps: { neverBundle: [ '@electron/remote', @@ -38,7 +28,7 @@ module.exports = defineConfig({ ], }, dts: false, - clean: ['index.js', 'sentry.js', 'reporters', 'chunks'], + clean: ['index.js'], sourcemap: false, hash: false, outputOptions: { diff --git a/vitest.config.mjs b/vitest.config.mjs index 5a7c2e8..8dc5d66 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -5,8 +5,8 @@ export default defineConfig({ coverage: { provider: 'v8', reporter: ['text', 'json-summary'], - include: ['reporters/*.js', 'chunks/*.js'], - exclude: ['reporters/index.js', 'reporters/base.js', 'chunks/rolldown-runtime.js'], + include: ['src/reporters/**/*.ts', 'src/remodel-debug-recorder.ts'], + exclude: ['src/reporters/index.ts'], all: true, thresholds: { lines: 95, From 6b71da422eb2e0be05474365f61f95934b0337d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 17:36:39 +0800 Subject: [PATCH 14/26] fix: export recorder settings for poi Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/smoke-load.cjs | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/smoke-load.cjs b/scripts/smoke-load.cjs index 6824735..689369b 100644 --- a/scripts/smoke-load.cjs +++ b/scripts/smoke-load.cjs @@ -271,7 +271,7 @@ async function main() { const plugin = require(entryPath) assert.strictEqual(plugin.show, false) - assert.strictEqual(typeof plugin.settingClass, 'function') + assert.strictEqual(typeof plugin.settingsClass, 'function') assert.strictEqual(typeof plugin.pluginDidLoad, 'function') assert.strictEqual(typeof plugin.pluginWillUnload, 'function') diff --git a/src/index.ts b/src/index.ts index 589384a..b5eda1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,7 +61,7 @@ const handleResponse = (e: GameResponseEvent) => { } export const show = false -export const settingClass = RemodelDebugSettings +export const settingsClass = RemodelDebugSettings export const pluginDidLoad = (_e: unknown) => { reporters = [ new QuestReporter(), From 9c072ef73f8de5d06fc0de5b98b3d9c4aca433d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 18:05:13 +0800 Subject: [PATCH 15/26] feat: localize remodel recorder settings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- i18n/en-US.json | 6 ++++ i18n/ja-JP.json | 8 ++++- i18n/zh-CN.json | 8 ++++- i18n/zh-TW.json | 8 ++++- package.json | 1 + pnpm-lock.yaml | 55 ++++++++++++++++++++++++++++++++++ scripts/smoke-load.cjs | 7 +++++ src/remodel-debug-settings.tsx | 20 ++++++------- tsdown.config.ts | 1 + 9 files changed, 100 insertions(+), 14 deletions(-) diff --git a/i18n/en-US.json b/i18n/en-US.json index 2c63c08..18aae46 100644 --- a/i18n/en-US.json +++ b/i18n/en-US.json @@ -1,2 +1,8 @@ { + "Captured records": "Captured records: {{count}}", + "Clear": "Clear", + "Enable remodel debug recorder": "Enable remodel debug recorder", + "Export": "Export", + "Remodel recipe debug recorder": "Remodel recipe debug recorder", + "Remodel recipe debug recorder description": "Opt-in local recorder for validating Akashi remodel API sequences. It captures only allowlisted remodel fields, keeps records in memory, and writes a file only when Export is clicked." } diff --git a/i18n/ja-JP.json b/i18n/ja-JP.json index b9e25a2..dc92e0d 100644 --- a/i18n/ja-JP.json +++ b/i18n/ja-JP.json @@ -1,4 +1,10 @@ { "Data Report": "統計データベースツール", - "Report data to database(http://db.kcwiki.moe)": "統計データベース(http://db.kcwiki.moe)登録用ツール" + "Report data to database(http://db.kcwiki.moe)": "統計データベース(http://db.kcwiki.moe)登録用ツール", + "Captured records": "記録数: {{count}}", + "Clear": "クリア", + "Enable remodel debug recorder": "改修デバッグレコーダーを有効化", + "Export": "エクスポート", + "Remodel recipe debug recorder": "改修レシピデバッグレコーダー", + "Remodel recipe debug recorder description": "明石の改修APIシーケンス検証用のローカルレコーダーです。許可された改修フィールドだけをメモリ上に記録し、エクスポートをクリックしたときだけファイルへ書き出します。" } diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index abe8a64..bd02bff 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -1,4 +1,10 @@ { "Data Report": "数据汇报", - "Report data to database(http://db.kcwiki.moe)": "汇报建造数据、海域掉落数据、开发数据" + "Report data to database(http://db.kcwiki.moe)": "汇报建造数据、海域掉落数据、开发数据", + "Captured records": "已捕获记录:{{count}}", + "Clear": "清空", + "Enable remodel debug recorder": "启用改修调试记录器", + "Export": "导出", + "Remodel recipe debug recorder": "改修配方调试记录器", + "Remodel recipe debug recorder description": "用于验证明石改修 API 顺序的本地可选记录器。只捕获允许的改修字段,记录仅保存在内存中,并且只会在点击导出时写入文件。" } diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index 0174d33..93628b6 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -1,4 +1,10 @@ { "Data Report": "數據匯報", - "Report data to database(http://db.kcwiki.moe)": "匯報建造數據、海域掉落數據、開發數據" + "Report data to database(http://db.kcwiki.moe)": "匯報建造數據、海域掉落數據、開發數據", + "Captured records": "已擷取記錄:{{count}}", + "Clear": "清除", + "Enable remodel debug recorder": "啟用改修除錯記錄器", + "Export": "匯出", + "Remodel recipe debug recorder": "改修配方除錯記錄器", + "Remodel recipe debug recorder description": "用於驗證明石改修 API 順序的本機選用記錄器。只擷取允許的改修欄位,記錄只保存在記憶體中,且只會在點擊匯出時寫入檔案。" } diff --git a/package.json b/package.json index f8dfe7d..3669eed 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "prettier": "^3.9.4", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-i18next": "^17.0.8", "storybook": "^10.4.6", "tsdown": "^0.22.0", "typescript": "^5.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abd0abf..27273a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: react-dom: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) + react-i18next: + specifier: ^17.0.8 + version: 17.0.8(i18next@26.3.4(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3) storybook: specifier: ^10.4.6 version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7) @@ -2223,11 +2226,22 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + husky@3.1.0: resolution: {integrity: sha512-FJkPoHHB+6s4a+jwPqBudBDvYZsoQW5/HBuMSehC8qDiCe50kpcxeqFoDSlow+9I6wg47YxBoT3WxaURlrDIIQ==} engines: {node: '>=8.6.0'} hasBin: true + i18next@26.3.4: + resolution: {integrity: sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==} + peerDependencies: + typescript: ^5 || ^6 + peerDependenciesMeta: + typescript: + optional: true + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3062,6 +3076,22 @@ packages: peerDependencies: react: ^19.2.7 + react-i18next@17.0.8: + resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==} + peerDependencies: + i18next: '>= 26.2.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 || ^6 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -3726,6 +3756,10 @@ packages: jsdom: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -5923,6 +5957,10 @@ snapshots: html-escaper@2.0.2: {} + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + husky@3.1.0: dependencies: chalk: 2.4.2 @@ -5937,6 +5975,10 @@ snapshots: run-node: 1.0.0 slash: 3.0.0 + i18next@26.3.4(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -6793,6 +6835,17 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 + react-i18next@17.0.8(i18next@26.3.4(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.7 + html-parse-stringify: 3.0.1 + i18next: 26.3.4(typescript@5.9.3) + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + react-dom: 19.2.7(react@19.2.7) + typescript: 5.9.3 + react-is@17.0.2: {} react@19.2.7: {} @@ -7505,6 +7558,8 @@ snapshots: transitivePeerDependencies: - msw + void-elements@3.1.0: {} + webpack-virtual-modules@0.6.2: {} which-boxed-primitive@1.1.1: diff --git a/scripts/smoke-load.cjs b/scripts/smoke-load.cjs index 689369b..554c1c8 100644 --- a/scripts/smoke-load.cjs +++ b/scripts/smoke-load.cjs @@ -247,6 +247,13 @@ Module._load = function smokeLoad(request, parent, isMain) { case 'react/jsx-runtime': case 'react/jsx-dev-runtime': return reactJsxRuntimeStub + case 'react-i18next': + return { + useTranslation: () => ({ + t: (key, options) => + options && typeof options.count !== 'undefined' ? `${key}: ${options.count}` : key, + }), + } case 'lodash': return lodashStub case 'moment-timezone': diff --git a/src/remodel-debug-settings.tsx b/src/remodel-debug-settings.tsx index dc5077a..c6f74dd 100644 --- a/src/remodel-debug-settings.tsx +++ b/src/remodel-debug-settings.tsx @@ -1,4 +1,5 @@ import { type CSSProperties, type ReactElement, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { clearRemodelDebugRecords, exportRemodelDebugRecords, @@ -26,6 +27,7 @@ const getRecorderState = () => ({ }) export default function RemodelDebugSettings(): ReactElement { + const { t } = useTranslation('poi-plugin-report') const [recorderState, setRecorderState] = useState(getRecorderState) useEffect(() => { @@ -43,27 +45,23 @@ export default function RemodelDebugSettings(): ReactElement { return (
-

Remodel recipe debug recorder

-

- Opt-in local recorder for validating Akashi remodel API sequences. It captures only - allowlisted remodel fields, keeps records in memory, and writes a file only when Export is - clicked. -

+

{t('Remodel recipe debug recorder')}

+

{t('Remodel recipe debug recorder description')}

-

Captured records: {recorderState.count}

+

{t('Captured records', { count: recorderState.count })}

diff --git a/tsdown.config.ts b/tsdown.config.ts index c71a5ea..cd10376 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -23,6 +23,7 @@ module.exports = defineConfig({ 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime', + 'react-i18next', 'semver', /^views\//, ], From 1e1297322ffe9d57dcfca2968c3c20680f202d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 18:17:11 +0800 Subject: [PATCH 16/26] fix: harden remodel debug edge cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-recorder.ts | 2 +- src/reporters/remodel-recipe.ts | 6 +++-- tests/remodel-debug-recorder.test.ts | 16 ++++++++++++- tests/reporters/remodel-recipe.test.ts | 31 ++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index 28159c3..51e339f 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -210,7 +210,7 @@ const sanitizeBody = (path: string, body: unknown): unknown => { } const createSanitizedContext = (slotId: string | number | undefined): SanitizedFleetContext => { - const deckShipIds = window._decks[0]?.api_ship?.slice(0, 2) || [] + const deckShipIds = window._decks?.[0]?.api_ship?.slice(0, 2) || [] const flagship = deckShipIds[0] == null ? undefined : window._ships[deckShipIds[0]] const secondShip = deckShipIds[1] == null ? undefined : window._ships[deckShipIds[1]] if (slotId != null) { diff --git a/src/reporters/remodel-recipe.ts b/src/reporters/remodel-recipe.ts index 3327448..f864bd5 100644 --- a/src/reporters/remodel-recipe.ts +++ b/src/reporters/remodel-recipe.ts @@ -291,7 +291,7 @@ export default class RemodelRecipeReporter extends BaseReporter { } getFleetContext(): RemodelRecipeFleetContext | undefined { - const deck = window._decks[0] + const deck = window._decks?.[0] const flagshipRosterId = deck?.api_ship?.[0] if (flagshipRosterId == null || Number(flagshipRosterId) <= 0) { return undefined @@ -534,7 +534,9 @@ export default class RemodelRecipeReporter extends BaseReporter { const currentDetail = this.currentDetail if (this.itemId != response.api_remodel_id[0]) { - console.error(`Inconsistent remodel item data: ${this.itemId}, ${request.api_slot_id}`) + console.error( + `Inconsistent remodel item data: expected item ${this.itemId}, got ${response.api_remodel_id[0]}`, + ) this.resetExecutionState() return } diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index 6d306fe..a68d751 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -249,9 +249,23 @@ describe('remodel debug recorder', () => { expect(() => recordRemodelDebugEvent(remodelDetailEvent)).not.toThrow() }) + it('handles missing decks while creating sanitized records', () => { + window._decks = undefined as unknown as typeof window._decks + + expect(createRemodelDebugRecord(remodelDetailEvent)).toMatchObject({ + context: { + firstFleet: {}, + selectedSlotItem: { + api_slotitem_id: 700, + api_level: 6, + }, + }, + }) + }) + it('logs and skips malformed circular captures', () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - window._decks = undefined as unknown as typeof window._decks + window._slotitems = undefined as unknown as typeof window._slotitems setRemodelDebugRecorderEnabled(true) recordRemodelDebugEvent(remodelDetailEvent) diff --git a/tests/reporters/remodel-recipe.test.ts b/tests/reporters/remodel-recipe.test.ts index ac1d756..fa329d7 100644 --- a/tests/reporters/remodel-recipe.test.ts +++ b/tests/reporters/remodel-recipe.test.ts @@ -385,6 +385,37 @@ describe('RemodelRecipeReporter', () => { consoleError.mockRestore() }) + it('logs item id mismatches without mixing item and slot identifiers', () => { + window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } + const reporter = new RemodelRecipeReporter() + const report = attachReportSpy(reporter) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle('POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', detailBody, { + api_id: '33', + api_slot_id: 501, + }) + reporter.handle( + 'POST', + '/kcsapi/api_req_kousyou/remodel_slot', + { + api_remodel_flag: true, + api_remodel_id: [999, 701], + }, + { + api_id: '33', + api_slot_id: 501, + }, + ) + + expect(report).not.toHaveBeenCalledWith('/api/report/v2/remodel_recipe', expect.anything()) + expect(consoleError).toHaveBeenCalledWith( + 'Inconsistent remodel item data: expected item 700, got 999', + ) + consoleError.mockRestore() + }) + it('preserves v2 execution reporting when v3 fleet context is unavailable', () => { window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } window._decks = [] From 6fa7c91e7570fe9351e0a2dbf6e39718a759f3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 18:18:57 +0800 Subject: [PATCH 17/26] test: declare lodash for source tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package.json | 1 + pnpm-lock.yaml | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 3669eed..50200ff 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "husky": "^3.0.8", "kcsapi": "^1.260604.0", "lint-staged": "^9.4.2", + "lodash": "^4.18.1", "prettier": "^3.9.4", "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27273a2..a1f1200 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: lint-staged: specifier: ^9.4.2 version: 9.5.0 + lodash: + specifier: ^4.18.1 + version: 4.18.1 prettier: specifier: ^3.9.4 version: 3.9.4 @@ -4023,7 +4026,7 @@ snapshots: dependencies: conventional-changelog-angular: 1.6.6 conventional-commits-parser: 3.2.4 - lodash: 4.17.21 + lodash: 4.18.1 '@commitlint/read@8.3.6': dependencies: @@ -5250,7 +5253,7 @@ snapshots: dependencies: JSONStream: 1.3.5 is-text-path: 1.0.1 - lodash: 4.17.21 + lodash: 4.18.1 meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 @@ -5859,7 +5862,7 @@ snapshots: git-raw-commits@2.0.11: dependencies: dargs: 7.0.0 - lodash: 4.17.21 + lodash: 4.18.1 meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 From 9fbb3029741abc30fadef23939086b3b2e3f994b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 18:29:40 +0800 Subject: [PATCH 18/26] fix: deep copy recorder snapshots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-recorder.ts | 7 +++++-- tests/remodel-debug-recorder.test.ts | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index 51e339f..73e1be7 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -307,10 +307,13 @@ export const clearRemodelDebugRecords = (): void => { notifyListeners() } -export const getRemodelDebugRecords = (): readonly RemodelDebugRecord[] => [...records] +const cloneRecord = (record: RemodelDebugRecord): RemodelDebugRecord => + cloneJson(record) as RemodelDebugRecord + +export const getRemodelDebugRecords = (): readonly RemodelDebugRecord[] => records.map(cloneRecord) export const exportRemodelDebugRecords = (): void => { - const blob = new Blob([JSON.stringify({ records }, null, 2)], { + const blob = new Blob([JSON.stringify({ records: getRemodelDebugRecords() }, null, 2)], { type: 'application/json', }) const href = URL.createObjectURL(blob) diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index a68d751..daf0bb4 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -293,10 +293,18 @@ describe('remodel debug recorder', () => { setRemodelDebugRecorderEnabled(true) recordRemodelDebugEvent(remodelDetailEvent) - const snapshot = getRemodelDebugRecords() as Array + const snapshot = getRemodelDebugRecords() as unknown as Array<{ + context?: { + selectedSlotItem?: { + api_level?: number + } + } + }> + snapshot[0]!.context!.selectedSlotItem!.api_level = 99 snapshot.length = 0 expect(getRemodelDebugRecords()).toHaveLength(1) + expect(getRemodelDebugRecords()[0]?.context.selectedSlotItem?.api_level).toBe(6) }) it('notifies subscribers when setting or records change', () => { @@ -334,7 +342,7 @@ describe('remodel debug recorder', () => { consoleError.mockRestore() }) - it('exports captures as a delayed-revoked JSON blob', () => { + it('exports captures as a delayed-revoked JSON blob', async () => { vi.useFakeTimers() setRemodelDebugRecorderEnabled(true) recordRemodelDebugEvent(remodelDetailEvent) @@ -349,13 +357,18 @@ describe('remodel debug recorder', () => { globalThis.document = { createElement, } as unknown as Document - const createObjectUrl = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:debug') + let exportedBlob: Blob | undefined + const createObjectUrl = vi.spyOn(URL, 'createObjectURL').mockImplementation((blob) => { + exportedBlob = blob as Blob + return 'blob:debug' + }) const revokeObjectUrl = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}) exportRemodelDebugRecords() expect(createElement).toHaveBeenCalledWith('a') expect(createObjectUrl).toHaveBeenCalledWith(expect.any(Blob)) + await expect(exportedBlob?.text()).resolves.toContain('"records"') expect(anchor.href).toBe('blob:debug') expect(anchor.download).toMatch(/^plugin-report-remodel-debug-/) expect(click).toHaveBeenCalled() From 35d20d6b0ec546b1ecf61a38a6661c3081204b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 18:35:49 +0800 Subject: [PATCH 19/26] fix: reject placeholder remodel item ids Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/reporters/remodel-recipe.ts | 2 +- tests/reporters/remodel-recipe.test.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/reporters/remodel-recipe.ts b/src/reporters/remodel-recipe.ts index f864bd5..32a923d 100644 --- a/src/reporters/remodel-recipe.ts +++ b/src/reporters/remodel-recipe.ts @@ -437,7 +437,7 @@ export default class RemodelRecipeReporter extends BaseReporter { this.itemLevel = slotItem?.api_level ?? -1 const changeFlag = response.api_change_flag ?? 0 this.stage = this.getStage(this.itemLevel, changeFlag) - if (this.itemId < 0 || this.stage < 0) { + if (this.itemId <= 0 || this.stage < 0) { console.error('Invalid remodel recipe slot item data') return } diff --git a/tests/reporters/remodel-recipe.test.ts b/tests/reporters/remodel-recipe.test.ts index fa329d7..b0dcd97 100644 --- a/tests/reporters/remodel-recipe.test.ts +++ b/tests/reporters/remodel-recipe.test.ts @@ -354,6 +354,23 @@ describe('RemodelRecipeReporter', () => { consoleError.mockRestore() }) + it('rejects placeholder item ids in detail observations', () => { + window._slotitems[501] = { api_slotitem_id: 0, api_level: 6 } + const reporter = new RemodelRecipeReporter() + const report = attachReportSpy(reporter) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + + reporter.handle('GET', '/kcsapi/api_req_kousyou/remodel_slotlist', listBody) + reporter.handle('POST', '/kcsapi/api_req_kousyou/remodel_slotlist_detail', detailBody, { + api_id: '33', + api_slot_id: 501, + }) + + expect(report).toHaveBeenCalledTimes(1) + expect(consoleError).toHaveBeenCalledWith('Invalid remodel recipe slot item data') + consoleError.mockRestore() + }) + it('rejects execution when the request slot mismatches cached detail state', () => { window._slotitems[501] = { api_slotitem_id: 700, api_level: 6 } const reporter = new RemodelRecipeReporter() From a9dd98a00897668ac7a08298cfbe46070fdef1da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 18:43:42 +0800 Subject: [PATCH 20/26] perf: avoid cloning records for settings count Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-recorder.ts | 2 ++ src/remodel-debug-settings.tsx | 4 ++-- tests/remodel-debug-recorder.test.ts | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index 73e1be7..548fb5c 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -312,6 +312,8 @@ const cloneRecord = (record: RemodelDebugRecord): RemodelDebugRecord => export const getRemodelDebugRecords = (): readonly RemodelDebugRecord[] => records.map(cloneRecord) +export const getRemodelDebugRecordCount = (): number => records.length + export const exportRemodelDebugRecords = (): void => { const blob = new Blob([JSON.stringify({ records: getRemodelDebugRecords() }, null, 2)], { type: 'application/json', diff --git a/src/remodel-debug-settings.tsx b/src/remodel-debug-settings.tsx index c6f74dd..3edc266 100644 --- a/src/remodel-debug-settings.tsx +++ b/src/remodel-debug-settings.tsx @@ -2,8 +2,8 @@ import { type CSSProperties, type ReactElement, useEffect, useState } from 'reac import { useTranslation } from 'react-i18next' import { clearRemodelDebugRecords, + getRemodelDebugRecordCount, exportRemodelDebugRecords, - getRemodelDebugRecords, isRemodelDebugRecorderEnabled, setRemodelDebugRecorderEnabled, subscribeRemodelDebugRecorder, @@ -23,7 +23,7 @@ const buttonRowStyle: CSSProperties = { const getRecorderState = () => ({ enabled: isRemodelDebugRecorderEnabled(), - count: getRemodelDebugRecords().length, + count: getRemodelDebugRecordCount(), }) export default function RemodelDebugSettings(): ReactElement { diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index daf0bb4..2ab8205 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { clearRemodelDebugRecords, createRemodelDebugRecord, + getRemodelDebugRecordCount, getRemodelDebugRecords, recordRemodelDebugEvent, setRemodelDebugRecorderEnabled, @@ -304,6 +305,7 @@ describe('remodel debug recorder', () => { snapshot.length = 0 expect(getRemodelDebugRecords()).toHaveLength(1) + expect(getRemodelDebugRecordCount()).toBe(1) expect(getRemodelDebugRecords()[0]?.context.selectedSlotItem?.api_level).toBe(6) }) From 12df2143d7662f30373c982ca0fd24d0e010d98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 19:06:53 +0800 Subject: [PATCH 21/26] fix: harden recorder notifications and stories Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-recorder.ts | 6 +++++- src/remodel-debug-settings.stories.tsx | 15 +++++++++++++++ tests/remodel-debug-recorder.test.ts | 17 +++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index 548fb5c..b3ea0b3 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -39,7 +39,11 @@ let enabledCache: boolean | undefined const notifyListeners = (): void => { for (const listener of listeners) { - listener() + try { + listener() + } catch (err) { + console.error(err) + } } } diff --git a/src/remodel-debug-settings.stories.tsx b/src/remodel-debug-settings.stories.tsx index 606dfc9..52a1b15 100644 --- a/src/remodel-debug-settings.stories.tsx +++ b/src/remodel-debug-settings.stories.tsx @@ -14,6 +14,20 @@ const resetRecorder = (enabled: boolean): void => { clearRemodelDebugRecords() } +const setupPoiGlobals = (): void => { + window._decks = [{ api_ship: [1, 2] }] + window._ships = { + 1: { api_ship_id: 187 }, + 2: { api_ship_id: 141 }, + } + window._slotitems = { + 501: { + api_level: 6, + api_slotitem_id: 39, + }, + } +} + const withRecorderState = (setup: () => void) => (Story: StoryRender): ReactElement => { @@ -51,6 +65,7 @@ export const Empty: Story = { export const EnabledWithCapture: Story = { decorators: [ withRecorderState(() => { + setupPoiGlobals() resetRecorder(true) recordRemodelDebugEvent({ time: Date.UTC(2026, 6, 3, 15), diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index 2ab8205..f400c3d 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -323,6 +323,23 @@ describe('remodel debug recorder', () => { expect(listener).toHaveBeenCalledTimes(3) }) + it('isolates listener failures during notifications', () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + const healthyListener = vi.fn() + const unsubscribeBad = subscribeRemodelDebugRecorder(() => { + throw new Error('listener failed') + }) + const unsubscribeHealthy = subscribeRemodelDebugRecorder(healthyListener) + + expect(() => setRemodelDebugRecorderEnabled(true)).not.toThrow() + + expect(consoleError).toHaveBeenCalledWith(expect.any(Error)) + expect(healthyListener).toHaveBeenCalled() + unsubscribeBad() + unsubscribeHealthy() + consoleError.mockRestore() + }) + it('does not throw when localStorage writes fail', () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) globalThis.window = { From 55e07e47f9a140d0ef2f075795fb23bd342d53f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 19:46:46 +0800 Subject: [PATCH 22/26] fix: read reporter version in unbundled tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/reporters/base.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/reporters/base.ts b/src/reporters/base.ts index e4e8ab8..bdcb4a4 100644 --- a/src/reporters/base.ts +++ b/src/reporters/base.ts @@ -11,8 +11,11 @@ const insecureAgent = new https.Agent({ }) const { SERVER_HOSTNAME, POI_VERSION } = window +const globalReporterVersion = (globalThis as { __REPORTER_VERSION__?: string }).__REPORTER_VERSION__ const REPORTER_VERSION = - typeof __REPORTER_VERSION__ === 'string' ? __REPORTER_VERSION__ : '0.0.0-dev' + typeof __REPORTER_VERSION__ === 'string' + ? __REPORTER_VERSION__ + : (globalReporterVersion ?? '0.0.0-dev') export default class BaseReporter { SERVER_HOSTNAME: string From f459a891802cd1b3553b043447ed5ecc49465c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 21:13:05 +0800 Subject: [PATCH 23/26] fix: lazy load reporter globals Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/reporters/base.ts | 24 +++++++++++++++--------- tests/reporter-utils.test.ts | 26 ++++++++++++++++---------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/reporters/base.ts b/src/reporters/base.ts index bdcb4a4..fd3ec85 100644 --- a/src/reporters/base.ts +++ b/src/reporters/base.ts @@ -10,20 +10,26 @@ const insecureAgent = new https.Agent({ rejectUnauthorized: false, }) -const { SERVER_HOSTNAME, POI_VERSION } = window -const globalReporterVersion = (globalThis as { __REPORTER_VERSION__?: string }).__REPORTER_VERSION__ -const REPORTER_VERSION = - typeof __REPORTER_VERSION__ === 'string' +const getReporterVersion = (): string => { + const globalReporterVersion = (globalThis as { __REPORTER_VERSION__?: string }) + .__REPORTER_VERSION__ + return typeof __REPORTER_VERSION__ === 'string' ? __REPORTER_VERSION__ : (globalReporterVersion ?? '0.0.0-dev') +} export default class BaseReporter { SERVER_HOSTNAME: string USERAGENT: string + POI_VERSION: string + REPORTER_VERSION: string constructor() { + const { SERVER_HOSTNAME, POI_VERSION } = globalThis.window this.SERVER_HOSTNAME = SERVER_HOSTNAME - this.USERAGENT = `Reporter/${REPORTER_VERSION} poi/${POI_VERSION}` + this.POI_VERSION = POI_VERSION + this.REPORTER_VERSION = getReporterVersion() + this.USERAGENT = `Reporter/${this.REPORTER_VERSION} poi/${this.POI_VERSION}` } getJson = async (path: string): Promise> => { @@ -46,8 +52,8 @@ export default class BaseReporter { path, }) Sentry.setContext('versions', { - reporter: REPORTER_VERSION, - poi: POI_VERSION, + reporter: this.REPORTER_VERSION, + poi: this.POI_VERSION, }) Sentry.captureException(err) }) @@ -83,8 +89,8 @@ export default class BaseReporter { path, }) Sentry.setContext('versions', { - reporter: REPORTER_VERSION, - poi: POI_VERSION, + reporter: this.REPORTER_VERSION, + poi: this.POI_VERSION, }) Sentry.setContext('data', info) Sentry.captureException(err) diff --git a/tests/reporter-utils.test.ts b/tests/reporter-utils.test.ts index e3ca392..23a7e7b 100644 --- a/tests/reporter-utils.test.ts +++ b/tests/reporter-utils.test.ts @@ -1,21 +1,27 @@ -import { describe, expect, it, vi } from 'vitest' +import { beforeAll, describe, expect, it } from 'vitest' +import type * as ReporterUtils from '../src/reporters/utils' -vi.hoisted(() => { +let getFirstPlaneCounts: typeof ReporterUtils.getFirstPlaneCounts +let getHpStyle: typeof ReporterUtils.getHpStyle +let getNightBattleCVCIType: typeof ReporterUtils.getNightBattleCVCIType +let getNightBattleDDCIType: typeof ReporterUtils.getNightBattleDDCIType +let getNightBattleSSCIType: typeof ReporterUtils.getNightBattleSSCIType + +beforeAll(async () => { globalThis.window = { _teitokuId: 1, _ships: {}, $ships: {}, } as unknown as Window & typeof globalThis + ;({ + getFirstPlaneCounts, + getHpStyle, + getNightBattleCVCIType, + getNightBattleDDCIType, + getNightBattleSSCIType, + } = await import('../src/reporters/utils')) }) -import { - getFirstPlaneCounts, - getHpStyle, - getNightBattleCVCIType, - getNightBattleDDCIType, - getNightBattleSSCIType, -} from '../src/reporters/utils' - const equip = ({ id = 0, type2 = 0, type3 = 0, houm = 0 } = {}) => ({ api_slotitem_id: id, api_type: [0, 0, type2, type3], From 5db9c784ba2536628936f534a086bd32394ef5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 21:35:53 +0800 Subject: [PATCH 24/26] build: remove stale chunk output config Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 1 - eslint.config.mjs | 1 - tsdown.config.ts | 3 --- 3 files changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index de75306..b4d09c0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,6 @@ build/Release node_modules # Generated plugin build output -chunks/*.js index.js reporters/*.js sentry.js diff --git a/eslint.config.mjs b/eslint.config.mjs index cc36af5..14ff4e1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,7 +9,6 @@ import tseslint from 'typescript-eslint' export default tseslint.config( { ignores: [ - 'chunks/**', 'coverage/**', 'index.js', 'node_modules/**', diff --git a/tsdown.config.ts b/tsdown.config.ts index cd10376..f3857d0 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -32,9 +32,6 @@ module.exports = defineConfig({ clean: ['index.js'], sourcemap: false, hash: false, - outputOptions: { - chunkFileNames: 'chunks/[name].js', - }, treeshake: false, minify: false, shims: false, From 355dc47fa40fac8ba7b6f1468cde5b4d9d44d098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 21:59:46 +0800 Subject: [PATCH 25/26] fix: keep debug captures with partial context Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/remodel-debug-recorder.ts | 6 +++--- tests/remodel-debug-recorder.test.ts | 29 +++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/remodel-debug-recorder.ts b/src/remodel-debug-recorder.ts index b3ea0b3..9f5ee23 100644 --- a/src/remodel-debug-recorder.ts +++ b/src/remodel-debug-recorder.ts @@ -215,10 +215,10 @@ const sanitizeBody = (path: string, body: unknown): unknown => { const createSanitizedContext = (slotId: string | number | undefined): SanitizedFleetContext => { const deckShipIds = window._decks?.[0]?.api_ship?.slice(0, 2) || [] - const flagship = deckShipIds[0] == null ? undefined : window._ships[deckShipIds[0]] - const secondShip = deckShipIds[1] == null ? undefined : window._ships[deckShipIds[1]] + const flagship = deckShipIds[0] == null ? undefined : window._ships?.[deckShipIds[0]] + const secondShip = deckShipIds[1] == null ? undefined : window._ships?.[deckShipIds[1]] if (slotId != null) { - const slotitem = window._slotitems[slotId] + const slotitem = window._slotitems?.[slotId] if (slotitem) { return { firstFleet: { diff --git a/tests/remodel-debug-recorder.test.ts b/tests/remodel-debug-recorder.test.ts index f400c3d..0111ee9 100644 --- a/tests/remodel-debug-recorder.test.ts +++ b/tests/remodel-debug-recorder.test.ts @@ -264,16 +264,35 @@ describe('remodel debug recorder', () => { }) }) - it('logs and skips malformed circular captures', () => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + it('keeps recording when slot item context is missing', () => { window._slotitems = undefined as unknown as typeof window._slotitems setRemodelDebugRecorderEnabled(true) recordRemodelDebugEvent(remodelDetailEvent) - expect(getRemodelDebugRecords()).toHaveLength(0) - expect(consoleError).toHaveBeenCalled() - consoleError.mockRestore() + expect(getRemodelDebugRecords()).toHaveLength(1) + expect(getRemodelDebugRecords()[0]?.context).toEqual({ + firstFleet: { + flagship: { api_ship_id: 101 }, + secondShip: { api_ship_id: 102 }, + }, + }) + }) + + it('keeps recording when ship context is missing', () => { + window._ships = undefined as unknown as typeof window._ships + setRemodelDebugRecorderEnabled(true) + + recordRemodelDebugEvent(remodelDetailEvent) + + expect(getRemodelDebugRecords()).toHaveLength(1) + expect(getRemodelDebugRecords()[0]?.context).toEqual({ + firstFleet: {}, + selectedSlotItem: { + api_level: 6, + api_slotitem_id: 700, + }, + }) }) it('caps in-memory captures to the latest 200 records', () => { From c2b9f71a2074653085608f6e41798c264f2a9bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=8C=E3=81=BF?= Date: Sat, 4 Jul 2026 22:10:42 +0800 Subject: [PATCH 26/26] test: derive reporter version in harness Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/helpers/reporter-test-harness.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/helpers/reporter-test-harness.ts b/tests/helpers/reporter-test-harness.ts index 2b8c9a0..45f2c12 100644 --- a/tests/helpers/reporter-test-harness.ts +++ b/tests/helpers/reporter-test-harness.ts @@ -1,6 +1,7 @@ import { createHash } from 'node:crypto' import Module from 'node:module' import { vi } from 'vitest' +import packageMeta from '../../package.json' import type { WindowSlotItem } from '../../src/types/window-state' import SourceAACIReporter from '../../src/reporters/aaci' import SourceBaseReporter from '../../src/reporters/base' @@ -78,7 +79,6 @@ const state = vi.hoisted(() => { }) as unknown as Window & typeof globalThis globalThis.window = createWindow() - ;(globalThis as unknown as { __REPORTER_VERSION__: string }).__REPORTER_VERSION__ = '8.1.0' ;(globalThis as any).__reporterTestHarnessState = { aaciState, selectorState, @@ -96,6 +96,9 @@ const state = vi.hoisted(() => { export const { aaciState, fetchState, momentState, selectorState, sentryState } = state +;(globalThis as unknown as { __REPORTER_VERSION__: string }).__REPORTER_VERSION__ = + packageMeta.version + declare global { var __reporterTestHarnessPatched: boolean | undefined }