@intlayer/lsp 9.5.12 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport {\n getConfiguration,\n searchConfigurationFile,\n} from '@intlayer/config/node';\nimport { getUnmergedDictionaries } from '@intlayer/dictionaries-entry/unmerged';\nimport { buildComponentFilesList } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport {\n type CompletionItem,\n CompletionItemKind,\n type CompletionParams,\n createConnection,\n type Diagnostic,\n DiagnosticSeverity,\n type Hover,\n type HoverParams,\n type InitializeParams,\n type InitializeResult,\n Location,\n MarkupKind,\n Position,\n ProposedFeatures,\n Range,\n type ReferenceParams,\n TextDocumentSyncKind,\n TextDocuments,\n} from 'vscode-languageserver/node';\nimport { TextDocument } from 'vscode-languageserver-textdocument';\nimport {\n buildCallerWithKeyPattern,\n buildKeyUsagePatterns,\n getCallerNamesAlternation,\n} from './callers/patterns';\nimport {\n formatDictionaryHover,\n formatFieldHover,\n formatFieldValue,\n getFieldByPath,\n getFieldsAtPath,\n} from './dictionaryUtils';\nimport {\n escapeRegularExpression,\n findFieldRangesInFile,\n offsetToRange,\n} from './findFieldInFile';\nimport { findKeyAtOffset } from './findKeyAtOffset';\nimport {\n findContentFieldAtOffset,\n findKeyInContentFile,\n} from './findKeyInContentFile';\nimport { type OxcNode, parseText, walkAst } from './oxcUtils';\nimport {\n DICTIONARIES_NOT_BUILT_NOTIFICATION,\n type DictionariesNotBuiltParams,\n} from './protocol';\nimport {\n collectNamespaceReferences,\n findMessageUsageAtOffset,\n matchCallNamespace,\n} from './usageAnalyzer';\n\n// Create a connection for the server, using Node's IPC as a transport.\n// Also include all preview / proposed LSP features.\nconst connection = createConnection(ProposedFeatures.all);\n\n// Create a simple text document manager.\nconst documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);\n\n/**\n * Send a log line to the client's Output panel (View → Output → \"Intlayer LSP\").\n * Uses connection.console.log so it travels over the JSON-RPC channel and never\n * touches stdout (which is reserved for the LSP protocol).\n */\nconst log = (message: string): void =>\n connection.console.log(`[Intlayer LSP] ${message}`);\n\nlet workspaceRoot: string | null | undefined;\n\ntype WorkspaceConfig = Pick<\n IntlayerConfig,\n 'system' | 'build' | 'content' | 'compiler'\n>;\n\n/**\n * Resolve (and cache) the Intlayer configuration for the current workspace.\n * The LSP process cwd is not the project root, so we must resolve the config\n * from `workspaceRoot` to get the right `system`/`build`/`content` paths.\n */\nconst projectConfigs = new Map<string, WorkspaceConfig>();\n\ntype DictionariesCache = {\n dictionaries: ReturnType<typeof getUnmergedDictionaries>;\n cachedAt: number;\n};\nconst projectDictionaries = new Map<string, DictionariesCache>();\nconst DICTIONARY_CACHE_TTL_MS = 5_000;\n\ntype SourceFilesCache = {\n files: string[];\n cachedAt: number;\n};\nconst projectSourceFiles = new Map<string, SourceFilesCache>();\nconst SOURCE_FILES_CACHE_TTL_MS = 30_000;\n\n/**\n * Directory of the file → directory holding the closest intlayer config.\n * Populated lazily by `getProjectRoot`.\n */\nconst projectRoots = new Map<string, string>();\n\n/**\n * Project roots already reported to the client as having no built dictionary.\n * Keeps the notification to one per project instead of one per open document;\n * cleared on invalidation so a later regression is reported again.\n */\nconst buildRequestedProjects = new Set<string>();\n\nconst invalidateConfigCaches = () => {\n projectConfigs.clear();\n projectDictionaries.clear();\n projectRoots.clear();\n buildRequestedProjects.clear();\n\n projectSourceFiles.clear();\n};\n\n/**\n * Resolve the project root for a file — the directory that holds the closest\n * intlayer config file.\n *\n * `getConfiguration({ baseDir })` treats `baseDir` as the project root and\n * derives every `system.*` path from it, so passing the file's own directory\n * would resolve `.intlayer/` next to the file (e.g. `src/.intlayer/…`) instead\n * of at the project root — leaving every dictionary lookup empty.\n */\nconst getProjectRoot = (fileDirectory: string): string => {\n const cached = projectRoots.get(fileDirectory);\n\n if (cached !== undefined) return cached;\n\n const { configurationFilePath } = searchConfigurationFile(fileDirectory);\n const root = configurationFilePath\n ? dirname(configurationFilePath)\n : (workspaceRoot ?? fileDirectory);\n\n projectRoots.set(fileDirectory, root);\n return root;\n};\n\nconst getProjectConfig = (absolutePath: string): WorkspaceConfig | null => {\n try {\n // Find the closest intlayer config relative to the file.\n const baseDir = getProjectRoot(dirname(absolutePath));\n const { system, build, content, compiler } = getConfiguration({ baseDir });\n const cacheKey = system.baseDir;\n\n if (!projectConfigs.has(cacheKey)) {\n projectConfigs.set(cacheKey, { system, build, content, compiler });\n log(\n `getProjectConfig — OK baseDir=${system.baseDir} unmergedDictionariesDir=${system.unmergedDictionariesDir}`\n );\n }\n return projectConfigs.get(cacheKey)!;\n } catch (error) {\n log(`getProjectConfig — FAILED: ${error}`);\n return null;\n }\n};\n\nconst getUnmergedDictionariesCached = (\n config: WorkspaceConfig\n): ReturnType<typeof getUnmergedDictionaries> => {\n const cacheKey = config.system.baseDir;\n const cached = projectDictionaries.get(cacheKey);\n const now = Date.now();\n\n if (cached && now - cached.cachedAt < DICTIONARY_CACHE_TTL_MS) {\n return cached.dictionaries;\n }\n\n const dictionaries = getUnmergedDictionaries(config);\n projectDictionaries.set(cacheKey, { dictionaries, cachedAt: now });\n return dictionaries;\n};\n\n// ---------------------------------------------------------------------------\n// Pattern helpers — derived lazily from the live config.\n// ---------------------------------------------------------------------------\n\nconst isContentFile = (uri: string, config: WorkspaceConfig): boolean => {\n const fileExtensions = config.content.fileExtensions ?? [];\n return fileExtensions.some((extension) => uri.endsWith(extension));\n};\n\n// ---------------------------------------------------------------------------\n// Async I/O helpers\n// ---------------------------------------------------------------------------\n\n/** Read a file as UTF-8, returning null on any error. */\nconst tryReadFile = async (filePath: string): Promise<string | null> => {\n try {\n return await readFile(filePath, 'utf-8');\n } catch {\n return null;\n }\n};\n\n/**\n * Run `callback` over every item with at most `limit` tasks in-flight at once.\n * JS is single-threaded so `results.push` inside callback is race-free.\n */\nconst runConcurrent = async <T>(\n items: T[],\n callback: (item: T) => Promise<void>,\n limit = 50\n): Promise<void> => {\n if (items.length === 0) return;\n let index = 0;\n const worker = async () => {\n while (index < items.length) {\n await callback(items[index++]!);\n }\n };\n await Promise.all(\n Array.from({ length: Math.min(limit, items.length) }, worker)\n );\n};\n\n/** Find the `key: \"<key>\"` line inside a content file so the cursor lands on it. */\nconst getKeyRange = async (\n absolutePath: string,\n key: string\n): Promise<Range> => {\n try {\n const content = await readFile(absolutePath, 'utf-8');\n const lines = content.split(/\\r?\\n/);\n const keyRegularExpression = new RegExp(\n `key\\\\s*:\\\\s*['\"\\`]${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}['\"\\`]`\n );\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const column = lines[lineIndex]?.search(keyRegularExpression);\n\n if (column !== undefined && column !== -1) {\n return Range.create(\n Position.create(lineIndex, column),\n Position.create(lineIndex, lines[lineIndex]!.length)\n );\n }\n }\n } catch {\n // fall through to the top of the file\n }\n\n return Range.create(Position.create(0, 0), Position.create(0, 0));\n};\n\n/**\n * Return every `.content` file location that declares the given key.\n * A key can be split across several content files (merged dictionaries), so we\n * return one `Location` per source file, pointing at the `key:` declaration.\n */\nconst getContentDeclarationLocations = async (\n key: string,\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter((dictionary) => dictionary?.key === key && dictionary.filePath);\n\n const locations: Location[] = [];\n\n await runConcurrent(dictionaries, async (dictionary) => {\n const filePath = dictionary.filePath!;\n const absolutePath = isAbsolute(filePath)\n ? filePath\n : join(config.system.baseDir, filePath);\n\n if (!existsSync(absolutePath)) return;\n\n locations.push(\n Location.create(\n pathToFileURL(absolutePath).href,\n await getKeyRange(absolutePath, key)\n )\n );\n });\n\n return locations;\n};\n\n/**\n * Return the cached source-file list, re-scanning only when the cache has\n * expired or been explicitly invalidated (config change, watched-file event).\n */\nconst getSourceFiles = async (\n directory: string,\n config: WorkspaceConfig\n): Promise<string[]> => {\n const now = Date.now();\n const cached = projectSourceFiles.get(directory);\n\n if (cached && now - cached.cachedAt < SOURCE_FILES_CACHE_TTL_MS) {\n return cached.files;\n }\n const startTime = Date.now();\n const files = buildComponentFilesList(config as unknown as IntlayerConfig, [\n '**/.output/**',\n '**/.svelte-kit/**',\n '**/src-tauri/**',\n '**/.next/**',\n '**/.nuxt/**',\n '**/.expo/**',\n '**/.vercel/**',\n '**/.turbo/**',\n '**/.tanstack/**',\n '**/dist/**',\n '**/build/**',\n '**/node_modules/**',\n ]);\n projectSourceFiles.set(directory, { files, cachedAt: Date.now() });\n log(\n `source files scanned: ${files.length} file(s) in ${Date.now() - startTime}ms`\n );\n return files;\n};\n\n/**\n * Return every source-file location that calls useIntlayer / getIntlayer with\n * the given key.\n */\nconst getKeyUsageLocations = async (\n key: string,\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const usagePatterns = buildKeyUsagePatterns(key);\n\n const filePaths = await getSourceFiles(config.system.baseDir, config);\n const locations: Location[] = [];\n const startTime = Date.now();\n\n await runConcurrent(filePaths, async (filePath) => {\n const text = await tryReadFile(filePath);\n // Quick string check before running the regexes — skips the vast majority of files\n\n if (!text?.includes(key)) return;\n\n const seenStarts = new Set<number>();\n\n for (const usagePattern of usagePatterns) {\n for (const match of text.matchAll(usagePattern)) {\n const matchStart = match.index!;\n\n if (seenStarts.has(matchStart)) continue;\n\n seenStarts.add(matchStart);\n const matchEnd = matchStart + match[0].length;\n locations.push(\n Location.create(\n pathToFileURL(filePath).href,\n offsetToRange(text, matchStart, matchEnd)\n )\n );\n }\n }\n });\n\n log(\n `getKeyUsageLocations(\"${key}\") — ${filePaths.length} file(s) searched in ${Date.now() - startTime}ms → ${locations.length} hit(s)`\n );\n return locations;\n};\n\n/**\n * Return every source-file location that accesses the field at `fieldPath` on\n * the result of `useIntlayer(dictionaryKey)` / `getIntlayer(dictionaryKey)`.\n *\n * `fieldPath` is an array of property names from the content root to the\n * target field (e.g. `['searchInput', 'text']` for a nested field).\n * Single-element paths (e.g. `['greet']`) behave like the previous\n * single-`fieldName` API.\n */\nconst getFieldUsageLocations = async (\n dictionaryKey: string,\n fieldPath: string[],\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config || fieldPath.length === 0) return [];\n\n const leafFieldName = fieldPath[fieldPath.length - 1]!;\n const escapedKey = escapeRegularExpression(dictionaryKey);\n const escapedLeaf = escapeRegularExpression(leafFieldName);\n const useCallRegularExpression = buildCallerWithKeyPattern(dictionaryKey);\n\n const filePaths = await getSourceFiles(config.system.baseDir, config);\n const locations: Location[] = [];\n const startTime = Date.now();\n\n await runConcurrent(filePaths, async (filePath) => {\n const text = await tryReadFile(filePath);\n\n if (!text) return;\n // Pre-filter: dictionaryKey and every segment of the path must appear in the file\n\n if (!text.includes(dictionaryKey)) return;\n\n if (fieldPath.some((segment) => !text.includes(segment))) return;\n\n if (!useCallRegularExpression.test(text)) return;\n\n for (const range of findFieldRangesInFile(\n text,\n escapedKey,\n leafFieldName,\n escapedLeaf,\n fieldPath\n )) {\n locations.push(Location.create(pathToFileURL(filePath).href, range));\n }\n });\n\n log(\n `getFieldUsageLocations(\"${dictionaryKey}.${fieldPath.join('.')}\") — ${filePaths.length} file(s) searched in ${Date.now() - startTime}ms → ${locations.length} hit(s)`\n );\n return locations;\n};\n\n/**\n * Find the line/column of the field's leaf name as a property key inside a\n * content file. Looks for `fieldName:` or `'field.name':` (flat keys with\n * dots, as used by lingui catalogs), not just any occurrence. Falls back to\n * the top of the file if the field cannot be located.\n */\nconst getFieldRangeInContentFile = async (\n absolutePath: string,\n fieldPath: string[]\n): Promise<Range> => {\n const content = await tryReadFile(absolutePath);\n const fieldName = fieldPath[fieldPath.length - 1];\n\n if (!content || !fieldName)\n return Range.create(Position.create(0, 0), Position.create(0, 0));\n\n // Match `fieldName:` at an object property key position (not a value),\n // allowing an optional quoted form for keys containing dots or dashes.\n const fieldRegularExpression = new RegExp(\n `(?<![.\\\\w])(['\"\\`]?)${escapeRegularExpression(fieldName)}\\\\1\\\\s*:`\n );\n const lines = content.split(/\\r?\\n/);\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex]!;\n const matchPosition = line.search(fieldRegularExpression);\n\n if (matchPosition === -1) continue;\n const column = line.indexOf(fieldName, matchPosition);\n\n if (column === -1) continue;\n return Range.create(\n Position.create(lineIndex, column),\n Position.create(lineIndex, column + fieldName.length)\n );\n }\n\n return Range.create(Position.create(0, 0), Position.create(0, 0));\n};\n\n/**\n * Return every content-file location where the field at `fieldPath` is\n * defined as a property of the dictionary with the given `dictionaryKey`.\n */\nconst getContentFieldLocations = async (\n dictionaryKey: string,\n fieldPath: string[],\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter(\n (dictionary) => dictionary?.key === dictionaryKey && dictionary.filePath\n );\n\n const locations: Location[] = [];\n\n await runConcurrent(dictionaries, async (dictionary) => {\n const filePath = dictionary.filePath!;\n const absolutePath = isAbsolute(filePath)\n ? filePath\n : join(config.system.baseDir, filePath);\n\n if (!existsSync(absolutePath)) return;\n\n locations.push(\n Location.create(\n pathToFileURL(absolutePath).href,\n await getFieldRangeInContentFile(absolutePath, fieldPath)\n )\n );\n });\n\n return locations;\n};\n\n// ---------------------------------------------------------------------------\n// Diagnostics — push-based (no capability needed)\n// ---------------------------------------------------------------------------\n\nconst DIAGNOSTIC_DEBOUNCE_MS = 300;\nconst diagnosticTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\n/**\n * Ask the client to build the project's dictionaries, at most once per project\n * root. The client answers by running the build, whose output trips the\n * watcher on `.intlayer/unmerged_dictionary/*.json` and invalidates the caches\n * here, which re-publishes diagnostics for every open document.\n */\nconst requestDictionaryBuild = (baseDir: string): void => {\n if (buildRequestedProjects.has(baseDir)) return;\n\n buildRequestedProjects.add(baseDir);\n log(`no built dictionary under ${baseDir} — asking the client to build`);\n connection.sendNotification(DICTIONARIES_NOT_BUILT_NOTIFICATION, {\n baseDir,\n } satisfies DictionariesNotBuiltParams);\n};\n\nconst computeDiagnostics = (\n text: string,\n config: WorkspaceConfig\n): Diagnostic[] => {\n const namespaceReferences = collectNamespaceReferences(text);\n\n if (namespaceReferences.length === 0) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const knownKeys = new Set(\n Object.values(unmergedDictionaries)\n .flat()\n .map((dictionary) => dictionary?.key)\n .filter((key): key is string => typeof key === 'string')\n );\n\n // Nothing built: the project has never run a build, or a production build\n // cleaned `.intlayer/unmerged_dictionary` without rewriting it (only dev\n // builds and editor-enabled builds write there). Every reference would be\n // flagged, so request a build rather than fill the file with false\n // positives.\n if (knownKeys.size === 0) {\n requestDictionaryBuild(config.system.baseDir);\n return [];\n }\n\n const diagnostics: Diagnostic[] = [];\n\n for (const reference of namespaceReferences) {\n if (knownKeys.has(reference.dictionaryKey)) continue;\n\n diagnostics.push({\n range: offsetToRange(\n text,\n reference.namespaceStart,\n reference.namespaceEnd\n ),\n severity: DiagnosticSeverity.Warning,\n source: 'intlayer',\n message: `Dictionary key \"${reference.dictionaryKey}\" is not declared in any content file`,\n });\n }\n\n return diagnostics;\n};\n\nconst sendDiagnosticsForDocument = (\n uri: string,\n text: string,\n version: number | null\n): void => {\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const config = getProjectConfig(absolutePath);\n\n const diagnostics =\n config && !isContentFile(uri, config)\n ? computeDiagnostics(text, config)\n : [];\n\n connection.sendDiagnostics({\n uri,\n version: version ?? undefined,\n diagnostics,\n });\n};\n\nconst scheduleDiagnostics = (\n uri: string,\n text: string,\n version: number | null\n): void => {\n const existing = diagnosticTimers.get(uri);\n\n if (existing !== undefined) clearTimeout(existing);\n\n diagnosticTimers.set(\n uri,\n setTimeout(() => {\n diagnosticTimers.delete(uri);\n sendDiagnosticsForDocument(uri, text, version);\n }, DIAGNOSTIC_DEBOUNCE_MS)\n );\n};\n\n// ---------------------------------------------------------------------------\n// Completion context detection\n// ---------------------------------------------------------------------------\n\ntype CompletionCtx =\n | { kind: 'key' }\n | { kind: 'field'; dictionaryKey: string; fieldPath: string[] }\n | null;\n\n/** Find the dictionary key for a simple variable assigned from any registered caller. */\nconst resolveVariableToDictionaryKey = (\n program: OxcNode,\n varName: string\n): string | null => {\n let result: string | null = null;\n\n walkAst(program, (node) => {\n if (result) return true;\n\n if (node['type'] !== 'VariableDeclarator') return;\n const id = node['id'] as OxcNode | undefined;\n\n if (id?.['type'] !== 'Identifier' || (id['name'] as string) !== varName)\n return;\n const init = node['init'] as OxcNode | undefined;\n\n if (!init) return;\n result = matchCallNamespace(init);\n return true;\n });\n return result;\n};\n\n/**\n * Detect what kind of completion is appropriate at the given offset.\n *\n * Handles:\n * - key completion: cursor inside first string arg of useIntlayer/getIntlayer\n * - field (member): cursor after `variable.` where variable came from useIntlayer\n * - field (destructure): cursor inside `const { | } = useIntlayer(\"key\")`\n */\nconst getCompletionContext = (\n textBefore: string,\n textAfter: string,\n program: OxcNode | null\n): CompletionCtx => {\n // Key completion\n\n if (\n new RegExp(\n `\\\\b(?:${getCallerNamesAlternation()})\\\\b(?:<[^<>]*>)?\\\\s*\\\\(\\\\s*['\"\\`][^'\"\\`\\\\n]*$`\n ).test(textBefore)\n ) {\n return { kind: 'key' };\n }\n\n // Field completion via member access: rootVar.path.to.\n const memberMatch = /\\b(\\w+)((?:\\.\\w+)*)\\.$/.exec(textBefore);\n\n if (memberMatch && program) {\n const rootVar = memberMatch[1]!;\n const chainStr = memberMatch[2]!;\n const dictKey = resolveVariableToDictionaryKey(program, rootVar);\n\n if (dictKey) {\n const fieldPath = chainStr ? chainStr.slice(1).split('.') : [];\n return { kind: 'field', dictionaryKey: dictKey, fieldPath };\n }\n }\n\n // Field completion inside destructuring: const { | } = useIntlayer(\"key\")\n\n if (/(?:const|let|var)\\s*\\{[^}]*$/.test(textBefore)) {\n const destructMatch = new RegExp(\n `^[^}]*\\\\}\\\\s*=\\\\s*(?:${getCallerNamesAlternation()})(?:<[^<>]*>)?\\\\s*\\\\(\\\\s*['\"\\`]([^'\"\\`\\\\n]+)['\"\\`]`\n ).exec(textAfter);\n\n if (destructMatch) {\n return { kind: 'field', dictionaryKey: destructMatch[1]!, fieldPath: [] };\n }\n }\n\n return null;\n};\n\nconnection.onInitialize((parameters: InitializeParams): InitializeResult => {\n workspaceRoot =\n parameters.rootPath ||\n (parameters.workspaceFolders?.[0]?.uri\n ? decodeURIComponent(new URL(parameters.workspaceFolders[0].uri).pathname)\n : undefined);\n invalidateConfigCaches();\n\n log(`initialized — workspaceRoot: ${workspaceRoot ?? '(none)'}`);\n\n if (parameters.workspaceFolders?.length) {\n log(\n `workspaceFolders: ${parameters.workspaceFolders.map((folder) => folder.name).join(', ')}`\n );\n }\n\n return {\n capabilities: {\n textDocumentSync: TextDocumentSyncKind.Incremental,\n definitionProvider: true,\n referencesProvider: true,\n hoverProvider: true,\n completionProvider: {\n triggerCharacters: ['\"', \"'\", '`', '.'],\n resolveProvider: false,\n },\n },\n };\n});\n\nconnection.onDefinition(async (parameters) => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const position = parameters.position;\n const document = documents.get(uri);\n\n if (!document) {\n log(`onDefinition — document not found in cache: ${uri}`);\n return null;\n }\n\n const text = document.getText();\n const offset = document.offsetAt(position);\n const shortenedUri = uri\n .replace(/^file:\\/\\//, '')\n .split('/')\n .slice(-2)\n .join('/');\n\n log(`onDefinition — ${shortenedUri} ${position.line}:${position.character}`);\n const config = getProjectConfig(absolutePath);\n\n if (config && isContentFile(uri, config)) {\n // Content-file → component navigation is handled exclusively by the VS Code\n // extension's intlayerContentDefinitionProvider (which returns DefinitionLink[]\n // with a proper originSelectionRange). Returning null here prevents the LSP\n // from emitting a duplicate set of Location[] results for the same request.\n log('onDefinition — content file: delegating to extension provider');\n return null;\n }\n\n // Cursor on the key string / function name → navigate to content declaration.\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n log(\n `onDefinition — useIntlayer key: \"${key}\" → searching content declarations`\n );\n const locations = await getContentDeclarationLocations(key, absolutePath);\n log(\n `onDefinition — found ${locations.length} content declaration(s) for key \"${key}\"`\n );\n return locations.length ? locations : null;\n }\n\n // Cursor on any message usage: destructured property, member access,\n // t('path.to.field') call, formatMessage({ id }), <Trans>, lingui t`…`, …\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage && usage.fieldPath.length > 0) {\n log(\n `onDefinition — usage field: \"${usage.dictionaryKey}.${usage.fieldPath.join('.')}\" (${usage.callerName}) → searching content field`\n );\n const locations = await getContentFieldLocations(\n usage.dictionaryKey,\n usage.fieldPath,\n absolutePath\n );\n log(\n `onDefinition — found ${locations.length} content file(s) with field \"${usage.fieldPath.join('.')}\"`\n );\n return locations.length ? locations : null;\n }\n\n if (usage) {\n // Dictionary-level usage (e.g. bare content variable) → content files.\n const locations = await getContentDeclarationLocations(\n usage.dictionaryKey,\n absolutePath\n );\n return locations.length ? locations : null;\n }\n\n log('onDefinition — no Intlayer key at cursor, no result');\n return null;\n});\n\n/**\n * \"Find All References\" handler (Shift+F12).\n *\n * From a content file, returns every source-file location that uses the key or\n * field at the cursor — the reverse direction of Go-to-Definition.\n *\n * From a source file, returns all content-file declarations for the key or\n * field at the cursor — complementing what Go-to-Definition already handles.\n */\nconnection.onReferences(async (parameters: ReferenceParams) => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const position = parameters.position;\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(position);\n const shortenedUri = uri\n .replace(/^file:\\/\\//, '')\n .split('/')\n .slice(-2)\n .join('/');\n\n log(`onReferences — ${shortenedUri} ${position.line}:${position.character}`);\n const config = getProjectConfig(absolutePath);\n\n if (config && isContentFile(uri, config)) {\n // Cursor on `key: \"…\"` → all useIntlayer(key) call sites\n const key = findKeyInContentFile(text, offset);\n\n if (key) {\n const locations = await getKeyUsageLocations(key, absolutePath);\n log(`onReferences — key \"${key}\" → ${locations.length} call site(s)`);\n return locations.length ? locations : null;\n }\n\n // Cursor on a content field → all field usage sites\n const extMatch = uri.match(/\\.[^.]+$/);\n const ext = extMatch ? extMatch[0] : '';\n const fieldInfo = findContentFieldAtOffset(text, offset, ext);\n\n if (fieldInfo) {\n const locations = await getFieldUsageLocations(\n fieldInfo.dictionaryKey,\n fieldInfo.fieldPath,\n absolutePath\n );\n log(\n `onReferences — field \"${fieldInfo.fieldPath.join('.')}\" → ${locations.length} usage(s)`\n );\n return locations.length ? locations : null;\n }\n\n return null;\n }\n\n // Source file: key string → content-file declarations\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n const locations = await getContentDeclarationLocations(key, absolutePath);\n log(`onReferences — key \"${key}\" → ${locations.length} declaration(s)`);\n return locations.length ? locations : null;\n }\n\n // Source file: field property, t('field') call, JSX message component, …\n // → content-file field definitions\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage && usage.fieldPath.length > 0) {\n const locations = await getContentFieldLocations(\n usage.dictionaryKey,\n usage.fieldPath,\n absolutePath\n );\n log(\n `onReferences — field \"${usage.fieldPath.join('.')}\" → ${locations.length} content definition(s)`\n );\n return locations.length ? locations : null;\n }\n\n return null;\n});\n\n// ---------------------------------------------------------------------------\n// Hover\n// ---------------------------------------------------------------------------\n\nconnection.onHover(async (parameters: HoverParams): Promise<Hover | null> => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(parameters.position);\n const config = getProjectConfig(absolutePath);\n\n if (!config) return null;\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const getDicts = (key: string) =>\n Object.values(unmergedDictionaries)\n .flat()\n .filter((d) => d?.key === key);\n\n if (isContentFile(uri, config)) {\n // Cursor on `key: \"...\"` declaration\n const key = findKeyInContentFile(text, offset);\n\n if (key) {\n const dicts = getDicts(key);\n\n if (dicts.length > 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, key),\n },\n };\n }\n }\n\n // Cursor on a content field\n const ext = uri.match(/\\.[^.]+$/)?.[0] ?? '';\n const fieldInfo = findContentFieldAtOffset(text, offset, ext);\n\n if (fieldInfo) {\n const dicts = getDicts(fieldInfo.dictionaryKey);\n\n if (dicts.length > 0) {\n const hoverText = formatFieldHover(\n dicts,\n fieldInfo.dictionaryKey,\n fieldInfo.fieldPath\n );\n\n if (hoverText) {\n return {\n contents: { kind: MarkupKind.Markdown, value: hoverText },\n };\n }\n }\n }\n\n return null;\n }\n\n // Source file: cursor on key string inside useIntlayer(\"key\")\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n const dicts = getDicts(key);\n\n if (dicts.length > 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, key),\n },\n };\n }\n return null;\n }\n\n // Source file: cursor on any message usage (field property, t('field'),\n // formatMessage({ id }), <Trans>, lingui t`…`, …)\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage) {\n const dicts = getDicts(usage.dictionaryKey);\n\n if (dicts.length > 0) {\n if (usage.fieldPath.length === 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, usage.dictionaryKey),\n },\n };\n }\n\n const hoverText = formatFieldHover(\n dicts,\n usage.dictionaryKey,\n usage.fieldPath\n );\n\n if (hoverText) {\n return {\n contents: { kind: MarkupKind.Markdown, value: hoverText },\n };\n }\n }\n }\n\n return null;\n});\n\n// ---------------------------------------------------------------------------\n// Completion\n// ---------------------------------------------------------------------------\n\nconnection.onCompletion(\n async (parameters: CompletionParams): Promise<CompletionItem[] | null> => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(parameters.position);\n const config = getProjectConfig(absolutePath);\n\n if (!config) return null;\n\n const program = parseText(text);\n const context = getCompletionContext(\n text.slice(0, offset),\n text.slice(offset),\n program\n );\n\n if (!context) return null;\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n\n if (context.kind === 'key') {\n const seen = new Set<string>();\n const items: CompletionItem[] = [];\n for (const dictionary of Object.values(unmergedDictionaries).flat()) {\n if (!dictionary?.key || seen.has(dictionary.key)) continue;\n\n seen.add(dictionary.key);\n items.push({\n label: dictionary.key,\n kind: CompletionItemKind.Value,\n detail: dictionary.title ?? undefined,\n documentation: dictionary.description\n ? { kind: MarkupKind.Markdown, value: dictionary.description }\n : undefined,\n });\n }\n log(`onCompletion — key: ${items.length} dictionary keys`);\n return items.length > 0 ? items : null;\n }\n\n if (context.kind === 'field') {\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter((dictionary) => dictionary?.key === context.dictionaryKey);\n\n if (dictionaries.length === 0) return null;\n\n const primary = dictionaries[0]!;\n const fields = getFieldsAtPath(primary.content, context.fieldPath);\n const items: CompletionItem[] = fields.map((fieldName) => {\n const value = getFieldByPath(primary.content, [\n ...context.fieldPath,\n fieldName,\n ]);\n return {\n label: fieldName,\n kind: CompletionItemKind.Field,\n detail: formatFieldValue(value),\n };\n });\n log(\n `onCompletion — field \"${context.dictionaryKey}.${context.fieldPath.join('.')}\": ${items.length} field(s)`\n );\n return items.length > 0 ? items : null;\n }\n\n return null;\n }\n);\n\n// ---------------------------------------------------------------------------\n// Push diagnostics\n// ---------------------------------------------------------------------------\n\ndocuments.onDidOpen((event) => {\n sendDiagnosticsForDocument(\n event.document.uri,\n event.document.getText(),\n event.document.version\n );\n});\n\ndocuments.onDidChangeContent((event) => {\n scheduleDiagnostics(\n event.document.uri,\n event.document.getText(),\n event.document.version\n );\n});\n\ndocuments.onDidClose((event) => {\n const uri = event.document.uri;\n const timer = diagnosticTimers.get(uri);\n\n if (timer !== undefined) {\n clearTimeout(timer);\n diagnosticTimers.delete(uri);\n }\n connection.sendDiagnostics({ uri, diagnostics: [] });\n});\n\n// Invalidate the source-file list whenever the client reports a file change.\n// The client registers a watcher for *.content.* files; any creation or\n// deletion means the project layout may have changed.\nconnection.onDidChangeWatchedFiles(() => {\n invalidateConfigCaches();\n log(\n 'watched files changed — source file list and dictionary cache invalidated'\n );\n\n // Diagnostics are only recomputed on open/change, so a key reported as\n // undeclared would stay flagged after its content file (or the built\n // dictionaries) appear. Re-publish for every open document.\n for (const document of documents.all()) {\n scheduleDiagnostics(document.uri, document.getText(), document.version);\n }\n});\n\n// Make the text document manager listen on the connection\n// for open, change and close text document events\ndocuments.listen(connection);\n\n// Listen on the connection\nconnection.listen();\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport {\n getConfiguration,\n searchConfigurationFile,\n} from '@intlayer/config/node';\nimport { clearAllCache } from '@intlayer/config/utils';\nimport { getUnmergedDictionaries } from '@intlayer/dictionaries-entry/unmerged';\nimport { buildComponentFilesList } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport {\n type CompletionItem,\n CompletionItemKind,\n type CompletionParams,\n createConnection,\n type Diagnostic,\n DiagnosticSeverity,\n type Hover,\n type HoverParams,\n type InitializeParams,\n type InitializeResult,\n Location,\n MarkupKind,\n Position,\n ProposedFeatures,\n Range,\n type ReferenceParams,\n TextDocumentSyncKind,\n TextDocuments,\n} from 'vscode-languageserver/node';\nimport { TextDocument } from 'vscode-languageserver-textdocument';\nimport {\n buildCallerWithKeyPattern,\n buildKeyUsagePatterns,\n getCallerNamesAlternation,\n} from './callers/patterns';\nimport {\n formatDictionaryHover,\n formatFieldHover,\n formatFieldValue,\n getFieldByPath,\n getFieldsAtPath,\n} from './dictionaryUtils';\nimport { findFieldDeclaration } from './findFieldDeclaration';\nimport {\n escapeRegularExpression,\n findFieldRangesInFile,\n offsetToRange,\n} from './findFieldInFile';\nimport { findKeyAtOffset } from './findKeyAtOffset';\nimport {\n findContentFieldAtOffset,\n findKeyInContentFile,\n} from './findKeyInContentFile';\nimport { type OxcNode, parseText, walkAst } from './oxcUtils';\nimport {\n DICTIONARIES_NOT_BUILT_NOTIFICATION,\n type DictionariesNotBuiltParams,\n} from './protocol';\nimport {\n type ResolvedDictionaryTarget,\n resolveDictionaryTarget,\n type UsageTarget,\n} from './resolveDictionaryTarget';\nimport {\n collectNamespaceReferences,\n findMessageUsageAtOffset,\n matchCallNamespace,\n} from './usageAnalyzer';\n\n// Create a connection for the server, using Node's IPC as a transport.\n// Also include all preview / proposed LSP features.\nconst connection = createConnection(ProposedFeatures.all);\n\n// Create a simple text document manager.\nconst documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);\n\n/**\n * Send a log line to the client's Output panel (View → Output → \"Intlayer LSP\").\n * Uses connection.console.log so it travels over the JSON-RPC channel and never\n * touches stdout (which is reserved for the LSP protocol).\n */\nconst log = (message: string): void =>\n connection.console.log(`[Intlayer LSP] ${message}`);\n\nlet workspaceRoot: string | null | undefined;\n\ntype WorkspaceConfig = Pick<\n IntlayerConfig,\n 'system' | 'build' | 'content' | 'compiler' | 'internationalization'\n>;\n\n/**\n * Resolve (and cache) the Intlayer configuration for the current workspace.\n * The LSP process cwd is not the project root, so we must resolve the config\n * from `workspaceRoot` to get the right `system`/`build`/`content` paths.\n */\nconst projectConfigs = new Map<string, WorkspaceConfig>();\n\ntype DictionariesCache = {\n dictionaries: ReturnType<typeof getUnmergedDictionaries>;\n cachedAt: number;\n};\nconst projectDictionaries = new Map<string, DictionariesCache>();\nconst DICTIONARY_CACHE_TTL_MS = 5_000;\n\ntype SourceFilesCache = {\n files: string[];\n cachedAt: number;\n};\nconst projectSourceFiles = new Map<string, SourceFilesCache>();\nconst SOURCE_FILES_CACHE_TTL_MS = 30_000;\n\n/**\n * Directory of the file → directory holding the closest intlayer config.\n * Populated lazily by `getProjectRoot`.\n */\nconst projectRoots = new Map<string, string>();\n\n/**\n * Project roots already reported to the client as having no built dictionary.\n * Keeps the notification to one per project instead of one per open document;\n * cleared on invalidation so a later regression is reported again.\n */\nconst buildRequestedProjects = new Set<string>();\n\nconst invalidateConfigCaches = () => {\n // `getConfiguration` memoizes by options: without this a changed\n // configuration file would keep resolving to its previous evaluation.\n clearAllCache();\n projectConfigs.clear();\n projectDictionaries.clear();\n projectRoots.clear();\n buildRequestedProjects.clear();\n\n projectSourceFiles.clear();\n};\n\n/**\n * Resolve the project root for a file — the directory that holds the closest\n * intlayer config file.\n *\n * `getConfiguration({ baseDir })` treats `baseDir` as the project root and\n * derives every `system.*` path from it, so passing the file's own directory\n * would resolve `.intlayer/` next to the file (e.g. `src/.intlayer/…`) instead\n * of at the project root — leaving every dictionary lookup empty.\n */\nconst getProjectRoot = (fileDirectory: string): string => {\n const cached = projectRoots.get(fileDirectory);\n\n if (cached !== undefined) return cached;\n\n const { configurationFilePath } = searchConfigurationFile(fileDirectory);\n const root = configurationFilePath\n ? dirname(configurationFilePath)\n : (workspaceRoot ?? fileDirectory);\n\n projectRoots.set(fileDirectory, root);\n return root;\n};\n\nconst getProjectConfig = (absolutePath: string): WorkspaceConfig | null => {\n try {\n // Find the closest intlayer config relative to the file.\n const baseDir = getProjectRoot(dirname(absolutePath));\n const { system, build, content, compiler, internationalization } =\n getConfiguration({ baseDir });\n const cacheKey = system.baseDir;\n\n if (!projectConfigs.has(cacheKey)) {\n projectConfigs.set(cacheKey, {\n system,\n build,\n content,\n compiler,\n internationalization,\n });\n log(\n `getProjectConfig — OK baseDir=${system.baseDir} unmergedDictionariesDir=${system.unmergedDictionariesDir}`\n );\n }\n return projectConfigs.get(cacheKey)!;\n } catch (error) {\n log(`getProjectConfig — FAILED: ${error}`);\n return null;\n }\n};\n\nconst getUnmergedDictionariesCached = (\n config: WorkspaceConfig\n): ReturnType<typeof getUnmergedDictionaries> => {\n const cacheKey = config.system.baseDir;\n const cached = projectDictionaries.get(cacheKey);\n const now = Date.now();\n\n if (cached && now - cached.cachedAt < DICTIONARY_CACHE_TTL_MS) {\n return cached.dictionaries;\n }\n\n const dictionaries = getUnmergedDictionaries(config);\n projectDictionaries.set(cacheKey, { dictionaries, cachedAt: now });\n return dictionaries;\n};\n\n// ---------------------------------------------------------------------------\n// Pattern helpers — derived lazily from the live config.\n// ---------------------------------------------------------------------------\n\nconst isContentFile = (uri: string, config: WorkspaceConfig): boolean => {\n const fileExtensions = config.content.fileExtensions ?? [];\n return fileExtensions.some((extension) => uri.endsWith(extension));\n};\n\n// ---------------------------------------------------------------------------\n// Async I/O helpers\n// ---------------------------------------------------------------------------\n\n/** Read a file as UTF-8, returning null on any error. */\nconst tryReadFile = async (filePath: string): Promise<string | null> => {\n try {\n return await readFile(filePath, 'utf-8');\n } catch {\n return null;\n }\n};\n\n/**\n * Run `callback` over every item with at most `limit` tasks in-flight at once.\n * JS is single-threaded so `results.push` inside callback is race-free.\n */\nconst runConcurrent = async <T>(\n items: T[],\n callback: (item: T) => Promise<void>,\n limit = 50\n): Promise<void> => {\n if (items.length === 0) return;\n let index = 0;\n const worker = async () => {\n while (index < items.length) {\n await callback(items[index++]!);\n }\n };\n await Promise.all(\n Array.from({ length: Math.min(limit, items.length) }, worker)\n );\n};\n\n/** Built dictionaries registered under `dictionaryKey`. */\nconst getDictionariesForKey = (\n config: WorkspaceConfig,\n dictionaryKey: string\n): Dictionary[] =>\n (\n getUnmergedDictionariesCached(config) as Record<\n string,\n Dictionary[] | undefined\n >\n )[dictionaryKey] ?? [];\n\n/**\n * Match a usage (or a bare dictionary key) against the built dictionaries,\n * following the compat adapters' fallbacks (`index` whole-file catalog, flat\n * dotted keys) — see `resolveDictionaryTarget`.\n */\nconst resolveTarget = (\n target: UsageTarget,\n config: WorkspaceConfig\n): Promise<ResolvedDictionaryTarget<Dictionary> | null> =>\n resolveDictionaryTarget(target, (dictionaryKey) =>\n getDictionariesForKey(config, dictionaryKey)\n );\n\n/**\n * Where a resolved target is declared: one location per source file (content\n * file, or one JSON catalog per locale), the default locale first so VS Code\n * previews it.\n */\nconst getDeclarationLocations = async (\n target: UsageTarget,\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const resolved = await resolveTarget(target, config);\n\n if (!resolved) return [];\n\n const defaultLocale = config.internationalization?.defaultLocale;\n const sourcePaths = [\n ...new Set(\n [...resolved.dictionaries]\n .sort(\n (first, second) =>\n Number(second.locale === defaultLocale) -\n Number(first.locale === defaultLocale)\n )\n .map((dictionary) => dictionary.filePath)\n .filter((filePath): filePath is string => Boolean(filePath))\n .map((filePath) =>\n isAbsolute(filePath)\n ? filePath\n : join(config.system.baseDir, filePath)\n )\n ),\n ];\n\n const locations = await Promise.all(\n sourcePaths.map(async (sourcePath) => {\n const text = await tryReadFile(sourcePath);\n\n if (text === null) return null;\n\n const span = findFieldDeclaration(\n text,\n sourcePath,\n resolved.dictionaryKey,\n resolved.fieldPath\n );\n const range = span\n ? offsetToRange(text, span.start, span.end)\n : Range.create(Position.create(0, 0), Position.create(0, 0));\n\n return Location.create(pathToFileURL(sourcePath).href, range);\n })\n );\n\n return locations.filter(\n (location): location is Location => location !== null\n );\n};\n\n/**\n * Return the cached source-file list, re-scanning only when the cache has\n * expired or been explicitly invalidated (config change, watched-file event).\n */\nconst getSourceFiles = async (\n directory: string,\n config: WorkspaceConfig\n): Promise<string[]> => {\n const now = Date.now();\n const cached = projectSourceFiles.get(directory);\n\n if (cached && now - cached.cachedAt < SOURCE_FILES_CACHE_TTL_MS) {\n return cached.files;\n }\n const startTime = Date.now();\n const files = buildComponentFilesList(config as unknown as IntlayerConfig, [\n '**/.output/**',\n '**/.svelte-kit/**',\n '**/src-tauri/**',\n '**/.next/**',\n '**/.nuxt/**',\n '**/.expo/**',\n '**/.vercel/**',\n '**/.turbo/**',\n '**/.tanstack/**',\n '**/dist/**',\n '**/build/**',\n '**/node_modules/**',\n ]);\n projectSourceFiles.set(directory, { files, cachedAt: Date.now() });\n log(\n `source files scanned: ${files.length} file(s) in ${Date.now() - startTime}ms`\n );\n return files;\n};\n\n/**\n * Return every source-file location that calls useIntlayer / getIntlayer with\n * the given key.\n */\nconst getKeyUsageLocations = async (\n key: string,\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const usagePatterns = buildKeyUsagePatterns(key);\n\n const filePaths = await getSourceFiles(config.system.baseDir, config);\n const locations: Location[] = [];\n const startTime = Date.now();\n\n await runConcurrent(filePaths, async (filePath) => {\n const text = await tryReadFile(filePath);\n // Quick string check before running the regexes — skips the vast majority of files\n\n if (!text?.includes(key)) return;\n\n const seenStarts = new Set<number>();\n\n for (const usagePattern of usagePatterns) {\n for (const match of text.matchAll(usagePattern)) {\n const matchStart = match.index!;\n\n if (seenStarts.has(matchStart)) continue;\n\n seenStarts.add(matchStart);\n const matchEnd = matchStart + match[0].length;\n locations.push(\n Location.create(\n pathToFileURL(filePath).href,\n offsetToRange(text, matchStart, matchEnd)\n )\n );\n }\n }\n });\n\n log(\n `getKeyUsageLocations(\"${key}\") — ${filePaths.length} file(s) searched in ${Date.now() - startTime}ms → ${locations.length} hit(s)`\n );\n return locations;\n};\n\n/**\n * Return every source-file location that accesses the field at `fieldPath` on\n * the result of `useIntlayer(dictionaryKey)` / `getIntlayer(dictionaryKey)`.\n *\n * `fieldPath` is an array of property names from the content root to the\n * target field (e.g. `['searchInput', 'text']` for a nested field).\n * Single-element paths (e.g. `['greet']`) behave like the previous\n * single-`fieldName` API.\n */\nconst getFieldUsageLocations = async (\n dictionaryKey: string,\n fieldPath: string[],\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config || fieldPath.length === 0) return [];\n\n const leafFieldName = fieldPath[fieldPath.length - 1]!;\n const escapedKey = escapeRegularExpression(dictionaryKey);\n const escapedLeaf = escapeRegularExpression(leafFieldName);\n const useCallRegularExpression = buildCallerWithKeyPattern(dictionaryKey);\n\n const filePaths = await getSourceFiles(config.system.baseDir, config);\n const locations: Location[] = [];\n const startTime = Date.now();\n\n await runConcurrent(filePaths, async (filePath) => {\n const text = await tryReadFile(filePath);\n\n if (!text) return;\n // Pre-filter: dictionaryKey and every segment of the path must appear in the file\n\n if (!text.includes(dictionaryKey)) return;\n\n if (fieldPath.some((segment) => !text.includes(segment))) return;\n\n if (!useCallRegularExpression.test(text)) return;\n\n for (const range of findFieldRangesInFile(\n text,\n escapedKey,\n leafFieldName,\n escapedLeaf,\n fieldPath\n )) {\n locations.push(Location.create(pathToFileURL(filePath).href, range));\n }\n });\n\n log(\n `getFieldUsageLocations(\"${dictionaryKey}.${fieldPath.join('.')}\") — ${filePaths.length} file(s) searched in ${Date.now() - startTime}ms → ${locations.length} hit(s)`\n );\n return locations;\n};\n\n// ---------------------------------------------------------------------------\n// Diagnostics — push-based (no capability needed)\n// ---------------------------------------------------------------------------\n\nconst DIAGNOSTIC_DEBOUNCE_MS = 300;\nconst diagnosticTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\n/**\n * Ask the client to build the project's dictionaries, at most once per project\n * root. The client answers by running the build, whose output trips the\n * watcher on `.intlayer/unmerged_dictionary/*.json` and invalidates the caches\n * here, which re-publishes diagnostics for every open document.\n */\nconst requestDictionaryBuild = (baseDir: string): void => {\n if (buildRequestedProjects.has(baseDir)) return;\n\n buildRequestedProjects.add(baseDir);\n log(`no built dictionary under ${baseDir} — asking the client to build`);\n connection.sendNotification(DICTIONARIES_NOT_BUILT_NOTIFICATION, {\n baseDir,\n } satisfies DictionariesNotBuiltParams);\n};\n\nconst computeDiagnostics = (\n text: string,\n config: WorkspaceConfig\n): Diagnostic[] => {\n const namespaceReferences = collectNamespaceReferences(text);\n\n if (namespaceReferences.length === 0) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const knownKeys = new Set(\n Object.values(unmergedDictionaries)\n .flat()\n .map((dictionary) => dictionary?.key)\n .filter((key): key is string => typeof key === 'string')\n );\n\n // Nothing built: the project has never run a build, or a production build\n // cleaned `.intlayer/unmerged_dictionary` without rewriting it (only dev\n // builds and editor-enabled builds write there). Every reference would be\n // flagged, so request a build rather than fill the file with false\n // positives.\n if (knownKeys.size === 0) {\n requestDictionaryBuild(config.system.baseDir);\n return [];\n }\n\n const diagnostics: Diagnostic[] = [];\n\n for (const reference of namespaceReferences) {\n if (knownKeys.has(reference.dictionaryKey)) continue;\n\n diagnostics.push({\n range: offsetToRange(\n text,\n reference.namespaceStart,\n reference.namespaceEnd\n ),\n severity: DiagnosticSeverity.Warning,\n source: 'intlayer',\n message: `Dictionary key \"${reference.dictionaryKey}\" is not declared in any content file`,\n });\n }\n\n return diagnostics;\n};\n\nconst sendDiagnosticsForDocument = (\n uri: string,\n text: string,\n version: number | null\n): void => {\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const config = getProjectConfig(absolutePath);\n\n const diagnostics =\n config && !isContentFile(uri, config)\n ? computeDiagnostics(text, config)\n : [];\n\n connection.sendDiagnostics({\n uri,\n version: version ?? undefined,\n diagnostics,\n });\n};\n\nconst scheduleDiagnostics = (\n uri: string,\n text: string,\n version: number | null\n): void => {\n const existing = diagnosticTimers.get(uri);\n\n if (existing !== undefined) clearTimeout(existing);\n\n diagnosticTimers.set(\n uri,\n setTimeout(() => {\n diagnosticTimers.delete(uri);\n sendDiagnosticsForDocument(uri, text, version);\n }, DIAGNOSTIC_DEBOUNCE_MS)\n );\n};\n\n// ---------------------------------------------------------------------------\n// Completion context detection\n// ---------------------------------------------------------------------------\n\ntype CompletionCtx =\n | { kind: 'key' }\n | { kind: 'field'; dictionaryKey: string; fieldPath: string[] }\n | null;\n\n/** Find the dictionary key for a simple variable assigned from any registered caller. */\nconst resolveVariableToDictionaryKey = (\n program: OxcNode,\n varName: string\n): string | null => {\n let result: string | null = null;\n\n walkAst(program, (node) => {\n if (result) return true;\n\n if (node['type'] !== 'VariableDeclarator') return;\n const id = node['id'] as OxcNode | undefined;\n\n if (id?.['type'] !== 'Identifier' || (id['name'] as string) !== varName)\n return;\n const init = node['init'] as OxcNode | undefined;\n\n if (!init) return;\n result = matchCallNamespace(init);\n return true;\n });\n return result;\n};\n\n/**\n * Detect what kind of completion is appropriate at the given offset.\n *\n * Handles:\n * - key completion: cursor inside first string arg of useIntlayer/getIntlayer\n * - field (member): cursor after `variable.` where variable came from useIntlayer\n * - field (destructure): cursor inside `const { | } = useIntlayer(\"key\")`\n */\nconst getCompletionContext = (\n textBefore: string,\n textAfter: string,\n program: OxcNode | null\n): CompletionCtx => {\n // Key completion\n\n if (\n new RegExp(\n `\\\\b(?:${getCallerNamesAlternation()})\\\\b(?:<[^<>]*>)?\\\\s*\\\\(\\\\s*['\"\\`][^'\"\\`\\\\n]*$`\n ).test(textBefore)\n ) {\n return { kind: 'key' };\n }\n\n // Field completion via member access: rootVar.path.to.\n const memberMatch = /\\b(\\w+)((?:\\.\\w+)*)\\.$/.exec(textBefore);\n\n if (memberMatch && program) {\n const rootVar = memberMatch[1]!;\n const chainStr = memberMatch[2]!;\n const dictKey = resolveVariableToDictionaryKey(program, rootVar);\n\n if (dictKey) {\n const fieldPath = chainStr ? chainStr.slice(1).split('.') : [];\n return { kind: 'field', dictionaryKey: dictKey, fieldPath };\n }\n }\n\n // Field completion inside destructuring: const { | } = useIntlayer(\"key\")\n\n if (/(?:const|let|var)\\s*\\{[^}]*$/.test(textBefore)) {\n const destructMatch = new RegExp(\n `^[^}]*\\\\}\\\\s*=\\\\s*(?:${getCallerNamesAlternation()})(?:<[^<>]*>)?\\\\s*\\\\(\\\\s*['\"\\`]([^'\"\\`\\\\n]+)['\"\\`]`\n ).exec(textAfter);\n\n if (destructMatch) {\n return { kind: 'field', dictionaryKey: destructMatch[1]!, fieldPath: [] };\n }\n }\n\n return null;\n};\n\nconnection.onInitialize((parameters: InitializeParams): InitializeResult => {\n workspaceRoot =\n parameters.rootPath ||\n (parameters.workspaceFolders?.[0]?.uri\n ? decodeURIComponent(new URL(parameters.workspaceFolders[0].uri).pathname)\n : undefined);\n invalidateConfigCaches();\n\n log(`initialized — workspaceRoot: ${workspaceRoot ?? '(none)'}`);\n\n if (parameters.workspaceFolders?.length) {\n log(\n `workspaceFolders: ${parameters.workspaceFolders.map((folder) => folder.name).join(', ')}`\n );\n }\n\n return {\n capabilities: {\n textDocumentSync: TextDocumentSyncKind.Incremental,\n definitionProvider: true,\n referencesProvider: true,\n hoverProvider: true,\n completionProvider: {\n triggerCharacters: ['\"', \"'\", '`', '.'],\n resolveProvider: false,\n },\n },\n };\n});\n\nconnection.onDefinition(async (parameters) => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const position = parameters.position;\n const document = documents.get(uri);\n\n if (!document) {\n log(`onDefinition — document not found in cache: ${uri}`);\n return null;\n }\n\n const text = document.getText();\n const offset = document.offsetAt(position);\n const shortenedUri = uri\n .replace(/^file:\\/\\//, '')\n .split('/')\n .slice(-2)\n .join('/');\n\n log(`onDefinition — ${shortenedUri} ${position.line}:${position.character}`);\n const config = getProjectConfig(absolutePath);\n\n if (config && isContentFile(uri, config)) {\n // Content-file → component navigation is handled exclusively by the VS Code\n // extension's intlayerContentDefinitionProvider (which returns DefinitionLink[]\n // with a proper originSelectionRange). Returning null here prevents the LSP\n // from emitting a duplicate set of Location[] results for the same request.\n log('onDefinition — content file: delegating to extension provider');\n return null;\n }\n\n // Cursor on the key string / function name → navigate to content declaration.\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n log(\n `onDefinition — useIntlayer key: \"${key}\" → searching content declarations`\n );\n const locations = await getDeclarationLocations(\n { dictionaryKey: key, fieldPath: [] },\n absolutePath\n );\n log(\n `onDefinition — found ${locations.length} content declaration(s) for key \"${key}\"`\n );\n return locations.length ? locations : null;\n }\n\n // Cursor on any message usage: destructured property, member access,\n // t('path.to.field') call, formatMessage({ id }), <Trans>, lingui t`…`, …\n const usage = findMessageUsageAtOffset(text, offset);\n\n // Dictionary-level usages (a bare content variable) land on the dictionary.\n if (usage) {\n log(\n `onDefinition — usage: \"${[usage.dictionaryKey, ...usage.fieldPath].join('.')}\" (${usage.callerName}) → searching declaration`\n );\n const locations = await getDeclarationLocations(usage, absolutePath);\n log(`onDefinition — found ${locations.length} declaration(s)`);\n return locations.length ? locations : null;\n }\n\n log('onDefinition — no Intlayer key at cursor, no result');\n return null;\n});\n\n/**\n * \"Find All References\" handler (Shift+F12).\n *\n * From a content file, returns every source-file location that uses the key or\n * field at the cursor — the reverse direction of Go-to-Definition.\n *\n * From a source file, returns all content-file declarations for the key or\n * field at the cursor — complementing what Go-to-Definition already handles.\n */\nconnection.onReferences(async (parameters: ReferenceParams) => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const position = parameters.position;\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(position);\n const shortenedUri = uri\n .replace(/^file:\\/\\//, '')\n .split('/')\n .slice(-2)\n .join('/');\n\n log(`onReferences — ${shortenedUri} ${position.line}:${position.character}`);\n const config = getProjectConfig(absolutePath);\n\n if (config && isContentFile(uri, config)) {\n // Cursor on `key: \"…\"` → all useIntlayer(key) call sites\n const key = findKeyInContentFile(text, offset);\n\n if (key) {\n const locations = await getKeyUsageLocations(key, absolutePath);\n log(`onReferences — key \"${key}\" → ${locations.length} call site(s)`);\n return locations.length ? locations : null;\n }\n\n // Cursor on a content field → all field usage sites\n const extMatch = uri.match(/\\.[^.]+$/);\n const ext = extMatch ? extMatch[0] : '';\n const fieldInfo = findContentFieldAtOffset(text, offset, ext);\n\n if (fieldInfo) {\n const locations = await getFieldUsageLocations(\n fieldInfo.dictionaryKey,\n fieldInfo.fieldPath,\n absolutePath\n );\n log(\n `onReferences — field \"${fieldInfo.fieldPath.join('.')}\" → ${locations.length} usage(s)`\n );\n return locations.length ? locations : null;\n }\n\n return null;\n }\n\n // Source file: key string → content-file declarations\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n const locations = await getDeclarationLocations(\n { dictionaryKey: key, fieldPath: [] },\n absolutePath\n );\n log(`onReferences — key \"${key}\" → ${locations.length} declaration(s)`);\n return locations.length ? locations : null;\n }\n\n // Source file: field property, t('field') call, JSX message component, …\n // → content-file field definitions\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage && usage.fieldPath.length > 0) {\n const locations = await getDeclarationLocations(usage, absolutePath);\n log(\n `onReferences — field \"${usage.fieldPath.join('.')}\" → ${locations.length} content definition(s)`\n );\n return locations.length ? locations : null;\n }\n\n return null;\n});\n\n// ---------------------------------------------------------------------------\n// Hover\n// ---------------------------------------------------------------------------\n\nconnection.onHover(async (parameters: HoverParams): Promise<Hover | null> => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(parameters.position);\n const config = getProjectConfig(absolutePath);\n\n if (!config) return null;\n\n if (isContentFile(uri, config)) {\n // Cursor on `key: \"...\"` declaration\n const key = findKeyInContentFile(text, offset);\n\n if (key) {\n const dicts = getDictionariesForKey(config, key);\n\n if (dicts.length > 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, key),\n },\n };\n }\n }\n\n // Cursor on a content field\n const ext = uri.match(/\\.[^.]+$/)?.[0] ?? '';\n const fieldInfo = findContentFieldAtOffset(text, offset, ext);\n\n if (fieldInfo) {\n const dicts = getDictionariesForKey(config, fieldInfo.dictionaryKey);\n\n if (dicts.length > 0) {\n const hoverText = formatFieldHover(\n dicts,\n fieldInfo.dictionaryKey,\n fieldInfo.fieldPath\n );\n\n if (hoverText) {\n return {\n contents: { kind: MarkupKind.Markdown, value: hoverText },\n };\n }\n }\n }\n\n return null;\n }\n\n // Source file: cursor on key string inside useIntlayer(\"key\")\n const key = findKeyAtOffset(text, offset);\n\n // Source file: cursor on the key string inside useIntlayer(\"key\"), or on\n // any message usage (field property, t('field'), formatMessage({ id }),\n // <Trans>, lingui t`…`, …)\n const target: UsageTarget | null = key\n ? { dictionaryKey: key, fieldPath: [] }\n : findMessageUsageAtOffset(text, offset);\n\n if (!target) return null;\n\n const resolved = await resolveTarget(target, config);\n\n if (!resolved) return null;\n\n if (resolved.fieldPath.length === 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(\n resolved.dictionaries,\n resolved.dictionaryKey\n ),\n },\n };\n }\n\n const hoverText = formatFieldHover(\n resolved.dictionaries,\n resolved.dictionaryKey,\n resolved.fieldPath\n );\n\n return hoverText\n ? { contents: { kind: MarkupKind.Markdown, value: hoverText } }\n : null;\n});\n\n// ---------------------------------------------------------------------------\n// Completion\n// ---------------------------------------------------------------------------\n\nconnection.onCompletion(\n async (parameters: CompletionParams): Promise<CompletionItem[] | null> => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(parameters.position);\n const config = getProjectConfig(absolutePath);\n\n if (!config) return null;\n\n const program = parseText(text);\n const context = getCompletionContext(\n text.slice(0, offset),\n text.slice(offset),\n program\n );\n\n if (!context) return null;\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n\n if (context.kind === 'key') {\n const seen = new Set<string>();\n const items: CompletionItem[] = [];\n for (const dictionary of Object.values(unmergedDictionaries).flat()) {\n if (!dictionary?.key || seen.has(dictionary.key)) continue;\n\n seen.add(dictionary.key);\n items.push({\n label: dictionary.key,\n kind: CompletionItemKind.Value,\n detail: dictionary.title ?? undefined,\n documentation: dictionary.description\n ? { kind: MarkupKind.Markdown, value: dictionary.description }\n : undefined,\n });\n }\n log(`onCompletion — key: ${items.length} dictionary keys`);\n return items.length > 0 ? items : null;\n }\n\n if (context.kind === 'field') {\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter((dictionary) => dictionary?.key === context.dictionaryKey);\n\n if (dictionaries.length === 0) return null;\n\n const primary = dictionaries[0]!;\n const fields = getFieldsAtPath(primary.content, context.fieldPath);\n const items: CompletionItem[] = fields.map((fieldName) => {\n const value = getFieldByPath(primary.content, [\n ...context.fieldPath,\n fieldName,\n ]);\n return {\n label: fieldName,\n kind: CompletionItemKind.Field,\n detail: formatFieldValue(value),\n };\n });\n log(\n `onCompletion — field \"${context.dictionaryKey}.${context.fieldPath.join('.')}\": ${items.length} field(s)`\n );\n return items.length > 0 ? items : null;\n }\n\n return null;\n }\n);\n\n// ---------------------------------------------------------------------------\n// Push diagnostics\n// ---------------------------------------------------------------------------\n\ndocuments.onDidOpen((event) => {\n sendDiagnosticsForDocument(\n event.document.uri,\n event.document.getText(),\n event.document.version\n );\n});\n\ndocuments.onDidChangeContent((event) => {\n scheduleDiagnostics(\n event.document.uri,\n event.document.getText(),\n event.document.version\n );\n});\n\ndocuments.onDidClose((event) => {\n const uri = event.document.uri;\n const timer = diagnosticTimers.get(uri);\n\n if (timer !== undefined) {\n clearTimeout(timer);\n diagnosticTimers.delete(uri);\n }\n connection.sendDiagnostics({ uri, diagnostics: [] });\n});\n\n// Invalidate the source-file list whenever the client reports a file change.\n// The client registers a watcher for *.content.* files; any creation or\n// deletion means the project layout may have changed.\nconnection.onDidChangeWatchedFiles(() => {\n invalidateConfigCaches();\n log(\n 'watched files changed — source file list and dictionary cache invalidated'\n );\n\n // Diagnostics are only recomputed on open/change, so a key reported as\n // undeclared would stay flagged after its content file (or the built\n // dictionaries) appear. Re-publish for every open document.\n for (const document of documents.all()) {\n scheduleDiagnostics(document.uri, document.getText(), document.version);\n }\n});\n\n// Make the text document manager listen on the connection\n// for open, change and close text document events\ndocuments.listen(connection);\n\n// Listen on the connection\nconnection.listen();\n"],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ import { getLibraryRootDictionaryKey } from "./usageAnalyzer.mjs";
2
+
3
+ //#region src/resolveDictionaryTarget.ts
4
+ /**
5
+ * Dictionary produced by a JSON source holding a whole namespace-less catalog
6
+ * (`./locales/{{locale}}.json`), which i18next's default namespace resolves to.
7
+ */
8
+ const WHOLE_FILE_DICTIONARY_KEY = "index";
9
+ /** Unwrap an intlayer translation node down to one locale's value. */
10
+ const unwrapTranslationNode = (node) => {
11
+ if (!node || typeof node !== "object") return node;
12
+ const record = node;
13
+ if (record["nodeType"] !== "translation") return node;
14
+ const translations = record["translation"];
15
+ if (!translations || typeof translations !== "object") return node;
16
+ return Object.values(translations)[0];
17
+ };
18
+ /**
19
+ * Walk `segments` through `content`, letting one property consume several
20
+ * segments when its name is their dotted join — catalogs mix nested objects
21
+ * and flat dotted keys (`{ settings: { 'profile.title': … } }`).
22
+ *
23
+ * @returns The property names actually traversed, or `null` when absent.
24
+ */
25
+ const findContentPath = (content, segments) => {
26
+ if (segments.length === 0) return [];
27
+ const node = unwrapTranslationNode(content);
28
+ if (!node || typeof node !== "object") return null;
29
+ const record = node;
30
+ for (let length = 1; length <= segments.length; length++) {
31
+ const propertyName = segments.slice(0, length).join(".");
32
+ if (!Object.hasOwn(record, propertyName)) continue;
33
+ const rest = findContentPath(record[propertyName], segments.slice(length));
34
+ if (rest) return [propertyName, ...rest];
35
+ }
36
+ return null;
37
+ };
38
+ /**
39
+ * Every dictionary/path pair a usage may read, most specific first.
40
+ *
41
+ * The analyzer guesses the dictionary from the source alone: a root-scope id
42
+ * (`t('shared.footer.github')`) names its first segment. The runtime adapters
43
+ * fall back to the whole-file catalog (`index`, lingui's `messages`) when that
44
+ * segment is no dictionary, with or without the segment kept in the path.
45
+ */
46
+ const getUsageTargetCandidates = ({ dictionaryKey, fieldPath, library }) => {
47
+ const libraryRootKey = library ? getLibraryRootDictionaryKey(library) : void 0;
48
+ const rootKeys = [WHOLE_FILE_DICTIONARY_KEY, libraryRootKey].filter((rootKey) => Boolean(rootKey));
49
+ const candidates = [{
50
+ dictionaryKey,
51
+ fieldPath
52
+ }];
53
+ if (rootKeys.includes(dictionaryKey)) {
54
+ const [firstSegment, ...rest] = fieldPath.flatMap((segment) => segment.split("."));
55
+ if (firstSegment) candidates.push({
56
+ dictionaryKey: firstSegment,
57
+ fieldPath: rest
58
+ });
59
+ } else for (const rootKey of rootKeys) candidates.push({
60
+ dictionaryKey: rootKey,
61
+ fieldPath: [dictionaryKey, ...fieldPath]
62
+ }, {
63
+ dictionaryKey: rootKey,
64
+ fieldPath
65
+ });
66
+ return candidates;
67
+ };
68
+ /**
69
+ * Match a usage against the built dictionaries: the first candidate whose
70
+ * field exists wins, else the first candidate whose dictionary exists (the
71
+ * field is then missing, but the dictionary is still a useful target).
72
+ *
73
+ * @param target - Dictionary key + field path guessed from the source.
74
+ * @param loadDictionaries - Built dictionaries by key.
75
+ */
76
+ const resolveDictionaryTarget = async (target, loadDictionaries) => {
77
+ let fallback = null;
78
+ for (const candidate of getUsageTargetCandidates(target)) {
79
+ const dictionaries = await loadDictionaries(candidate.dictionaryKey) ?? [];
80
+ if (dictionaries.length === 0) continue;
81
+ for (const dictionary of dictionaries) {
82
+ const contentPath = findContentPath(dictionary.content, candidate.fieldPath);
83
+ if (contentPath) return {
84
+ dictionaryKey: candidate.dictionaryKey,
85
+ fieldPath: contentPath,
86
+ dictionaries,
87
+ isFieldResolved: true
88
+ };
89
+ }
90
+ fallback ??= {
91
+ ...candidate,
92
+ dictionaries,
93
+ isFieldResolved: false
94
+ };
95
+ }
96
+ return fallback;
97
+ };
98
+
99
+ //#endregion
100
+ export { findContentPath, getUsageTargetCandidates, resolveDictionaryTarget };
101
+ //# sourceMappingURL=resolveDictionaryTarget.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveDictionaryTarget.mjs","names":[],"sources":["../../src/resolveDictionaryTarget.ts"],"sourcesContent":["import { getLibraryRootDictionaryKey } from './usageAnalyzer';\n\n/** A dictionary and the path of a field inside its content. */\nexport type DictionaryTarget = {\n dictionaryKey: string;\n fieldPath: string[];\n};\n\n/** A usage-derived target, with the library that produced it. */\nexport type UsageTarget = DictionaryTarget & {\n /** Caller library, e.g. `'lingui'` — selects its whole-file catalog. */\n library?: string;\n};\n\n/** A target matched against the built dictionaries. */\nexport type ResolvedDictionaryTarget<TDictionary> = DictionaryTarget & {\n /** Every built dictionary (one per source file / locale) under the key. */\n dictionaries: TDictionary[];\n /**\n * `true` when `fieldPath` exists in the content. `fieldPath` then holds the\n * actual property names, flat dotted keys joined (`['profile.title']`).\n */\n isFieldResolved: boolean;\n};\n\n/**\n * Dictionary produced by a JSON source holding a whole namespace-less catalog\n * (`./locales/{{locale}}.json`), which i18next's default namespace resolves to.\n */\nconst WHOLE_FILE_DICTIONARY_KEY = 'index';\n\n/**\n * Minimal shape read from a built dictionary.\n */\ntype DictionaryWithContent = { content?: unknown };\n\n/**\n * Dictionaries loaded by key; `null`/`undefined`/empty when none is built.\n */\nexport type DictionaryLoader<TDictionary> = (\n dictionaryKey: string\n) =>\n | TDictionary[]\n | null\n | undefined\n | Promise<TDictionary[] | null | undefined>;\n\n/** Unwrap an intlayer translation node down to one locale's value. */\nconst unwrapTranslationNode = (node: unknown): unknown => {\n if (!node || typeof node !== 'object') return node;\n\n const record = node as Record<string, unknown>;\n\n if (record['nodeType'] !== 'translation') return node;\n\n const translations = record['translation'];\n\n if (!translations || typeof translations !== 'object') return node;\n\n return Object.values(translations)[0];\n};\n\n/**\n * Walk `segments` through `content`, letting one property consume several\n * segments when its name is their dotted join — catalogs mix nested objects\n * and flat dotted keys (`{ settings: { 'profile.title': … } }`).\n *\n * @returns The property names actually traversed, or `null` when absent.\n */\nexport const findContentPath = (\n content: unknown,\n segments: string[]\n): string[] | null => {\n if (segments.length === 0) return [];\n\n const node = unwrapTranslationNode(content);\n\n if (!node || typeof node !== 'object') return null;\n\n const record = node as Record<string, unknown>;\n\n for (let length = 1; length <= segments.length; length++) {\n const propertyName = segments.slice(0, length).join('.');\n\n if (!Object.hasOwn(record, propertyName)) continue;\n\n const rest = findContentPath(record[propertyName], segments.slice(length));\n\n if (rest) return [propertyName, ...rest];\n }\n\n return null;\n};\n\n/**\n * Every dictionary/path pair a usage may read, most specific first.\n *\n * The analyzer guesses the dictionary from the source alone: a root-scope id\n * (`t('shared.footer.github')`) names its first segment. The runtime adapters\n * fall back to the whole-file catalog (`index`, lingui's `messages`) when that\n * segment is no dictionary, with or without the segment kept in the path.\n */\nexport const getUsageTargetCandidates = ({\n dictionaryKey,\n fieldPath,\n library,\n}: UsageTarget): DictionaryTarget[] => {\n const libraryRootKey = library\n ? getLibraryRootDictionaryKey(library)\n : undefined;\n const rootKeys = [WHOLE_FILE_DICTIONARY_KEY, libraryRootKey].filter(\n (rootKey): rootKey is string => Boolean(rootKey)\n );\n\n const candidates: DictionaryTarget[] = [{ dictionaryKey, fieldPath }];\n\n if (rootKeys.includes(dictionaryKey)) {\n // A single-segment id parked in the root catalog may be its own\n // dictionary (`mockBanner` split out of `messages`).\n const [firstSegment, ...rest] = fieldPath.flatMap((segment) =>\n segment.split('.')\n );\n\n if (firstSegment) {\n candidates.push({ dictionaryKey: firstSegment, fieldPath: rest });\n }\n } else {\n for (const rootKey of rootKeys) {\n candidates.push(\n { dictionaryKey: rootKey, fieldPath: [dictionaryKey, ...fieldPath] },\n { dictionaryKey: rootKey, fieldPath }\n );\n }\n }\n\n return candidates;\n};\n\n/**\n * Match a usage against the built dictionaries: the first candidate whose\n * field exists wins, else the first candidate whose dictionary exists (the\n * field is then missing, but the dictionary is still a useful target).\n *\n * @param target - Dictionary key + field path guessed from the source.\n * @param loadDictionaries - Built dictionaries by key.\n */\nexport const resolveDictionaryTarget = async <\n TDictionary extends DictionaryWithContent,\n>(\n target: UsageTarget,\n loadDictionaries: DictionaryLoader<TDictionary>\n): Promise<ResolvedDictionaryTarget<TDictionary> | null> => {\n let fallback: ResolvedDictionaryTarget<TDictionary> | null = null;\n\n for (const candidate of getUsageTargetCandidates(target)) {\n const dictionaries =\n (await loadDictionaries(candidate.dictionaryKey)) ?? [];\n\n if (dictionaries.length === 0) continue;\n\n for (const dictionary of dictionaries) {\n const contentPath = findContentPath(\n dictionary.content,\n candidate.fieldPath\n );\n\n if (contentPath) {\n return {\n dictionaryKey: candidate.dictionaryKey,\n fieldPath: contentPath,\n dictionaries,\n isFieldResolved: true,\n };\n }\n }\n\n fallback ??= { ...candidate, dictionaries, isFieldResolved: false };\n }\n\n return fallback;\n};\n"],"mappings":";;;;;;;AA6BA,MAAM,4BAA4B;;AAmBlC,MAAM,yBAAyB,SAA2B;CACxD,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO;CAE9C,MAAM,SAAS;CAEf,IAAI,OAAO,gBAAgB,eAAe,OAAO;CAEjD,MAAM,eAAe,OAAO;CAE5B,IAAI,CAAC,gBAAgB,OAAO,iBAAiB,UAAU,OAAO;CAE9D,OAAO,OAAO,OAAO,YAAY,CAAC,CAAC;AACrC;;;;;;;;AASA,MAAa,mBACX,SACA,aACoB;CACpB,IAAI,SAAS,WAAW,GAAG,OAAO,CAAC;CAEnC,MAAM,OAAO,sBAAsB,OAAO;CAE1C,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO;CAE9C,MAAM,SAAS;CAEf,KAAK,IAAI,SAAS,GAAG,UAAU,SAAS,QAAQ,UAAU;EACxD,MAAM,eAAe,SAAS,MAAM,GAAG,MAAM,CAAC,CAAC,KAAK,GAAG;EAEvD,IAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,GAAG;EAE1C,MAAM,OAAO,gBAAgB,OAAO,eAAe,SAAS,MAAM,MAAM,CAAC;EAEzE,IAAI,MAAM,OAAO,CAAC,cAAc,GAAG,IAAI;CACzC;CAEA,OAAO;AACT;;;;;;;;;AAUA,MAAa,4BAA4B,EACvC,eACA,WACA,cACqC;CACrC,MAAM,iBAAiB,UACnB,4BAA4B,OAAO,IACnC;CACJ,MAAM,WAAW,CAAC,2BAA2B,cAAc,CAAC,CAAC,QAC1D,YAA+B,QAAQ,OAAO,CACjD;CAEA,MAAM,aAAiC,CAAC;EAAE;EAAe;CAAU,CAAC;CAEpE,IAAI,SAAS,SAAS,aAAa,GAAG;EAGpC,MAAM,CAAC,cAAc,GAAG,QAAQ,UAAU,SAAS,YACjD,QAAQ,MAAM,GAAG,CACnB;EAEA,IAAI,cACF,WAAW,KAAK;GAAE,eAAe;GAAc,WAAW;EAAK,CAAC;CAEpE,OACE,KAAK,MAAM,WAAW,UACpB,WAAW,KACT;EAAE,eAAe;EAAS,WAAW,CAAC,eAAe,GAAG,SAAS;CAAE,GACnE;EAAE,eAAe;EAAS;CAAU,CACtC;CAIJ,OAAO;AACT;;;;;;;;;AAUA,MAAa,0BAA0B,OAGrC,QACA,qBAC0D;CAC1D,IAAI,WAAyD;CAE7D,KAAK,MAAM,aAAa,yBAAyB,MAAM,GAAG;EACxD,MAAM,eACH,MAAM,iBAAiB,UAAU,aAAa,KAAM,CAAC;EAExD,IAAI,aAAa,WAAW,GAAG;EAE/B,KAAK,MAAM,cAAc,cAAc;GACrC,MAAM,cAAc,gBAClB,WAAW,SACX,UAAU,SACZ;GAEA,IAAI,aACF,OAAO;IACL,eAAe,UAAU;IACzB,WAAW;IACX;IACA,iBAAiB;GACnB;EAEJ;EAEA,aAAa;GAAE,GAAG;GAAW;GAAc,iBAAiB;EAAM;CACpE;CAEA,OAAO;AACT"}
@@ -1,5 +1,6 @@
1
1
  import { buildParentMap, getObjectPropertyNode, getObjectPropertyString, getPropertyKeyName, getStaticStringValue, nodeEnd, nodeStart, parseText, walkAst } from "./oxcUtils.mjs";
