mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
641 lines
17 KiB
JavaScript
641 lines
17 KiB
JavaScript
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import process from 'node:process'
|
|
|
|
// TypeScript 7 is a native CLI; AST consumers still need the legacy JavaScript API.
|
|
import ts from 'typescript-api'
|
|
import { isTestOnlySourcePath } from './test-only-source-path.mjs'
|
|
|
|
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'])
|
|
const SKIP_PATH_PARTS = new Set(['.git', 'dist', 'node_modules', 'out', '__snapshots__', 'assets'])
|
|
const LOCALIZATION_CALL_NAMES = new Set(['t', 'translate'])
|
|
const CLASS_PROPERTY_NAMES = new Set(['className', 'classNames'])
|
|
// Why: the children of these elements are CSS or code, never copy a translator should see.
|
|
const NON_COPY_JSX_ELEMENTS = new Set(['style', 'script'])
|
|
const USER_VISIBLE_JSX_ATTRIBUTES = new Set([
|
|
'ariaLabel',
|
|
'aria-label',
|
|
'aria-description',
|
|
'alt',
|
|
'description',
|
|
'emptyText',
|
|
'helperText',
|
|
'keywords',
|
|
'label',
|
|
'message',
|
|
'placeholder',
|
|
'subtitle',
|
|
'text',
|
|
'title',
|
|
'toggleDescription',
|
|
'tooltip'
|
|
])
|
|
const USER_VISIBLE_OBJECT_KEYS = new Set([
|
|
'ariaLabel',
|
|
'badge',
|
|
'description',
|
|
'emptyText',
|
|
'error',
|
|
'helperText',
|
|
'keywords',
|
|
'label',
|
|
'message',
|
|
'placeholder',
|
|
'subtitle',
|
|
'title',
|
|
'toggleDescription',
|
|
'tooltip'
|
|
])
|
|
const USER_VISIBLE_FUNCTION_NAMES = new Set([
|
|
'alert',
|
|
'confirm',
|
|
'prompt',
|
|
'showError',
|
|
'showToast'
|
|
])
|
|
const USER_VISIBLE_OBJECT_METHODS = new Set([
|
|
'error',
|
|
'info',
|
|
'loading',
|
|
'message',
|
|
'promise',
|
|
'success',
|
|
'warning'
|
|
])
|
|
const USER_VISIBLE_OBJECT_NAMES = new Set(['toast'])
|
|
// Only comparison operands are non-copy; rejecting all non-+ operators hides guarded JSX and ?? fallback strings.
|
|
const COPY_PRESERVING_BINARY_OPERATORS = new Set([
|
|
ts.SyntaxKind.PlusToken,
|
|
ts.SyntaxKind.QuestionQuestionToken,
|
|
ts.SyntaxKind.BarBarToken,
|
|
ts.SyntaxKind.AmpersandAmpersandToken
|
|
])
|
|
|
|
function normalizePath(root, filePath) {
|
|
return path.relative(root, filePath).split(path.sep).join('/')
|
|
}
|
|
|
|
export function isSkippedFile(root, filePath) {
|
|
const relative = normalizePath(root, filePath)
|
|
if (relative.endsWith('.d.ts') || isTestOnlySourcePath(relative)) {
|
|
return true
|
|
}
|
|
return relative.split('/').some((part) => SKIP_PATH_PARTS.has(part))
|
|
}
|
|
|
|
async function collectSourceFiles(root, dir) {
|
|
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
const files = []
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
if (!SKIP_PATH_PARTS.has(entry.name)) {
|
|
files.push(...(await collectSourceFiles(root, fullPath)))
|
|
}
|
|
continue
|
|
}
|
|
if (
|
|
entry.isFile() &&
|
|
SOURCE_EXTENSIONS.has(path.extname(entry.name)) &&
|
|
!isSkippedFile(root, fullPath)
|
|
) {
|
|
files.push(fullPath)
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
function hasHumanLanguageText(text) {
|
|
const trimmed = text.replace(/\s+/g, ' ').trim()
|
|
if (trimmed.length < 2) {
|
|
return false
|
|
}
|
|
if (/^[\d\s!-/:-@[-`{-~]+$/.test(trimmed)) {
|
|
return false
|
|
}
|
|
return /[A-Za-z\u00C0-\u024F\u3040-\u30ff\u3400-\u9fff\uac00-\ud7af]/.test(trimmed)
|
|
}
|
|
|
|
function compactText(text) {
|
|
return text.replace(/\s+/g, ' ').trim()
|
|
}
|
|
|
|
function lineAndColumn(sourceFile, node) {
|
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
return { line: position.line + 1, column: position.character + 1 }
|
|
}
|
|
|
|
function propertyNameText(name) {
|
|
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) {
|
|
return name.text
|
|
}
|
|
if (ts.isComputedPropertyName(name) && ts.isStringLiteralLike(name.expression)) {
|
|
return name.expression.text
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function expressionNameText(node) {
|
|
if (ts.isIdentifier(node)) {
|
|
return node.text
|
|
}
|
|
if (ts.isPropertyAccessExpression(node)) {
|
|
return `${expressionNameText(node.expression) ?? ''}.${node.name.text}`.replace(/^\./, '')
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function stringParts(node) {
|
|
if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
return [{ text: node.text, dynamic: false }]
|
|
}
|
|
if (!ts.isTemplateExpression(node)) {
|
|
return []
|
|
}
|
|
return [
|
|
{ text: node.head.text, dynamic: true },
|
|
...node.templateSpans.map((span) => ({ text: span.literal.text, dynamic: true }))
|
|
]
|
|
}
|
|
|
|
function isInsideLocalizationCall(node) {
|
|
let current = node.parent
|
|
while (current) {
|
|
if (ts.isCallExpression(current)) {
|
|
const name = expressionNameText(current.expression)
|
|
if (name && LOCALIZATION_CALL_NAMES.has(name.split('.').at(-1) ?? name)) {
|
|
return true
|
|
}
|
|
}
|
|
current = current.parent
|
|
}
|
|
return false
|
|
}
|
|
|
|
function isJsxAttributeValue(node) {
|
|
const parent = node.parent
|
|
if (!parent) {
|
|
return undefined
|
|
}
|
|
if (ts.isJsxAttribute(parent)) {
|
|
return propertyNameText(parent.name)
|
|
}
|
|
if (parent && ts.isJsxExpression(parent) && parent.parent && ts.isJsxAttribute(parent.parent)) {
|
|
return propertyNameText(parent.parent.name)
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function ancestorJsxAttributeName(node) {
|
|
let current = node.parent
|
|
while (current) {
|
|
if (ts.isJsxAttribute(current)) {
|
|
return propertyNameText(current.name)
|
|
}
|
|
if (
|
|
ts.isJsxExpression(current) ||
|
|
ts.isConditionalExpression(current) ||
|
|
ts.isParenthesizedExpression(current) ||
|
|
ts.isBinaryExpression(current)
|
|
) {
|
|
current = current.parent
|
|
continue
|
|
}
|
|
return undefined
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function isRenderedJsxExpression(node) {
|
|
let current = node.parent
|
|
while (current) {
|
|
if (ts.isJsxExpression(current)) {
|
|
return (
|
|
ts.isJsxElement(current.parent) ||
|
|
ts.isJsxFragment(current.parent) ||
|
|
ts.isJsxSelfClosingElement(current.parent)
|
|
)
|
|
}
|
|
if (
|
|
ts.isConditionalExpression(current) ||
|
|
ts.isParenthesizedExpression(current) ||
|
|
ts.isTemplateExpression(current) ||
|
|
ts.isNoSubstitutionTemplateLiteral(current)
|
|
) {
|
|
if (ts.isConditionalExpression(current) && current.condition === node) {
|
|
return false
|
|
}
|
|
current = current.parent
|
|
continue
|
|
}
|
|
if (ts.isBinaryExpression(current)) {
|
|
if (!COPY_PRESERVING_BINARY_OPERATORS.has(current.operatorToken.kind)) {
|
|
return false
|
|
}
|
|
current = current.parent
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
function nearestObjectPropertyName(node) {
|
|
let current = node.parent
|
|
while (current) {
|
|
if (ts.isPropertyAssignment(current) || ts.isShorthandPropertyAssignment(current)) {
|
|
return propertyNameText(current.name)
|
|
}
|
|
if (ts.isObjectLiteralExpression(current) || ts.isArrayLiteralExpression(current)) {
|
|
current = current.parent
|
|
continue
|
|
}
|
|
return undefined
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function hasAncestorObjectPropertyName(node, names) {
|
|
let current = node.parent
|
|
while (current) {
|
|
if (
|
|
(ts.isPropertyAssignment(current) || ts.isShorthandPropertyAssignment(current)) &&
|
|
names.has(propertyNameText(current.name) ?? '')
|
|
) {
|
|
return true
|
|
}
|
|
current = current.parent
|
|
}
|
|
return false
|
|
}
|
|
|
|
function nearestAncestorObjectPropertyName(node) {
|
|
let current = node.parent
|
|
while (current) {
|
|
if (ts.isPropertyAssignment(current) || ts.isShorthandPropertyAssignment(current)) {
|
|
return propertyNameText(current.name)
|
|
}
|
|
current = current.parent
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function findAncestor(node, predicate) {
|
|
let current = node.parent
|
|
while (current) {
|
|
if (predicate(current)) {
|
|
return current
|
|
}
|
|
current = current.parent
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function isUserVisibleCallArgument(node) {
|
|
const call = findAncestor(node, ts.isCallExpression)
|
|
if (!call) {
|
|
return false
|
|
}
|
|
const expressionName = expressionNameText(call.expression)
|
|
if (!expressionName) {
|
|
return false
|
|
}
|
|
const parts = expressionName.split('.')
|
|
const methodName = parts.at(-1)
|
|
const objectName = parts.at(-2)
|
|
return (
|
|
USER_VISIBLE_FUNCTION_NAMES.has(expressionName) ||
|
|
USER_VISIBLE_FUNCTION_NAMES.has(methodName ?? '') ||
|
|
(objectName !== undefined &&
|
|
USER_VISIBLE_OBJECT_NAMES.has(objectName) &&
|
|
USER_VISIBLE_OBJECT_METHODS.has(methodName ?? ''))
|
|
)
|
|
}
|
|
|
|
function isInsideNonCopyJsxElement(node) {
|
|
const element = findAncestor(node, (ancestor) => ts.isJsxElement(ancestor))
|
|
return Boolean(element && NON_COPY_JSX_ELEMENTS.has(element.openingElement.tagName.getText()))
|
|
}
|
|
|
|
function classifyStringNode(node) {
|
|
if (
|
|
hasAncestorObjectPropertyName(node, CLASS_PROPERTY_NAMES) ||
|
|
isInsideNonCopyJsxElement(node)
|
|
) {
|
|
return undefined
|
|
}
|
|
|
|
if (
|
|
findAncestor(
|
|
node,
|
|
(ancestor) =>
|
|
ts.isBinaryExpression(ancestor) &&
|
|
!COPY_PRESERVING_BINARY_OPERATORS.has(ancestor.operatorToken.kind)
|
|
)
|
|
) {
|
|
return undefined
|
|
}
|
|
|
|
const jsxAttributeName = isJsxAttributeValue(node)
|
|
if (jsxAttributeName) {
|
|
return USER_VISIBLE_JSX_ATTRIBUTES.has(jsxAttributeName)
|
|
? `jsx-attribute:${jsxAttributeName}`
|
|
: undefined
|
|
}
|
|
|
|
const ancestorAttributeName = ancestorJsxAttributeName(node)
|
|
if (ancestorAttributeName) {
|
|
return USER_VISIBLE_JSX_ATTRIBUTES.has(ancestorAttributeName)
|
|
? `jsx-attribute:${ancestorAttributeName}`
|
|
: undefined
|
|
}
|
|
|
|
if (ts.isJsxText(node)) {
|
|
return 'jsx-text'
|
|
}
|
|
|
|
const objectPropertyName = nearestObjectPropertyName(node)
|
|
if (objectPropertyName && !USER_VISIBLE_OBJECT_KEYS.has(objectPropertyName)) {
|
|
return undefined
|
|
}
|
|
|
|
const ancestorObjectPropertyName = nearestAncestorObjectPropertyName(node)
|
|
if (ancestorObjectPropertyName && !USER_VISIBLE_OBJECT_KEYS.has(ancestorObjectPropertyName)) {
|
|
return undefined
|
|
}
|
|
|
|
if (isRenderedJsxExpression(node)) {
|
|
return 'jsx-expression'
|
|
}
|
|
|
|
if (isUserVisibleCallArgument(node)) {
|
|
return 'user-visible-call'
|
|
}
|
|
|
|
if (objectPropertyName) {
|
|
return `object-property:${objectPropertyName}`
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
function areaForFile(relativePath) {
|
|
const rendererPrefix = 'src/renderer/src/'
|
|
if (!relativePath.startsWith(rendererPrefix)) {
|
|
return relativePath.split('/').slice(0, 2).join('/')
|
|
}
|
|
|
|
const withoutPrefix = relativePath.slice(rendererPrefix.length)
|
|
const parts = withoutPrefix.split('/')
|
|
if (parts[0] === 'components' && parts[1]) {
|
|
return `renderer/${parts[1]}`
|
|
}
|
|
return `renderer/${parts[0] ?? 'root'}`
|
|
}
|
|
|
|
export function collectLocalizationCandidates(filePath, sourceText, root = process.cwd()) {
|
|
const sourceKind =
|
|
filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
|
const sourceFile = ts.createSourceFile(
|
|
filePath,
|
|
sourceText,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
sourceKind
|
|
)
|
|
const reports = []
|
|
const relativePath = normalizePath(root, filePath)
|
|
|
|
function pushReport(node, kind, text, dynamic = false) {
|
|
const value = compactText(text)
|
|
if (!hasHumanLanguageText(value) || isInsideLocalizationCall(node)) {
|
|
return
|
|
}
|
|
const position = lineAndColumn(sourceFile, node)
|
|
reports.push({
|
|
area: areaForFile(relativePath),
|
|
filePath: relativePath,
|
|
start: node.getStart(sourceFile),
|
|
end: node.getEnd(),
|
|
line: position.line,
|
|
column: position.column,
|
|
kind,
|
|
text: value,
|
|
dynamic
|
|
})
|
|
}
|
|
|
|
function visit(node) {
|
|
if (ts.isJsxText(node)) {
|
|
pushReport(node, 'jsx-text', node.text)
|
|
return
|
|
}
|
|
|
|
if (
|
|
ts.isStringLiteralLike(node) ||
|
|
ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
ts.isTemplateExpression(node)
|
|
) {
|
|
const kind = classifyStringNode(node)
|
|
if (kind) {
|
|
for (const part of stringParts(node)) {
|
|
pushReport(node, kind, part.text, part.dynamic)
|
|
}
|
|
}
|
|
}
|
|
|
|
ts.forEachChild(node, visit)
|
|
}
|
|
|
|
visit(sourceFile)
|
|
return reports
|
|
}
|
|
|
|
function groupByArea(reports) {
|
|
const groups = new Map()
|
|
for (const report of reports) {
|
|
const group = groups.get(report.area) ?? { area: report.area, count: 0, files: new Map() }
|
|
group.count += 1
|
|
group.files.set(report.filePath, (group.files.get(report.filePath) ?? 0) + 1)
|
|
groups.set(report.area, group)
|
|
}
|
|
return [...groups.values()].sort((left, right) => right.count - left.count)
|
|
}
|
|
|
|
function formatReports(_root, reports) {
|
|
return reports
|
|
.map(
|
|
(report) =>
|
|
`${report.filePath}:${report.line}:${report.column} ${report.kind}: ${JSON.stringify(report.text)}`
|
|
)
|
|
.join('\n')
|
|
}
|
|
|
|
function formatMarkdownReport(reports) {
|
|
const groups = groupByArea(reports)
|
|
const lines = [
|
|
'# Localization Candidate Inventory',
|
|
'',
|
|
`Generated: ${new Date().toISOString()}`,
|
|
'',
|
|
`Total candidates: ${reports.length}`,
|
|
'',
|
|
'## Area Summary',
|
|
''
|
|
]
|
|
|
|
for (const group of groups) {
|
|
lines.push(`- ${group.area}: ${group.count} candidates across ${group.files.size} files`)
|
|
}
|
|
|
|
lines.push('', '## Candidates', '')
|
|
for (const group of groups) {
|
|
lines.push(`### ${group.area}`, '')
|
|
for (const report of reports.filter((entry) => entry.area === group.area)) {
|
|
lines.push(
|
|
`- \`${report.filePath}:${report.line}:${report.column}\` ${report.kind}: ${JSON.stringify(report.text)}`
|
|
)
|
|
}
|
|
lines.push('')
|
|
}
|
|
|
|
return lines.join('\n')
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const options = {
|
|
allowlistPath: path.join('config', 'localization-coverage-allowlist.json'),
|
|
check: false,
|
|
format: 'summary',
|
|
outputPath: null,
|
|
sourceRoot: path.join('src', 'renderer', 'src')
|
|
}
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index]
|
|
if (arg === '--json') {
|
|
options.format = 'json'
|
|
} else if (arg === '--markdown') {
|
|
options.format = 'markdown'
|
|
} else if (arg === '--check') {
|
|
options.check = true
|
|
} else if (arg === '--allowlist') {
|
|
options.allowlistPath = argv[index + 1] ?? options.allowlistPath
|
|
index += 1
|
|
} else if (arg === '--output') {
|
|
options.outputPath = argv[index + 1] ?? null
|
|
index += 1
|
|
} else if (arg === '--source-root') {
|
|
options.sourceRoot = argv[index + 1] ?? options.sourceRoot
|
|
index += 1
|
|
}
|
|
}
|
|
|
|
return options
|
|
}
|
|
|
|
function candidateSignature(candidate) {
|
|
return JSON.stringify({
|
|
filePath: candidate.filePath,
|
|
kind: candidate.kind,
|
|
text: candidate.text,
|
|
dynamic: candidate.dynamic
|
|
})
|
|
}
|
|
|
|
function countBySignature(reports) {
|
|
const counts = new Map()
|
|
for (const report of reports) {
|
|
const signature = candidateSignature(report)
|
|
counts.set(signature, (counts.get(signature) ?? 0) + 1)
|
|
}
|
|
return counts
|
|
}
|
|
|
|
async function readAllowlist(root, allowlistPath) {
|
|
const absolutePath = path.resolve(root, allowlistPath)
|
|
const raw = await fs.readFile(absolutePath, 'utf8')
|
|
return JSON.parse(raw)
|
|
}
|
|
|
|
function findNewCandidates(reports, allowlist) {
|
|
const allowedCounts = new Map(
|
|
allowlist.map((entry) => [
|
|
JSON.stringify({
|
|
filePath: entry.filePath,
|
|
kind: entry.kind,
|
|
text: entry.text,
|
|
dynamic: entry.dynamic
|
|
}),
|
|
entry.count
|
|
])
|
|
)
|
|
const seenCounts = countBySignature(reports)
|
|
const newCandidates = []
|
|
|
|
for (const report of reports) {
|
|
const signature = candidateSignature(report)
|
|
const seenCount = seenCounts.get(signature) ?? 0
|
|
const allowedCount = allowedCounts.get(signature) ?? 0
|
|
if (seenCount > allowedCount) {
|
|
newCandidates.push(report)
|
|
seenCounts.set(signature, seenCount - 1)
|
|
}
|
|
}
|
|
|
|
return newCandidates
|
|
}
|
|
|
|
export async function main(root = process.cwd(), argv = process.argv.slice(2)) {
|
|
const options = parseArgs(argv)
|
|
const absoluteSourceRoot = path.resolve(root, options.sourceRoot)
|
|
const files = await collectSourceFiles(root, absoluteSourceRoot)
|
|
const reports = []
|
|
|
|
for (const filePath of files) {
|
|
const sourceText = await fs.readFile(filePath, 'utf8')
|
|
reports.push(...collectLocalizationCandidates(filePath, sourceText, root))
|
|
}
|
|
|
|
if (options.check) {
|
|
const allowlist = await readAllowlist(root, options.allowlistPath)
|
|
const newCandidates = findNewCandidates(reports, allowlist)
|
|
if (newCandidates.length > 0) {
|
|
console.error('New unlocalized renderer strings were found.')
|
|
console.error('Localize them or add a reviewed exclusion to the localization allowlist.')
|
|
console.error('')
|
|
console.error(formatReports(root, newCandidates))
|
|
return 1
|
|
}
|
|
console.log(`Localization coverage check passed with ${reports.length} allowlisted candidates.`)
|
|
return 0
|
|
}
|
|
|
|
const output =
|
|
options.format === 'json'
|
|
? `${JSON.stringify(reports, null, 2)}\n`
|
|
: options.format === 'markdown'
|
|
? `${formatMarkdownReport(reports)}\n`
|
|
: `${reports.length} localization candidates in ${files.length} files.\n${groupByArea(
|
|
reports
|
|
)
|
|
.map((group) => `${group.area}: ${group.count}`)
|
|
.join('\n')}\n`
|
|
|
|
if (options.outputPath) {
|
|
await fs.mkdir(path.dirname(path.resolve(root, options.outputPath)), { recursive: true })
|
|
await fs.writeFile(path.resolve(root, options.outputPath), output)
|
|
} else {
|
|
process.stdout.write(output)
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
process.exit(await main())
|
|
}
|