import { readFileSync } from 'node:fs'; import { createServer } from 'vite'; const sourcePath = 'src/game/scenes/CampScene.ts'; const source = readFileSync(sourcePath, 'utf8'); const errors = []; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true }, appType: 'custom' }); try { const { battleScenarios, defaultBattleScenario } = await server.ssrLoadModule('/src/game/data/battles.ts'); const { firstPursuitScoutVisitDefinition } = await server.ssrLoadModule( '/src/game/data/firstPursuitScoutVisit.ts' ); const { secondBattleReliefExplorationDefinition } = await server.ssrLoadModule( '/src/game/data/secondBattleReliefExploration.ts' ); const { thirdCampCompanionDialogues, thirdCampExplorationVisitId } = await server.ssrLoadModule( '/src/game/data/thirdCampExploration.ts' ); const { thirdBattleReturnCampStep } = await server.ssrLoadModule( '/src/game/data/thirdBattleReturnDialogue.ts' ); const scenarioData = await server.ssrLoadModule('/src/game/data/scenario.ts'); const { isCampaignStep } = await server.ssrLoadModule('/src/game/state/campaignState.ts'); const itemRewardArrays = collectItemRewardArrays(source); itemRewardArrays.forEach(({ body, index, offset }) => validateItemRewards(body, index, offset)); const campBattleIdEntries = collectCampBattleIdEntries(source, defaultBattleScenario.id); const campBattleIdKeys = new Set(campBattleIdEntries.map((entry) => entry.key)); validateCampBattleIdEntries(campBattleIdEntries, battleScenarios); const knownBondsById = collectKnownBondsById(battleScenarios, scenarioData); const dialogueEvents = materializeCanonicalThirdCampCompanionDialogues( collectCampEvents(source, 'campDialogues'), thirdCampCompanionDialogues ); const visitEvents = collectCampEvents(source, 'campVisits').map((event) => materializeCanonicalThirdCampExplorationVisit( materializeCanonicalSecondBattleReliefVisit( materializeCanonicalFirstPursuitVisit( event, firstPursuitScoutVisitDefinition ), secondBattleReliefExplorationDefinition ), thirdCampExplorationVisitId, thirdBattleReturnCampStep ) ); const campaignStepReferences = validateCampEventCollection(dialogueEvents, 'campDialogues', campBattleIdKeys, isCampaignStep) + validateCampEventCollection(visitEvents, 'campVisits', campBattleIdKeys, isCampaignStep); const campBondReferences = validateCampBondReferences(dialogueEvents, 'campDialogues', knownBondsById, { matchUnitIds: true }) + validateCampBondReferences(visitEvents, 'campVisits', knownBondsById); const rewardBondLinks = validateRewardBondLinks(dialogueEvents, 'campDialogues', 'rewardExp') + validateRewardBondLinks(visitEvents, 'campVisits', 'bondExp'); validateNumericProperties('gold', { min: 1, max: 20000 }); validateNumericProperties('bondExp', { min: 0, max: 200 }); validateNumericProperties('rewardExp', { min: 1, max: 200 }); const interactionUxGuardCount = validateCampInteractionUxGuards(); if (errors.length > 0) { throw new Error(`Camp reward data verification failed:\n${errors.map((error) => `- ${error}`).join('\n')}`); } console.log( `Verified ${dialogueEvents.length} camp dialogues, ${visitEvents.length} camp visits, ${campBattleIdEntries.length} camp battle ids, ${campaignStepReferences} campaign-step references, ${campBondReferences} camp bond references, ${rewardBondLinks} reward-bond links, ${itemRewardArrays.length} camp item reward lists, camp reward numeric fields, and ${interactionUxGuardCount} camp interaction UX guards.` ); } finally { await server.close(); } function collectItemRewardArrays(text) { return [...text.matchAll(/itemRewards:\s*\[([\s\S]*?)\]/g)].map((match, index) => ({ body: match[1], index, offset: match.index ?? 0 })); } function validateItemRewards(body, index, offset) { const entries = [...body.matchAll(/'((?:\\'|[^'])*)'/g)].map((match) => match[1].replace(/\\'/g, "'")); if (entries.length === 0) { return; } const labels = new Set(); entries.forEach((reward, rewardIndex) => { const parsed = parseRewardLabel(reward); if (!parsed.label) { errors.push(`${sourcePath}:${lineNumber(offset)} itemRewards[${index}][${rewardIndex}] has an empty parsed label`); } if (!Number.isInteger(parsed.amount) || parsed.amount < 1) { errors.push(`${sourcePath}:${lineNumber(offset)} itemRewards[${index}][${rewardIndex}] has invalid amount ${parsed.amount}`); } if (labels.has(parsed.label)) { errors.push(`${sourcePath}:${lineNumber(offset)} itemRewards[${index}] has duplicate label "${parsed.label}"`); } labels.add(parsed.label); }); } function validateNumericProperties(propertyName, { min, max }) { const pattern = new RegExp(`${propertyName}:\\s*(-?\\d+)`, 'g'); for (const match of source.matchAll(pattern)) { const value = Number(match[1]); if (!Number.isInteger(value) || value < min || value > max) { errors.push(`${sourcePath}:${lineNumber(match.index ?? 0)} ${propertyName} has out-of-range value ${value}`); } } } function validateCampInteractionUxGuards() { const visitRewardMethod = extractPrivateMethod(source, 'showVisitReward'); const dialogueRewardMethod = extractPrivateMethod(source, 'showDialogueReward'); const noticeMethod = extractPrivateMethod(source, 'showCampNotice'); const noticeDurationMethod = extractPrivateMethod(source, 'campNoticeHoldDuration'); const enterKeyMethod = extractPrivateMethod(source, 'handleSortieEnterKey'); const navigationMethod = extractPrivateMethod(source, 'startCampNavigation'); const steppedNavigationMethod = extractPrivateMethod(source, 'startCampNavigationWithCampaignStep'); const startStoryMethod = extractPrivateMethod(source, 'startVictoryStory'); const saveMethod = extractPrivateMethod(source, 'saveCampToSlot'); const wolongLeadMethod = extractPrivateMethod(source, 'hasWolongAudienceLead'); const renderBondListMethod = extractPrivateMethod(source, 'renderBondList'); let checkedGuardCount = 0; expectCampUx( visitRewardMethod.includes('획득 내역') && visitRewardMethod.includes('${this.visitRewardText(choice)}') && visitRewardMethod.includes('응답 · ${choice.response}') && !visitRewardMethod.includes('delayedCall'), 'visit rewards must display the acquisition details and response together without delayed replacement' ); checkedGuardCount += 1; expectCampUx( dialogueRewardMethod.includes('획득 내역') && dialogueRewardMethod.includes('공명 +${rewardExp}') && dialogueRewardMethod.includes('응답 · ${choice.response}') && !dialogueRewardMethod.includes('delayedCall'), 'dialogue rewards must display the acquisition details and response together without delayed replacement' ); checkedGuardCount += 1; expectCampUx( noticeMethod.includes('const holdDuration = this.campNoticeHoldDuration(message)') && noticeMethod.includes('delay: holdDuration') && noticeMethod.includes('this.time.delayedCall(holdDuration, removeNotice)') && noticeMethod.includes('text.height + 28'), 'camp notices must size multi-line content, use a message-aware hold duration, and preserve it without fade motion' ); checkedGuardCount += 1; const minimumHoldMs = Number(/const campNoticeMinimumHoldMs = (\d+);/.exec(source)?.[1] ?? 0); expectCampUx( minimumHoldMs >= 2500 && noticeDurationMethod.includes('readableCharacterCount * campNoticeCharacterHoldMs') && noticeDurationMethod.includes('campNoticeMaximumHoldMs'), `camp notice timing must retain a readable minimum and scale with body length (found ${minimumHoldMs}ms)` ); checkedGuardCount += 1; expectCampUx( source.includes('private navigationPending = false;') && enterKeyMethod.includes('event.repeat || this.navigationPending'), 'sortie Enter handling must ignore key-repeat events and pending navigation' ); checkedGuardCount += 1; const navigationLockIndex = navigationMethod.indexOf('this.navigationPending = true;'); const navigationBlockerIndex = navigationMethod.indexOf('this.showCampNavigationBlocker();'); const lazySceneReadyIndex = navigationMethod.indexOf('await ensureLazyScene(this.game, key);'); const stateCommitIndex = navigationMethod.indexOf('commitState();'); const sceneStartIndex = navigationMethod.indexOf('this.scene.start(key, data);'); expectCampUx( navigationMethod.includes('if (this.navigationPending)') && navigationLockIndex >= 0 && navigationBlockerIndex > navigationLockIndex && lazySceneReadyIndex > navigationBlockerIndex && stateCommitIndex > lazySceneReadyIndex && sceneStartIndex > stateCommitIndex && navigationMethod.includes('this.navigationPending = false;') && navigationMethod.includes('this.hideCampNavigationBlocker();'), 'lazy camp navigation must lock the screen, finish loading, commit state, and then start the scene' ); checkedGuardCount += 1; expectCampUx( startStoryMethod.includes('if (this.navigationPending)') && !startStoryMethod.includes('startLazyScene(') && ((startStoryMethod.match(/this\.startCampNavigation\(/g)?.length ?? 0) + (startStoryMethod.match(/this\.startCampNavigationWithCampaignStep\(/g)?.length ?? 0)) >= 4, 'every camp story, battle, and ending transition must route through the scene navigation lock' ); checkedGuardCount += 1; expectCampUx( steppedNavigationMethod.includes('const previousStep = getCampaignState().step') && steppedNavigationMethod.includes('markCampaignStep(step)') && steppedNavigationMethod.includes('getCampaignState().step === step') && steppedNavigationMethod.includes('markCampaignStep(previousStep)') && navigationMethod.includes('recoverState?.()'), 'campaign-step navigation must restore the previous step when lazy scene loading fails' ); checkedGuardCount += 1; const transientUndoFields = [ 'sortieSwapUndoState', 'sortieRecommendationUndoState', 'sortiePresetUndoState', 'sortieGuidedImprovementUndoState', 'reportFormationHistoryUndoState' ]; const clearedUndoField = transientUndoFields.find((field) => new RegExp(`this\\.${field}\\s*=\\s*undefined`).test(saveMethod)); expectCampUx( !clearedUndoField && !saveMethod.includes('persistSortieSelection('), `manual camp saves must preserve applicable undo state${clearedUndoField ? ` (cleared ${clearedUndoField})` : ''}` ); checkedGuardCount += 1; expectCampUx( startStoryMethod.includes("flow.nextBattleId === campBattleIds.seventeenth && !this.hasWolongAudienceLead()") && wolongLeadMethod.includes("completedVisits.includes('jingzhou-scholar-rumors')") && wolongLeadMethod.includes("completedVisits.includes('refugee-village-patrol')"), 'the Wolong sortie gate must accept a completed information visit regardless of which visit reward choice was taken' ); checkedGuardCount += 1; expectCampUx( renderBondListMethod.includes('dialogue.id === this.selectedDialogueId') && renderBondListMethod.includes('[selectedBond, ...relatedBonds.filter'), 'the resonance list must keep the currently selected dialogue bond visible when more than four bonds are available' ); checkedGuardCount += 1; return checkedGuardCount; } function expectCampUx(condition, message) { if (!condition) { errors.push(`${sourcePath}: ${message}`); } } function extractPrivateMethod(text, methodName) { const match = new RegExp(`\\bprivate\\s+${methodName}\\s*\\(`).exec(text); if (!match) { errors.push(`${sourcePath}: cannot find private method ${methodName}`); return ''; } const bodyStart = text.indexOf('{', match.index + match[0].length); if (bodyStart < 0) { errors.push(`${sourcePath}:${lineNumber(match.index)} cannot find body for private method ${methodName}`); return ''; } const bodyEnd = findMatchingDelimiter(text, bodyStart, '{', '}'); return text.slice(bodyStart, bodyEnd + 1); } function collectCampBattleIdEntries(text, defaultBattleScenarioId) { const objectSource = extractConstObject(text, 'campBattleIds'); const objectOffset = text.indexOf(objectSource); return [...objectSource.matchAll(/^\s*(\w+):\s*([^,\n]+)/gm)].map((match) => { const expression = match[2].trim(); return { key: match[1], expression, value: parseCampBattleIdExpression(expression, defaultBattleScenarioId), offset: objectOffset + (match.index ?? 0) }; }); } function parseCampBattleIdExpression(expression, defaultBattleScenarioId) { const stringMatch = /^'([^']+)'$/.exec(expression); if (stringMatch) { return stringMatch[1]; } if (expression === 'defaultBattleScenario.id') { return defaultBattleScenarioId; } return ''; } function validateCampBattleIdEntries(entries, battleScenarios) { const knownBattleIds = new Set(Object.keys(battleScenarios)); const seenBattleIds = new Map(); entries.forEach((entry) => { const context = `campBattleIds.${entry.key}`; if (!entry.value) { errors.push(`${sourcePath}:${lineNumber(entry.offset)} ${context} uses unsupported expression "${entry.expression}"`); return; } if (!knownBattleIds.has(entry.value)) { errors.push(`${sourcePath}:${lineNumber(entry.offset)} ${context} references unknown battle id "${entry.value}"`); } if (seenBattleIds.has(entry.value)) { errors.push( `${sourcePath}:${lineNumber(entry.offset)} ${context} duplicates battle id "${entry.value}" first used by campBattleIds.${seenBattleIds.get( entry.value )}` ); } seenBattleIds.set(entry.value, entry.key); }); } function collectKnownBondsById(battleScenarios, scenarioData) { const bondsById = new Map(); const addBonds = (bonds) => { (bonds ?? []).forEach((bond) => { if (!bondsById.has(bond.id)) { bondsById.set(bond.id, bond); } }); }; Object.values(battleScenarios).forEach((scenario) => { addBonds(scenario.bonds); }); Object.values(scenarioData).forEach((value) => { if (isBattleBondArray(value)) { addBonds(value); } }); return bondsById; } function isBattleBondArray(value) { return Array.isArray(value) && value.length > 0 && value.every(isBattleBondEntry); } function isBattleBondEntry(value) { return ( value && typeof value.id === 'string' && Array.isArray(value.unitIds) && typeof value.title === 'string' && Number.isFinite(value.level) && Number.isFinite(value.exp) ); } function collectCampEvents(text, arrayName) { const { body, offset } = extractConstArray(text, arrayName); return collectTopLevelObjects(body).map((event) => ({ ...event, offset: offset + event.offset })); } function materializeCanonicalThirdCampCompanionDialogues( events, definitions ) { const quote = (value) => `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`; return events.flatMap((event) => { if ( !event.body.includes('...dialogue') || !event.body.includes('choices: dialogue.choices') ) { return [event]; } return definitions.map((definition) => { const choiceSource = definition.choices .map( (choice) => `{ id: ${quote(choice.id)}, rewardExp: ${choice.rewardExp} }` ) .join(', '); return { ...event, body: `{ id: ${quote(definition.id)}, ` + 'availableAfterBattleIds: [campBattleIds.third], ' + `unitIds: [${definition.unitIds.map(quote).join(', ')}], ` + `bondId: ${quote(definition.bondId)}, ` + `rewardExp: ${definition.rewardExp}, ` + `choices: [${choiceSource}] }` }; }); }); } function materializeCanonicalFirstPursuitVisit(event, definition) { if (!event.body.includes('...firstPursuitScoutVisitDefinition')) { return event; } const quote = (value) => `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`; const choiceSource = definition.choices .map( (choice) => `{ id: ${quote(choice.id)}, bondExp: ${choice.bondExp}, ` + `itemRewards: [${choice.itemRewards.map(quote).join(', ')}] }` ) .join(', '); const canonicalSource = `{ id: ${quote(definition.id)}, ` + 'availableAfterBattleIds: [campBattleIds.first], ' + `bondId: ${quote(definition.bondId)}, choices: [${choiceSource}], `; return { ...event, body: canonicalSource + event.body.slice(1) }; } function materializeCanonicalSecondBattleReliefVisit(event, definition) { if (!event.body.includes('secondBattleReliefExplorationDefinition')) { return event; } const quote = (value) => `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`; const choiceSource = definition.choices .map( (choice) => `{ id: ${quote(choice.id)}, bondId: ${quote(choice.bondId)}, ` + `bondExp: ${choice.bondExp}, ` + `itemRewards: [${choice.itemRewards.map(quote).join(', ')}] }` ) .join(', '); const canonicalSource = `{ id: ${quote(definition.id)}, ` + 'availableAfterBattleIds: [campBattleIds.second], ' + "availableDuringSteps: ['second-camp'], " + `choices: [${choiceSource}], `; return { ...event, body: canonicalSource + event.body.slice(1) }; } function materializeCanonicalThirdCampExplorationVisit( event, visitId, campStep ) { if (!event.body.includes('id: thirdCampExplorationVisitId')) { return event; } const quote = (value) => `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`; const canonicalSource = `{ id: ${quote(visitId)}, ` + 'availableAfterBattleIds: [campBattleIds.third], ' + `availableDuringSteps: [${quote(campStep)}], `; return { ...event, body: canonicalSource + event.body.slice(1) }; } function validateCampEventCollection(events, collectionName, campBattleIdKeys, isCampaignStep) { const seenIds = new Map(); let campaignStepReferenceCount = 0; if (events.length === 0) { errors.push(`${sourcePath}: ${collectionName} has no entries`); } events.forEach((event, index) => { const context = `${collectionName}[${index}]`; const idEntries = collectStringProperties(event.body, 'id'); const eventId = idEntries[0]?.value ?? ''; if (!eventId.trim()) { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has no id`); } else if (seenIds.has(eventId)) { errors.push( `${sourcePath}:${lineNumber(event.offset)} ${context} duplicates id "${eventId}" first used at ${sourcePath}:${seenIds.get( eventId )}` ); } else { seenIds.set(eventId, lineNumber(event.offset)); } const choiceIds = idEntries.slice(1).map((entry) => entry.value); const seenChoiceIds = new Set(); choiceIds.forEach((choiceId, choiceIndex) => { if (!choiceId.trim()) { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context}.choices[${choiceIndex}] has an empty id`); } if (seenChoiceIds.has(choiceId)) { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has duplicate choice id "${choiceId}"`); } seenChoiceIds.add(choiceId); }); const availableMatch = /availableAfterBattleIds:\s*\[([\s\S]*?)\]/.exec(event.body); if (!availableMatch || availableMatch[1].trim().length === 0) { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has no availableAfterBattleIds`); } for (const reference of event.body.matchAll(/campBattleIds\.(\w+)/g)) { if (!campBattleIdKeys.has(reference[1])) { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} references unknown campBattleIds.${reference[1]}`); } } campaignStepReferenceCount += validateAvailableDuringSteps(event, context, isCampaignStep); }); return campaignStepReferenceCount; } function validateAvailableDuringSteps(event, context, isCampaignStep) { const availableDuringMatch = /availableDuringSteps:\s*\[([\s\S]*?)\]/.exec(event.body); if (!availableDuringMatch) { return 0; } const body = availableDuringMatch[1]; const bodyOffset = event.offset + (availableDuringMatch.index ?? 0) + availableDuringMatch[0].indexOf(body); const steps = collectStringLiterals(body, bodyOffset); if (steps.length === 0) { errors.push(`${sourcePath}:${lineNumber(bodyOffset)} ${context} has an empty availableDuringSteps list`); } steps.forEach((step) => { if (!isCampaignStep(step.value)) { errors.push(`${sourcePath}:${lineNumber(step.offset)} ${context} references unknown campaign step "${step.value}"`); } }); return steps.length; } function validateCampBondReferences(events, collectionName, knownBondsById, options = {}) { let bondReferenceCount = 0; events.forEach((event, index) => { const context = `${collectionName}[${index}]`; const bondIdEntries = collectStringProperties(event.body, 'bondId'); if (bondIdEntries.length === 0) { return; } bondReferenceCount += bondIdEntries.length; if (bondIdEntries.length > 1 && collectionName !== 'campVisits') { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has multiple bondId properties`); } const unitIds = options.matchUnitIds ? collectStringArrayProperty(event.body, 'unitIds', event.offset) : []; if (options.matchUnitIds && unitIds.length === 0) { errors.push(`${sourcePath}:${lineNumber(event.offset)} ${context} has bondId but no unitIds`); } bondIdEntries.forEach((entry) => { const bond = knownBondsById.get(entry.value); if (!bond) { errors.push(`${sourcePath}:${lineNumber(event.offset + entry.offset)} ${context} references unknown bondId "${entry.value}"`); return; } if (options.matchUnitIds && !sameStringSet(unitIds.map((unit) => unit.value), bond.unitIds ?? [])) { errors.push( `${sourcePath}:${lineNumber(event.offset + entry.offset)} ${context} bondId "${entry.value}" units [${( bond.unitIds ?? [] ).join(', ')}] do not match dialogue unitIds [${unitIds.map((unit) => unit.value).join(', ')}]` ); } }); }); return bondReferenceCount; } function validateRewardBondLinks(events, collectionName, rewardPropertyName) { let rewardReferenceCount = 0; events.forEach((event, index) => { const context = `${collectionName}[${index}]`; const rewardEntries = collectNumericProperties(event.body, rewardPropertyName, event.offset).filter((entry) => entry.value > 0); if (rewardEntries.length === 0) { return; } rewardReferenceCount += rewardEntries.length; const bondIdCount = collectStringProperties(event.body, 'bondId').length; if (bondIdCount === 0) { errors.push( `${sourcePath}:${lineNumber(rewardEntries[0].offset)} ${context} has ${rewardPropertyName} rewards but no bondId` ); } else if ( bondIdCount > 1 && bondIdCount !== rewardEntries.length ) { errors.push( `${sourcePath}:${lineNumber(rewardEntries[0].offset)} ${context} has ` + `${rewardEntries.length} ${rewardPropertyName} rewards but ` + `${bondIdCount} per-choice bondIds` ); } }); return rewardReferenceCount; } function collectStringProperties(text, propertyName) { const pattern = new RegExp(`\\b${propertyName}:\\s*'((?:\\\\'|[^'])*)'`, 'g'); return [...text.matchAll(pattern)].map((match) => ({ value: match[1].replace(/\\'/g, "'"), offset: match.index ?? 0 })); } function collectNumericProperties(text, propertyName, offset = 0) { const pattern = new RegExp(`\\b${propertyName}:\\s*(-?\\d+)`, 'g'); return [...text.matchAll(pattern)].map((match) => ({ value: Number(match[1]), offset: offset + (match.index ?? 0) })); } function collectStringLiterals(text, offset = 0) { return [...text.matchAll(/'((?:\\'|[^'])*)'/g)].map((match) => ({ value: match[1].replace(/\\'/g, "'"), offset: offset + (match.index ?? 0) })); } function collectStringArrayProperty(text, propertyName, eventOffset = 0) { const pattern = new RegExp(`\\b${propertyName}:\\s*\\[([\\s\\S]*?)\\]`); const match = pattern.exec(text); if (!match) { return []; } const arrayBody = match[1]; const arrayOffset = eventOffset + (match.index ?? 0) + match[0].indexOf(arrayBody); return collectStringLiterals(arrayBody, arrayOffset); } function sameStringSet(left, right) { if (left.length !== right.length) { return false; } const rightSet = new Set(right); return left.every((value) => rightSet.has(value)); } function parseRewardLabel(reward) { const normalized = String(reward).replace(/\s+/g, ' ').trim(); const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/); if (!match) { return { label: normalized, amount: 1 }; } return { label: match[1].trim() || normalized, amount: Number(match[2] ?? 1) }; } function lineNumber(offset) { return source.slice(0, offset).split(/\r?\n/).length; } function extractConstObject(text, objectName) { const startPattern = new RegExp(`const\\s+${objectName}\\b[^=]*=\\s*\\{`); const match = startPattern.exec(text); if (!match) { throw new Error(`Cannot find ${objectName}.`); } const start = match.index + match[0].length - 1; const end = findMatchingDelimiter(text, start, '{', '}'); return text.slice(start, end + 1); } function extractConstArray(text, arrayName) { const startPattern = new RegExp(`const\\s+${arrayName}\\b[\\s\\S]*?=\\s*\\[`); const match = startPattern.exec(text); if (!match) { throw new Error(`Cannot find ${arrayName}.`); } const start = match.index + match[0].length - 1; const end = findMatchingDelimiter(text, start, '[', ']'); return { body: text.slice(start + 1, end), offset: start + 1 }; } function collectTopLevelObjects(text) { const objects = []; let depth = 0; let start = -1; let quote = ''; let escaped = false; for (let index = 0; index < text.length; index += 1) { const char = text[index]; if (quote) { if (escaped) { escaped = false; } else if (char === '\\') { escaped = true; } else if (char === quote) { quote = ''; } continue; } if (char === "'" || char === '"' || char === '`') { quote = char; continue; } if (char === '{') { if (depth === 0) { start = index; } depth += 1; } else if (char === '}') { depth -= 1; if (depth === 0 && start >= 0) { objects.push({ body: text.slice(start, index + 1), offset: start }); start = -1; } } } return objects; } function findMatchingDelimiter(text, start, openChar, closeChar) { let depth = 0; let quote = ''; let escaped = false; for (let index = start; index < text.length; index += 1) { const char = text[index]; if (quote) { if (escaped) { escaped = false; } else if (char === '\\') { escaped = true; } else if (char === quote) { quote = ''; } continue; } if (char === "'" || char === '"' || char === '`') { quote = char; continue; } if (char === openChar) { depth += 1; } else if (char === closeChar) { depth -= 1; if (depth === 0) { return index; } } } throw new Error(`Cannot find matching ${closeChar} from offset ${start}.`); }