2
2
  import { callers_exports } from "./callers/index.mjs";
3
+ import { EDITOR_CALLERS } from "./callers/editorCallers.mjs";
3
4
 
4
5
  //#region src/usageAnalyzer.ts
5
6
  /**
@@ -12,6 +13,8 @@ const ACCESSOR_PROPERTY_NAMES = /* @__PURE__ */ new Set([
12
13
  "raw",
13
14
  "use"
14
15
  ]);
16
+ /** The shared registry plus the callers only the editor tooling resolves. */
17
+ const ANALYZED_CALLERS = [...callers_exports.ALL_CALLERS, ...EDITOR_CALLERS];
15
18
  /**
16
19
  * Resolve a value source against a call expression's arguments.
17
20
  */
@@ -108,6 +111,18 @@ const resolveMessageId = (descriptor, fixedDictionaryKey, messageId) => {
108
111
  };
109
112
  };
110
113
  /**
114
+ * Split a namespace-less message id at its first dot: the first segment is the
115
+ * dictionary key, the rest the field path (`'home.hero.title'` → `home`,
116
+ * `['hero', 'title']`).
117
+ */
118
+ const splitMessageIdAtFirstSegment = (messageId) => {
119
+ const [dictionaryKey, ...fieldPath] = messageId.split(".");
120
+ return dictionaryKey ? {
121
+ dictionaryKey,
122
+ fieldPath
123
+ } : null;
124
+ };
125
+ /**
111
126
  * The whole-file catalog a library falls back to when an id-derived key names
112
127
  * no dictionary, declared on any of its callers (`useLingui` → `messages`).
113
128
  */
