@@ -10,7 +10,6 @@ import remapping, { type DecodedSourceMap, type SourceMapInput } from '@ampproje
1010import type { ɵParsedTranslation } from '@angular/localize' ;
1111import type { Node } from '@oxc-project/types' ;
1212import { MagicString } from 'magic-string' ;
13- import assert from 'node:assert' ;
1413import { deserialize } from 'node:v8' ;
1514import { parseSync } from 'oxc-parser' ;
1615import { traversePostOrder } from '../oxc/traversal' ;
@@ -43,18 +42,17 @@ export interface InlineCodeRequest {
4342 */
4443 translation ?: Blob | SharedArrayBuffer ;
4544
45+ translationKey ?: string ;
46+
4647 /**
4748 * How to handle missing translations.
4849 */
4950 missingTranslation ?: 'error' | 'warning' | 'ignore' ;
5051}
5152
52- /**
53- * The response returned from a code request.
54- */
55- export interface InlineCodeResult {
56- output : string ;
57- messages : { type : 'error' | 'warning' ; message : string } [ ] ;
53+ export interface InlineFileBatchLocaleEntry {
54+ translation ?: Blob | SharedArrayBuffer ;
55+ translationKey ?: string ;
5856}
5957
6058/**
@@ -79,7 +77,7 @@ export interface InlineFileBatchRequest {
7977 /**
8078 * The locale specifiers and optional translations to use during the inlining process of the file.
8179 */
82- locales : ReadonlyMap < string , Blob | SharedArrayBuffer | undefined > ;
80+ locales : ReadonlyMap < string , InlineFileBatchLocaleEntry | Blob | SharedArrayBuffer | undefined > ;
8381
8482 /**
8583 * How to handle missing translations.
@@ -103,6 +101,38 @@ export interface InlineFileBatchRequest {
103101 * all long-term worker caches are cleared.
104102 */
105103 generation ?: number ;
104+
105+ /**
106+ * Optional file contents Blob when dispatched via the shared worker pool.
107+ */
108+ fileBlob ?: Blob ;
109+
110+ /**
111+ * Optional cache key uniquely identifying the file content and AST metadata.
112+ */
113+ fileKey ?: string ;
114+
115+ /**
116+ * Optional sourcemap Blob for the file when dispatched via the shared worker pool.
117+ */
118+ mapBlob ?: Blob ;
119+ }
120+
121+ export interface InlineDiagnosticMessage {
122+ type : 'error' | 'warning' ;
123+ message : string ;
124+ }
125+
126+ export interface InlineFileResult {
127+ file : string ;
128+ code : string ;
129+ map ?: string ;
130+ messages : InlineDiagnosticMessage [ ] ;
131+ }
132+
133+ export interface InlineCodeResult {
134+ output : string ;
135+ messages : InlineDiagnosticMessage [ ] ;
106136}
107137
108138/**
@@ -112,7 +142,7 @@ export interface InlineLocaleResult {
112142 locale : string ;
113143 code ?: string ;
114144 map ?: string ;
115- messages : { type : 'error' | 'warning' ; message : string } [ ] ;
145+ messages : InlineDiagnosticMessage [ ] ;
116146}
117147
118148/**
@@ -130,6 +160,17 @@ export type InlineFileBatchResult =
130160 results : InlineLocaleResult [ ] ;
131161 } ;
132162
163+ /**
164+ * Maximum number of AST metadata structures cached in memory per worker isolate.
165+ * Bounding capacity prevents unbounded memory growth across watch rebuilds.
166+ */
167+ const MAX_CACHED_FILES = 256 ;
168+
169+ /**
170+ * Maximum number of deserialized translation dictionaries cached in memory per worker isolate.
171+ */
172+ const MAX_CACHED_TRANSLATIONS = 32 ;
173+
133174/**
134175 * Cached file data including code and extracted localization metadata.
135176 */
@@ -139,12 +180,12 @@ interface CachedFileData {
139180}
140181
141182/**
142- * Cache of file data promises keyed by filename.
183+ * Cache of file data promises keyed by `${filename}\0${hash}` or filename.
143184 */
144185const fileDataCache = new Map < string , Promise < CachedFileData > > ( ) ;
145186
146187/**
147- * Cache of deserialized translation messages keyed by locale.
188+ * Deserialized translation message dictionary cache keyed by `${locale}\0${translationKey}` or locale.
148189 */
149190const deserializedTranslations = new Map < string , Promise < Record < string , ɵParsedTranslation > > > ( ) ;
150191
@@ -154,72 +195,106 @@ const deserializedTranslations = new Map<string, Promise<Record<string, ɵParsed
154195let currentGeneration : number | undefined ;
155196
156197/**
157- * Retrieves the file data for a filename, loading and extracting localization metadata.
158- * If `cache` is true, the result is cached in `fileDataCache` across requests in this Worker .
159- * If `cache` is false (ephemeral), the result is not retained in `fileDataCache`, allowing it
160- * to be garbage-collected once the batch request finishes.
198+ * Retrieves the code and extracted localization metadata for a file .
199+ * Caches the metadata promise in memory to avoid reparsing the AST across locales .
200+ * If `cache` is false (ephemeral), the result is not retained in `fileDataCache`,
201+ * allowing it to be garbage-collected once the batch request finishes.
161202 *
162- * @param filename The name of the file to load .
203+ * @param filename The name of the file.
163204 * @param codeBlob The source code file as a Blob.
205+ * @param fileKey Optional cache key uniquely identifying the file content.
164206 * @param cache Whether to cache the loaded file data in the Worker's long-term cache.
165- * @returns The cached or newly extracted code and localization metadata .
207+ * @returns The cached file data .
166208 */
167- function loadFileData ( filename : string , codeBlob : Blob , cache = true ) : Promise < CachedFileData > {
168- const existing = fileDataCache . get ( filename ) ;
169- if ( existing ) {
170- if ( ! cache ) {
171- fileDataCache . delete ( filename ) ;
172- }
173-
174- return existing ;
175- }
209+ function getFileData (
210+ filename : string ,
211+ codeBlob : Blob ,
212+ fileKey ?: string ,
213+ cache = true ,
214+ ) : Promise < CachedFileData > {
215+ const cacheKey = fileKey ?? filename ;
216+ let dataPromise = fileDataCache . get ( cacheKey ) ;
217+ if ( ! dataPromise ) {
218+ dataPromise = ( async ( ) => {
219+ const code = await codeBlob . text ( ) ;
176220
177- const fileDataPromise = ( async ( ) => {
178- const code = await codeBlob . text ( ) ;
179- const metadata = extractLocalizeMetadata ( filename , code ) ;
221+ return {
222+ code,
223+ metadata : extractLocalizeMetadata ( filename , code ) ,
224+ } ;
225+ } ) ( ) . catch ( ( error ) => {
226+ if ( fileDataCache . get ( cacheKey ) === dataPromise ) {
227+ fileDataCache . delete ( cacheKey ) ;
228+ }
229+ throw error ;
230+ } ) ;
180231
181- return { code, metadata } ;
182- } ) ( ) ;
232+ if ( cache ) {
233+ if ( fileDataCache . size >= MAX_CACHED_FILES ) {
234+ const oldestKey = fileDataCache . keys ( ) . next ( ) . value ;
235+ if ( oldestKey !== undefined ) {
236+ fileDataCache . delete ( oldestKey ) ;
237+ }
238+ }
183239
184- if ( cache ) {
185- fileDataPromise . catch ( ( ) => {
186- fileDataCache . delete ( filename ) ;
187- } ) ;
188- fileDataCache . set ( filename , fileDataPromise ) ;
240+ fileDataCache . set ( cacheKey , dataPromise ) ;
241+ }
242+ } else if ( cache ) {
243+ fileDataCache . delete ( cacheKey ) ;
244+ fileDataCache . set ( cacheKey , dataPromise ) ;
245+ } else {
246+ fileDataCache . delete ( cacheKey ) ;
189247 }
190248
191- return fileDataPromise ;
249+ return dataPromise ;
192250}
193251
194252/**
195253 * Deserializes or wraps the translation messages for a locale, reusing the result for any
196- * subsequent request that targets the same locale.
197- * @param locale The locale identifier.
198- * @param translation Optional serialized translation messages (SharedArrayBuffer or Blob) .
199- * @returns The translation messages, or undefined if the locale has no translations .
254+ * subsequent request that targets the same locale and translation payload .
255+ *
256+ * @param request The translation request object containing locale, translation payload, and optional key .
257+ * @param explicitTranslation Optional fallback translation payload if request is a string .
200258 */
201259function loadTranslation (
202260 locale : string ,
203261 translation ?: Blob | SharedArrayBuffer ,
262+ translationKey ?: string ,
204263) : Promise < Record < string , ɵParsedTranslation > > | undefined {
205264 if ( ! translation ) {
206265 return undefined ;
207266 }
208267
209- let messagesPromise = deserializedTranslations . get ( locale ) ;
268+ const cacheKey = translationKey ? `${ locale } \0${ translationKey } ` : undefined ;
269+ let messagesPromise = cacheKey ? deserializedTranslations . get ( cacheKey ) : undefined ;
210270 if ( ! messagesPromise ) {
211271 if ( translation instanceof Blob ) {
212272 messagesPromise = translation
213273 . arrayBuffer ( )
214274 . then ( ( buffer ) => deserialize ( new Uint8Array ( buffer ) ) as Record < string , ɵParsedTranslation > )
215275 . catch ( ( error ) => {
216- deserializedTranslations . delete ( locale ) ;
276+ if ( cacheKey && deserializedTranslations . get ( cacheKey ) === messagesPromise ) {
277+ deserializedTranslations . delete ( cacheKey ) ;
278+ }
217279 throw error ;
218280 } ) ;
219281 } else {
220282 messagesPromise = Promise . resolve ( createSharedTranslationProxy ( translation ) ) ;
221283 }
222- deserializedTranslations . set ( locale , messagesPromise ) ;
284+
285+ if ( cacheKey ) {
286+ if ( deserializedTranslations . size >= MAX_CACHED_TRANSLATIONS ) {
287+ const oldestKey = deserializedTranslations . keys ( ) . next ( ) . value ;
288+ if ( oldestKey !== undefined ) {
289+ deserializedTranslations . delete ( oldestKey ) ;
290+ }
291+ }
292+
293+ deserializedTranslations . set ( cacheKey , messagesPromise ) ;
294+ }
295+ } else if ( cacheKey ) {
296+ deserializedTranslations . delete ( cacheKey ) ;
297+ deserializedTranslations . set ( cacheKey , messagesPromise ) ;
223298 }
224299
225300 return messagesPromise ;
@@ -242,14 +317,25 @@ export async function inlineFileBatch(
242317
243318 if ( request . activeLocales ) {
244319 const activeSet = new Set ( request . activeLocales ) ;
245- for ( const locale of deserializedTranslations . keys ( ) ) {
246- if ( ! activeSet . has ( locale ) ) {
247- deserializedTranslations . delete ( locale ) ;
320+ for ( const key of deserializedTranslations . keys ( ) ) {
321+ const keyLocale = key . includes ( '\0' ) ? key . split ( '\0' , 1 ) [ 0 ] : key ;
322+ if ( ! activeSet . has ( keyLocale ) ) {
323+ deserializedTranslations . delete ( key ) ;
248324 }
249325 }
250326 }
251327
252- const { code, metadata } = await loadFileData ( request . filename , request . code , ! request . ephemeral ) ;
328+ const codeBlob = request . code ?? request . fileBlob ;
329+ if ( ! codeBlob ) {
330+ throw new Error ( `File content not provided for: ${ request . filename } ` ) ;
331+ }
332+
333+ const { code, metadata } = await getFileData (
334+ request . filename ,
335+ codeBlob ,
336+ request . fileKey ,
337+ ! request . ephemeral ,
338+ ) ;
253339
254340 // Fast path: file has no $localize call sites or locale insert sites
255341 if ( metadata . callSites . length === 0 && metadata . localeInsertSites . length === 0 ) {
@@ -265,20 +351,31 @@ export async function inlineFileBatch(
265351
266352 // Parse the sourcemap once for the entire batch if provided.
267353 // It will naturally be garbage-collected after this batch action returns.
354+ const rawMapBlob = request . map ?? request . mapBlob ;
268355 let map : SourceMapInput | undefined ;
269- if ( request . map ) {
270- const rawMap = await request . map . text ( ) ;
356+ let rawMap : string | undefined ;
357+ if ( rawMapBlob ) {
358+ rawMap = await rawMapBlob . text ( ) ;
271359 map = rawMap ? ( JSON . parse ( rawMap ) as SourceMapInput ) : undefined ;
272360 }
273361
274362 const results = await Promise . all (
275- Array . from ( request . locales , async ( [ locale , translation ] ) => {
363+ Array . from ( request . locales , async ( [ locale , entry ] ) => {
364+ const translation =
365+ entry && typeof entry === 'object' && 'translation' in entry
366+ ? entry . translation
367+ : ( entry as Blob | SharedArrayBuffer | undefined ) ;
368+ const translationKey =
369+ entry && typeof entry === 'object' && 'translationKey' in entry
370+ ? entry . translationKey
371+ : undefined ;
372+
276373 const result = await inlineLocalize (
277374 code ,
278375 map ,
279376 metadata ,
280377 locale ,
281- await loadTranslation ( locale , translation ) ,
378+ await loadTranslation ( locale , translation , translationKey ) ,
282379 request . filename ,
283380 request . missingTranslation ,
284381 ) ;
@@ -302,7 +399,7 @@ export async function inlineFileBatch(
302399 * Inlines the provided locale and translation into JavaScript code that contains `$localize` usage.
303400 * This function is a secondary entry primarily for use with component HMR update modules.
304401 *
305- * @param request An InlineRequest object representing the options for inlining
402+ * @param request An InlineCodeRequest object representing the options for inlining
306403 * @returns An object containing the inlined code.
307404 */
308405export async function inlineCode ( request : InlineCodeRequest ) : Promise < InlineCodeResult > {
@@ -312,7 +409,7 @@ export async function inlineCode(request: InlineCodeRequest): Promise<InlineCode
312409 undefined ,
313410 metadata ,
314411 request . locale ,
315- await loadTranslation ( request . locale , request . translation ) ,
412+ await loadTranslation ( request . locale , request . translation , request . translationKey ) ,
316413 request . filename ,
317414 request . missingTranslation ,
318415 ) ;
@@ -534,7 +631,7 @@ async function inlineLocalize(
534631 }
535632
536633 const outputCode = magicString . toString ( ) ;
537- let outputMap ;
634+ let outputMap : string | undefined ;
538635 if ( map ) {
539636 // A decoded map is generated here rather than an encoded one because remapping decodes its
540637 // inputs. Encoding the mappings only for remapping to immediately decode them again doubles
@@ -544,12 +641,14 @@ async function inlineLocalize(
544641 includeContent : true ,
545642 hires : 'boundary' ,
546643 } ) ;
547- outputMap = remapping ( [ { ...rawMap , version : 3 } satisfies DecodedSourceMap , map ] , ( ) => null ) ;
644+ outputMap = JSON . stringify (
645+ remapping ( [ { ...rawMap , version : 3 } satisfies DecodedSourceMap , map ] , ( ) => null ) ,
646+ ) ;
548647 }
549648
550649 return {
551650 code : outputCode ,
552- map : outputMap && JSON . stringify ( outputMap ) ,
651+ map : outputMap ,
553652 diagnostics,
554653 } ;
555654}
0 commit comments