-
Notifications
You must be signed in to change notification settings - Fork 834
feat: migrate dictionary cache to JSON format (#1266) #1965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: migrate dictionary cache to JSON format (#1266) #1965
Conversation
- Replace Function-evaluated JS with safer JSON.parse - Add backward-compatible migration for legacy export default format - Change file extension from .js to .json - Remove unsafe Function constructor usage - Add comprehensive test coverage for migration paths Fixes lingodotdev#1266
📝 WalkthroughWalkthroughThis PR migrates the dictionary cache format from JavaScript modules (evaluated via Function constructor) to JSON format with automatic backward-compatible migration. The constant for the dictionary filename is updated to reflect Changes
Sequence DiagramsequenceDiagram
participant App as Application
participant Reader as Cache Reader
participant FS as File System
participant Migrator as Migration Logic
App->>Reader: Read cache
Reader->>FS: Read file
FS-->>Reader: File content
alt Legacy Format (export default)
Reader->>Migrator: Detect legacy format
Migrator->>Migrator: Parse JSON from export default
Migrator->>FS: Write as pure JSON
FS-->>Migrator: File written
Migrator-->>Reader: Parsed object
else New Format (JSON)
Reader->>Reader: JSON.parse content
end
Reader-->>App: Cache object
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@packages/compiler/src/lib/lcp/cache.ts`:
- Around line 187-205: Detection uses fileContent.trim() but the subsequent
replace/parse uses the untrimmed fileContent, so files with leading whitespace
pass isLegacyFormat but fail the replacement and JSON.parse; fix by performing
the replacement and JSON.parse against the trimmed input (or use a regex that
allows leading whitespace, e.g. /^\s*export default/), i.e. compute a trimmed
variable (e.g. const trimmed = fileContent.trim()), use that for isLegacyFormat
and for producing jsonContent before JSON.parse, then write the migrated JSON to
cachePath via fs.writeFileSync as before (symbols: fileContent, isLegacyFormat,
jsonContent, JSON.parse, fs.writeFileSync, cachePath).
🧹 Nitpick comments (2)
packages/compiler/src/lib/lcp/cache.spec.ts (1)
464-475: Consider verifyingmkdirSyncis called when directory doesn't exist.The test mocks
existsSyncto returnfalsefor all calls, which includes both the cache file and directory checks. The implementation should callmkdirSyncin this case, but the test doesn't verify this.🧪 Optional: Add assertion for directory creation
LCPCache.ensureDictionaryFile({ sourceRoot: params.sourceRoot, lingoDir: params.lingoDir, }); + expect(mockMkdirSync).toHaveBeenCalledWith( + expect.any(String), + { recursive: true } + ); expect(mockWriteFileSync).toHaveBeenCalledWith(cachePath, "{}");packages/compiler/src/lib/lcp/cache.ts (1)
177-209: Consider adding error handling for malformed cache files.Both
JSON.parsecalls (Lines 197 and 208) can throw on corrupted/malformed cache files, which could crash the build process. Returning a fresh empty cache on parse errors would make the system more resilient.🛡️ Optional: Add defensive parsing
private static _read(params: LCPCacheParams): DictionaryCacheSchema { const cachePath = this._getCachePath(params); if (!fs.existsSync(cachePath)) { return { version: 0.1, files: {}, }; } const fileContent = fs.readFileSync(cachePath, "utf8"); // Detect legacy format (export default {...};) and migrate to JSON const isLegacyFormat = fileContent.trim().startsWith("export default"); + const parseOrDefault = (content: string): DictionaryCacheSchema => { + try { + return JSON.parse(content); + } catch { + return { version: 0.1, files: {} }; + } + }; + if (isLegacyFormat) { // Strip 'export default' and trailing semicolon, then parse as JSON const jsonContent = fileContent + .trim() .replace(/^export default/, "") .replace(/;\s*$/, "") .trim(); - const obj = JSON.parse(jsonContent); + const obj = parseOrDefault(jsonContent); // Migrate: immediately rewrite in new JSON format const newContent = JSON.stringify(obj, null, 2); fs.writeFileSync(cachePath, newContent); return obj; } // New format: parse directly as JSON - return JSON.parse(fileContent); + return parseOrDefault(fileContent); }
| // Detect legacy format (export default {...};) and migrate to JSON | ||
| const isLegacyFormat = fileContent.trim().startsWith("export default"); | ||
|
|
||
| if (isLegacyFormat) { | ||
| // Strip 'export default' and trailing semicolon, then parse as JSON | ||
| const jsonContent = fileContent | ||
| .replace(/^export default/, "") | ||
| .replace(/;\s*$/, "") | ||
| .trim(); | ||
|
|
||
| const obj = JSON.parse(jsonContent); | ||
|
|
||
| // Migrate: immediately rewrite in new JSON format | ||
| // This is a synchronous write to ensure migration happens atomically | ||
| const newContent = JSON.stringify(obj, null, 2); | ||
| fs.writeFileSync(cachePath, newContent); | ||
|
|
||
| return obj; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential bug: Leading whitespace handling inconsistency.
The legacy format detection uses fileContent.trim().startsWith() (Line 188), but the replace(/^export default/, "") (Line 193) operates on the original fileContent without trimming. If a legacy file has leading whitespace, detection succeeds but the replacement fails, causing JSON.parse to throw.
🐛 Proposed fix
// Detect legacy format (export default {...};) and migrate to JSON
const isLegacyFormat = fileContent.trim().startsWith("export default");
if (isLegacyFormat) {
// Strip 'export default' and trailing semicolon, then parse as JSON
const jsonContent = fileContent
+ .trim()
.replace(/^export default/, "")
.replace(/;\s*$/, "")
.trim();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Detect legacy format (export default {...};) and migrate to JSON | |
| const isLegacyFormat = fileContent.trim().startsWith("export default"); | |
| if (isLegacyFormat) { | |
| // Strip 'export default' and trailing semicolon, then parse as JSON | |
| const jsonContent = fileContent | |
| .replace(/^export default/, "") | |
| .replace(/;\s*$/, "") | |
| .trim(); | |
| const obj = JSON.parse(jsonContent); | |
| // Migrate: immediately rewrite in new JSON format | |
| // This is a synchronous write to ensure migration happens atomically | |
| const newContent = JSON.stringify(obj, null, 2); | |
| fs.writeFileSync(cachePath, newContent); | |
| return obj; | |
| } | |
| // Detect legacy format (export default {...};) and migrate to JSON | |
| const isLegacyFormat = fileContent.trim().startsWith("export default"); | |
| if (isLegacyFormat) { | |
| // Strip 'export default' and trailing semicolon, then parse as JSON | |
| const jsonContent = fileContent | |
| .trim() | |
| .replace(/^export default/, "") | |
| .replace(/;\s*$/, "") | |
| .trim(); | |
| const obj = JSON.parse(jsonContent); | |
| // Migrate: immediately rewrite in new JSON format | |
| // This is a synchronous write to ensure migration happens atomically | |
| const newContent = JSON.stringify(obj, null, 2); | |
| fs.writeFileSync(cachePath, newContent); | |
| return obj; | |
| } |
🤖 Prompt for AI Agents
In `@packages/compiler/src/lib/lcp/cache.ts` around lines 187 - 205, Detection
uses fileContent.trim() but the subsequent replace/parse uses the untrimmed
fileContent, so files with leading whitespace pass isLegacyFormat but fail the
replacement and JSON.parse; fix by performing the replacement and JSON.parse
against the trimmed input (or use a regex that allows leading whitespace, e.g.
/^\s*export default/), i.e. compute a trimmed variable (e.g. const trimmed =
fileContent.trim()), use that for isLegacyFormat and for producing jsonContent
before JSON.parse, then write the migrated JSON to cachePath via
fs.writeFileSync as before (symbols: fileContent, isLegacyFormat, jsonContent,
JSON.parse, fs.writeFileSync, cachePath).
feat: migrate dictionary cache to JSON format (#1266)
Description
This PR migrates the dictionary cache format from a Function-evaluated JavaScript file (
export default {...}) to a safer, standard JSON format (.json). This resolves the security risk associated with usingnew Function(...)to evaluate cache files and improves tooling compatibility.Fixes #1266
Key Changes
dictionary.jstodictionary.json.new Function(...)constructor withJSON.parsefor reading cache files.dictionary.js(or legacy format content), it:export defaultsyntax.Implementation Details
dictionary.json.Verification
Ran the full test suite to ensure no regressions:
pnpm testResult: 228/228 existing tests passed, including new migration tests.
Checklist
Summary by CodeRabbit
New Features
Tests