lingo.dev icon indicating copy to clipboard operation
lingo.dev copied to clipboard

fix: replace Function constructor with JSON.parse in cache reading

Open manasdutta04 opened this issue 1 month ago • 1 comments

Fix: Replace Function constructor with JSON.parse in cache reading

Summary

This PR replaces the current approach that evaluates cache files using the Function constructor with a safer, simpler JSON-based format and reader. It provides a seamless migration path from the existing export default {...}; format to JSON, and adds comprehensive tests to cover read/write and migration scenarios.

Fixes #1266

Motivation

  • Using new Function(...) to evaluate a cache file is unnecessarily risky and harder to maintain
  • A plain JSON cache improves safety, tooling compatibility, and clarity
  • Migration can be implemented without breaking users by auto-detecting and converting the old format

Changes Made

1. Core Implementation

Changed cache file format to JSON

  • File format: Changed from dictionary.js (with export default {...}) to dictionary.json (pure JSON)
  • Reading: Now uses JSON.parse() instead of new Function(...)
  • Writing: Outputs pure JSON without JavaScript wrapper
  • Parser: Updated Prettier parser from typescript to json

Backward-compatible migration

  • Auto-detects legacy format that starts with export default
  • Safely parses legacy content by stripping the wrapper and using JSON.parse() - no code execution
  • Automatically re-writes cache in new JSON format after first read
  • Cleans up legacy dictionary.js files after successful migration
  • Logs migration with optional debug flag (process.env.DEBUG)

Error handling

  • Gracefully handles corrupted JSON cache files
  • Returns empty cache on parse failures with warning message
  • Creates directories recursively if they don't exist

2. Updated Constants

  • Changed LCP_DICTIONARY_FILE_NAME from "dictionary.js" to "dictionary.json"
  • Added LCP_DICTIONARY_LEGACY_FILE_NAME constant for backward compatibility

3. Demo Projects

Migrated all demo dictionary files from legacy JS format to pure JSON:

  • demo/adonisjs/inertia/lingo/dictionary.jsdictionary.json
  • demo/next-app/src/lingo/dictionary.jsdictionary.json
  • demo/react-router-app/app/lingo/dictionary.jsdictionary.json
  • demo/vite-project/src/lingo/dictionary.jsdictionary.json

4. Comprehensive Test Coverage

Added 14 tests covering all scenarios:

JSON format tests (5)

  • ✅ Writing cache in JSON format
  • ✅ Reading cache from JSON format
  • ✅ Returning empty cache when file doesn't exist
  • ✅ Ensuring dictionary file with empty JSON object
  • ✅ Maintaining stable key ordering

Legacy migration tests (3)

  • ✅ Migrating from legacy JS format to JSON
  • ✅ Handling legacy format without 'export default' prefix
  • ✅ Handling corrupted legacy cache gracefully

Multiple locales tests (2)

  • ✅ Handling multiple locales in cache
  • ✅ Sorting locales alphabetically in cache entries

Hash validation tests (2)

  • ✅ Invalidating cached entries when hash changes
  • ✅ Reusing cached entries when hash matches

Error handling tests (2)

  • ✅ Handling corrupted JSON cache gracefully
  • ✅ Creating directory if it doesn't exist

Files Changed

Modified (5 files)

  • packages/compiler/src/_const.ts - Updated cache filename constants
  • packages/compiler/src/lib/lcp/cache.ts - Complete refactor with JSON support and migration
  • packages/compiler/src/lib/lcp/cache.spec.ts - Added 14 comprehensive tests
  • packages/compiler/src/_utils.spec.ts - Updated tests to expect .json extension
  • packages/compiler/src/_loader-utils.spec.ts - Updated tests to use dictionary.json

Deleted (4 files)

  • demo/adonisjs/inertia/lingo/dictionary.js
  • demo/next-app/src/lingo/dictionary.js
  • demo/react-router-app/app/lingo/dictionary.js
  • demo/vite-project/src/lingo/dictionary.js

Added (4 files)

  • demo/adonisjs/inertia/lingo/dictionary.json
  • demo/next-app/src/lingo/dictionary.json
  • demo/react-router-app/app/lingo/dictionary.json
  • demo/vite-project/src/lingo/dictionary.json

Testing

All tests pass successfully:

✓ src/lib/lcp/cache.spec.ts (14)
  ✓ LCPCache (14)
    ✓ JSON format (5)
    ✓ Legacy format migration (3)
    ✓ Multiple locales (2)
    ✓ Hash validation (2)
    ✓ Error handling (2)

Test Files  1 passed (1)
     Tests  14 passed (14)

Migration Path for Users

Existing projects with dictionary.js:

  1. The cache reader automatically detects the old format
  2. Reads the legacy file safely (no code execution)
  3. On next write, saves in new JSON format
  4. Legacy dictionary.js file is automatically deleted

New projects:

  • Will use dictionary.json from the start
  • No migration needed

Security Improvements

Before:

// UNSAFE - executes code from file
const obj = new Function(`return (${cache})`)();

After:

// SAFE - just parses JSON
return JSON.parse(content);

Checklist

  • [x] Removed unsafe new Function() constructor
  • [x] Implemented JSON-based cache with JSON.parse()
  • [x] Added backward-compatible migration
  • [x] Added 14 comprehensive tests (all passing)
  • [x] Migrated all demo projects to new format
  • [x] Updated constants and file references
  • [x] Graceful error handling for corrupted files
  • [x] Automatic cleanup of legacy files

manasdutta04 avatar Nov 04 '25 14:11 manasdutta04

Also, @manasdutta04 have you joined our discord? I'd love to go over something with you. Can you DM me there please?

sumitsaurabh927 avatar Nov 04 '25 22:11 sumitsaurabh927

Hey @sumitsaurabh927, I DM you on Discord, and yes, I’ve joined the Discord server!

manasdutta04 avatar Nov 05 '25 06:11 manasdutta04

@manasdutta04 there's a failing check on this PR and your branch is out of date.

Also, still waiting for the DM 🙂

sumitsaurabh927 avatar Nov 05 '25 18:11 sumitsaurabh927

Hey, I have already sent you DM over discord @sumitsaurabh927 My Discord user name: manasdutta.in

I am working on the fix

manasdutta04 avatar Nov 06 '25 11:11 manasdutta04

closing the PR till the discussion finishes in #1266. feel free to recreate later.

maxprilutskiy avatar Nov 12 '25 07:11 maxprilutskiy