@@ -209,14 +224,26 @@ const resolveBinding = (bindings, variableName, offset) => {
209
224
  const matchDescriptorForCallee = (state, calleeNode, filter) => {
210
225
  const callee = getCalleeName(calleeNode);
211
226
  if (!callee) return null;
212
- for (const descriptor of state.activeDescriptors) {
213
- if (descriptor.callerName !== callee.name) continue;
227
+ const storeName = getSvelteStoreName(state, callee);
228
+ const calleeNames = storeName ? [storeName, callee.name] : [callee.name];
229
+ for (const calleeName of calleeNames) for (const descriptor of state.activeDescriptors) {
230
+ if (descriptor.callerName !== calleeName) continue;
214
231
  if (callee.isMethod && !descriptor.matchAsMethod) continue;
215
232
  if (filter && !filter(descriptor)) continue;
216
233
  return descriptor;
217
234
  }
218
235
  return null;
219
236
  };
237
+ /**
238
+ * Svelte auto-subscribes an imported store through a `$` prefix:
239
+ * `import { _ } from 'svelte-i18n'` is read as `$_('home.title')`. Returns the
240
+ * store's imported name for such a callee.
241
+ */
242
+ const getSvelteStoreName = (state, callee) => {
243
+ if (callee.isMethod || !callee.name.startsWith("$")) return null;
244
+ const storeName = callee.name.slice(1);
245
+ return state.importedLocalNames.has(storeName) ? storeName : null;
246
+ };
220
247
  /** The module specifier a caller local name was imported from, if any. */
221
248
  const getModuleSource = (state, descriptor) => state.importedLocalNames.get(descriptor.callerName) ?? void 0;
222
249
  /**
@@ -286,6 +313,10 @@ const collectBindings = (state, program) => {
286
313
  }
287
314
  if (node["type"] !== "VariableDeclarator") return;
288
315
  const initializer = unwrapAwait(node["init"]);
316
+ if (initializer?.["type"] === "ArrowFunctionExpression" || initializer?.["type"] === "FunctionExpression") {
317
+ collectPrefixingWrapperBinding(state, node, initializer);
318
+ return;
319
+ }
289
320
  if (initializer?.["type"] !== "CallExpression") return;
290
321
  const descriptor = matchDescriptorForCallee(state, initializer["callee"], (candidate) => candidate.translationFunction !== "self");
291
322
  if (!descriptor) return;
@@ -348,6 +379,109 @@ const collectBindings = (state, program) => {
348
379
  }
349
380
  });
350
381
  };
382
+ /** The single returned expression of a function body, if any. */
383
+ const getReturnedExpression = (functionNode) => {
384
+ const body = functionNode["body"];
385
+ if (body?.["type"] !== "BlockStatement") return body;
386
+ const statements = body["body"] ?? [];
387
+ const onlyStatement = statements.length === 1 ? statements[0] : void 0;
388
+ return onlyStatement?.["type"] === "ReturnStatement" ? onlyStatement["argument"] : void 0;
389
+ };
390
+ /**
391
+ * The static prefix of a `` `footer.${id}` `` template whose only
392
+ * substitution is `parameterName`, split into path segments.
393
+ */
394
+ const getTemplatePrefixSegments = (templateNode, parameterName) => {
395
+ if (templateNode?.["type"] !== "TemplateLiteral") return null;
396
+ const quasis = templateNode["quasis"] ?? [];
397
+ const expressions = templateNode["expressions"] ?? [];
398
+ const readQuasi = (quasi) => {
399
+ return (quasi?.["value"])?.["cooked"] ?? void 0;
400
+ };
401
+ if (expressions.length !== 1 || expressions[0]?.["type"] !== "Identifier" || expressions[0]["name"] !== parameterName || readQuasi(quasis[1]) !== "") return null;
402
+ const prefix = readQuasi(quasis[0]);
403
+ if (!prefix?.endsWith(".")) return null;
404
+ return prefix.slice(0, -1).split(".");
405
+ };
406
+ /**
407
+ * Register a local wrapper that prefixes every id before translating it as a
408
+ * translator binding:
409
+ *
410
+ * const t = (id: string) => i18n._(`footer.${id}`); t('github')
411
+ * const tf = (key) => t(`form.${key}`); tf('name')
412
+ */
413
+ const collectPrefixingWrapperBinding = (state, declaratorNode, functionNode) => {
414
+ const idNode = declaratorNode["id"];
415
+ const parameterNode = functionNode["params"]?.[0];
416
+ const callNode = getReturnedExpression(functionNode);
417
+ if (idNode?.["type"] !== "Identifier" || parameterNode?.["type"] !== "Identifier" || callNode?.["type"] !== "CallExpression") return;
418
+ const prefixSegments = getTemplatePrefixSegments(callNode["arguments"]?.[0], parameterNode["name"]);
419
+ if (!prefixSegments) return;
420
+ const scope = {
421
+ ...getEnclosingScope(state, declaratorNode),
422
+ declarationEnd: nodeEnd(declaratorNode)
423
+ };
424
+ const calleeNode = callNode["callee"];
425
+ const wrappedBinding = calleeNode?.["type"] === "Identifier" ? resolveBinding(state.translatorBindings, calleeNode["name"], nodeStart(callNode)) : void 0;
426
+ if (wrappedBinding) {
427
+ addBinding(state.translatorBindings, idNode["name"], {
428
+ ...wrappedBinding,
429
+ keyPrefix: [...wrappedBinding.keyPrefix, ...prefixSegments],
430
+ ...scope
431
+ });
432
+ return;
433
+ }
434
+ const descriptor = matchDescriptorForCallee(state, calleeNode, (candidate) => candidate.translationFunction === "self");
435
+ if (!descriptor) return;
436
+ addBinding(state.translatorBindings, idNode["name"], {
437
+ dictionaryKey: readValueSources(callNode, descriptor.namespaceSources),
438
+ keyPrefix: prefixSegments,
439
+ descriptor,
440
+ moduleSource: getModuleSource(state, descriptor),
441
+ ...scope
442
+ });
443
+ };
444
+ /** Methods of a translation function that take a message id first. */
445
+ const TRANSLATOR_METHOD_NAMES = /* @__PURE__ */ new Set([
446
+ "rich",
447
+ "markup",
448
+ "raw",
449
+ "has"
450
+ ]);
451
+ /**
452
+ * The translator identifier a call goes through: `t` in `t('id')` and in
453
+ * `t.rich('id')`.
454
+ */
455
+ const getTranslatorCalleeIdentifier = (calleeNode) => {
456
+ if (calleeNode?.["type"] === "Identifier") return calleeNode;
457
+ if (calleeNode?.["type"] !== "MemberExpression" || calleeNode["computed"]) return null;
458
+ const objectNode = calleeNode["object"];
459
+ const propertyNode = calleeNode["property"];
460
+ return objectNode?.["type"] === "Identifier" && TRANSLATOR_METHOD_NAMES.has(propertyNode?.["name"]) ? objectNode : null;
461
+ };
462
+ /**
463
+ * ngx-translate template forms, which live in HTML (component templates or
464
+ * inline `template:` strings) and never reach the JS AST:
465
+ * `{{ 'home.title' | translate }}` and `[translate]="'home.title'"`.
466
+ */
467
+ const TRANSLATE_PIPE_PATTERN = /(['"])([^'"\r\n{}|]+)\1\s*\|\s*translate\b|\[translate\]\s*=\s*"'([^'"\r\n]+)'"/g;
468
+ const collectTranslatePipeUsages = (text) => {
469
+ if (!text.includes("translate")) return [];
470
+ const usages = [];
471
+ for (const match of text.matchAll(TRANSLATE_PIPE_PATTERN)) {
472
+ const split = splitMessageIdAtFirstSegment(match[2] ?? match[3] ?? "");
473
+ if (!split) continue;
474
+ usages.push({
475
+ ...split,
476
+ start: match.index,
477
+ end: match.index + match[0].length,
478
+ kind: "call",
479
+ callerName: "translate",
480
+ library: "@ngx-translate/core"
481
+ });
482
+ }
483
+ return usages;
484
+ };
351
485
  /**
352
486
  * Record `t('path.to.field')` calls on translator bindings, and self-caller
353
487
  * calls (`formatMessage({ id })`, `i18n._('id')`).
@@ -360,8 +494,9 @@ const collectCallUsages = (state, program) => {
360
494
  }
361
495
  if (node["type"] !== "CallExpression") return;
362
496
  const callee = node["callee"];
363
- if (callee?.["type"] === "Identifier") {
364
- const binding = resolveBinding(state.translatorBindings, callee["name"], nodeStart(node));
497
+ const translatorNode = getTranslatorCalleeIdentifier(callee);
498
+ if (translatorNode) {
499
+ const binding = resolveBinding(state.translatorBindings, translatorNode["name"], nodeStart(node));
365
500
  if (binding) {
366
501
  const argumentsList = node["arguments"];
367
502
  const messageId = getStaticStringValue(argumentsList?.[0]);
@@ -370,9 +505,9 @@ const collectCallUsages = (state, program) => {
370
505
  let dictionaryKey = namespaceOverride ?? binding.dictionaryKey;
371
506
  let fieldPath = namespaceOverride ? messagePath.split(".") : [...binding.keyPrefix, ...messagePath.split(".")];
372
507
  if (dictionaryKey === null) {
373
- const segments = messagePath.split(".");
374
- dictionaryKey = segments[0] ?? null;
375
- fieldPath = segments.slice(1);
508
+ const split = splitMessageIdAtFirstSegment([...binding.keyPrefix, messagePath].join("."));
509
+ dictionaryKey = split?.dictionaryKey ?? null;
510
+ fieldPath = split?.fieldPath ?? [];
376
511
  }
377
512
  if (dictionaryKey) state.usages.push({
378
513
  dictionaryKey,
@@ -479,7 +614,7 @@ const collectJsxUsages = (state, program) => {
479
614
  dictionaryKey,
480
615
  fieldPath: descriptor.flatKey ? [messageId] : messageId.split(".")
481
616
  };
482
- else resolved = resolveMessageId(descriptor, null, messageId);
617
+ else resolved = resolveMessageId(descriptor, null, messageId) ?? splitMessageIdAtFirstSegment(messageId);
483
618
  if (!resolved) continue;
484
619
  state.usages.push({
485
620
  ...resolved,
@@ -598,15 +733,16 @@ const collectMemberUsages = (state, program, parentMap) => {
598
733
  * @returns Usages ordered by start offset.
599
734
  */
600
735
  const collectMessageUsages = (text) => {
736
+ const pipeUsages = collectTranslatePipeUsages(text);
601
737
  const program = parseText(text);
602
- if (!program) return [];
738
+ if (!program) return pipeUsages;
603
739
  const fileImportSources = collectImportSources(program);
604
740
  const parentMap = buildParentMap(program);
605
741
  const state = {
606
742
  usages: [],
607
743
  translatorBindings: /* @__PURE__ */ new Map(),
608
744
  contentBindings: /* @__PURE__ */ new Map(),
609
- activeDescriptors: callers_exports.ALL_CALLERS.filter((descriptor) => (0, callers_exports.isCallerActive)(descriptor, fileImportSources)),
745
+ activeDescriptors: ANALYZED_CALLERS.filter((descriptor) => (0, callers_exports.isCallerActive)(descriptor, fileImportSources)),
610
746
  importedLocalNames: collectImportedLocalNames(program),
611
747
  parentMap,
612
748
  program
@@ -615,7 +751,7 @@ const collectMessageUsages = (text) => {
615
751
  collectCallUsages(state, program);
616
752
  collectJsxUsages(state, program);
617
753
  collectMemberUsages(state, program, parentMap);
618
- return state.usages.sort((first, second) => first.start - second.start);
754
+ return [...state.usages, ...pipeUsages].sort((first, second) => first.start - second.start);
619
755
  };
620
756
  /**
621
757
  * Resolve the dictionary key declared by a single call-expression node when
@@ -663,7 +799,7 @@ const collectNamespaceReferences = (text) => {
663
799
  const program = parseText(text);
664
800
  if (!program) return [];
665
801
  const fileImportSources = collectImportSources(program);
666
- const activeDescriptors = callers_exports.ALL_CALLERS.filter((descriptor) => descriptor.translationFunction !== "self" && (0, callers_exports.isCallerActive)(descriptor, fileImportSources));
802
+ const activeDescriptors = ANALYZED_CALLERS.filter((descriptor) => descriptor.translationFunction !== "self" && (0, callers_exports.isCallerActive)(descriptor, fileImportSources));
667
803
  const references = [];
668
804
  walkAst(program, (node) => {
669
805
  if (node["type"] !== "CallExpression") return;
@@ -710,7 +846,7 @@ const collectCallerBindings = (text) => {
710
846
  usages: [],
711
847
  translatorBindings: /* @__PURE__ */ new Map(),
712
848
  contentBindings: /* @__PURE__ */ new Map(),
713
- activeDescriptors: callers_exports.ALL_CALLERS.filter((descriptor) => (0, callers_exports.isCallerActive)(descriptor, fileImportSources)),
849
+ activeDescriptors: ANALYZED_CALLERS.filter((descriptor) => (0, callers_exports.isCallerActive)(descriptor, fileImportSources)),
714
850
  importedLocalNames: collectImportedLocalNames(program),
715
851
  parentMap: buildParentMap(program),
716
852
  program
@@ -766,5 +902,5 @@ const findMessageUsageAtOffset = (text, offset) => {
766
902
  };
767
903
 
768
904
  //#endregion
769
- export { collectCallerBindings, collectMessageUsages, collectNamespaceReferences, findMessageUsageAtOffset, matchCallNamespace };
905
+ export { collectCallerBindings, collectMessageUsages, collectNamespaceReferences, findMessageUsageAtOffset, getLibraryRootDictionaryKey, matchCallNamespace };
770
906
  //# sourceMappingURL=usageAnalyzer.mjs.map