@@ -17,10 +17,10 @@ import { parseSync, visitorKeys } from 'oxc-parser';
1717/**
1818 * The options passed to the inliner for each file request
1919 */
20- interface InlineFileRequest {
20+ export interface InlineFileRequest {
2121 /**
2222 * The filename that should be processed. The data for the file is provided to the Worker
23- * during Worker initialization.
23+ * during Worker initialization or per request .
2424 */
2525 filename : string ;
2626
@@ -35,12 +35,18 @@ interface InlineFileRequest {
3535 * reference instead of being copied into it for every request.
3636 */
3737 translation ?: Blob ;
38+
39+ missingTranslation ?: 'error' | 'warning' | 'ignore' ;
40+ shouldOptimize ?: boolean ;
41+ fileBlob ?: Blob ;
42+ mapBlob ?: Blob ;
43+ files ?: ReadonlyMap < string , Blob > ;
3844}
3945
4046/**
4147 * The options passed to the inliner for each code request
4248 */
43- interface InlineCodeRequest {
49+ export interface InlineCodeRequest {
4450 /**
4551 * The code that should be processed.
4652 */
@@ -62,12 +68,32 @@ interface InlineCodeRequest {
6268 * reference instead of being copied into it for every request.
6369 */
6470 translation ?: Blob ;
71+
72+ missingTranslation ?: 'error' | 'warning' | 'ignore' ;
73+ shouldOptimize ?: boolean ;
74+ }
75+
76+ export interface InlineDiagnosticMessage {
77+ type : 'error' | 'warning' ;
78+ message : string ;
79+ }
80+
81+ export interface InlineFileResult {
82+ file : string ;
83+ code : string ;
84+ map ?: string ;
85+ messages : InlineDiagnosticMessage [ ] ;
86+ }
87+
88+ export interface InlineCodeResult {
89+ output : string ;
90+ messages : InlineDiagnosticMessage [ ] ;
6591}
6692
6793// Extract the application files and common options used for inline requests from the Worker context
68- const { files, missingTranslation } = ( workerData || { } ) as {
69- files : ReadonlyMap < string , Blob > ;
70- missingTranslation : 'error' | 'warning' | 'ignore' ;
94+ const { files, missingTranslation = 'ignore' } = ( workerData || { } ) as {
95+ files ? : ReadonlyMap < string , Blob > ;
96+ missingTranslation ? : 'error' | 'warning' | 'ignore' ;
7197} ;
7298
7399/**
@@ -79,34 +105,36 @@ interface CachedFileData {
79105}
80106
81107/**
82- * Cache of file data promises keyed by filename.
108+ * Cache of file data promises keyed by file Blob.
109+ * WeakMap ensures cached AST metadata and source code are automatically garbage-collected
110+ * when the parent inliner releases the file Blobs upon build completion.
83111 */
84- const fileDataCache = new Map < string , Promise < CachedFileData > > ( ) ;
112+ const fileDataCache = new WeakMap < Blob , Promise < CachedFileData > > ( ) ;
85113
86114/**
87- * Cache of deserialized translation messages keyed by locale .
115+ * Cache of deserialized translation messages keyed by translation Blob .
88116 */
89- const deserializedTranslations = new Map < string , Promise < Record < string , unknown > > > ( ) ;
117+ const deserializedTranslations = new WeakMap < Blob , Promise < Record < string , unknown > > > ( ) ;
90118
91119/**
92120 * Retrieves the cached file data for a filename, loading and extracting it on the first request.
93121 *
94122 * @param filename The name of the file to load.
123+ * @param fileBlob Optional Blob containing the file data.
95124 * @returns The cached code and localization metadata.
96125 */
97- function getFileData ( filename : string ) : Promise < CachedFileData > {
98- let fileDataPromise = fileDataCache . get ( filename ) ;
99- if ( ! fileDataPromise ) {
100- fileDataPromise = ( async ( ) => {
101- const data = files . get ( filename ) ;
102- assert ( data !== undefined , `Invalid inline request for file '${ filename } '.` ) ;
126+ async function getFileData ( filename : string , fileBlob ?: Blob ) : Promise < CachedFileData > {
127+ const data = fileBlob ?? files ?. get ( filename ) ;
128+ assert ( data !== undefined , `Invalid inline request for file '${ filename } '.` ) ;
103129
104- const code = await data . text ( ) ;
130+ let fileDataPromise = fileDataCache . get ( data ) ;
131+ if ( ! fileDataPromise ) {
132+ fileDataPromise = data . text ( ) . then ( ( code ) => {
105133 const metadata = extractLocalizeMetadata ( filename , code ) ;
106134
107135 return { code, metadata } ;
108- } ) ( ) ;
109- fileDataCache . set ( filename , fileDataPromise ) ;
136+ } ) ;
137+ fileDataCache . set ( data , fileDataPromise ) ;
110138 }
111139
112140 return fileDataPromise ;
@@ -121,17 +149,17 @@ function getFileData(filename: string): Promise<CachedFileData> {
121149function loadTranslation (
122150 request : InlineFileRequest | InlineCodeRequest ,
123151) : Promise < Record < string , unknown > > | undefined {
124- const { locale , translation } = request ;
152+ const { translation } = request ;
125153 if ( ! translation ) {
126154 return undefined ;
127155 }
128156
129- let messagesPromise = deserializedTranslations . get ( locale ) ;
157+ let messagesPromise = deserializedTranslations . get ( translation ) ;
130158 if ( ! messagesPromise ) {
131159 messagesPromise = translation
132160 . arrayBuffer ( )
133161 . then ( ( buffer ) => deserialize ( new Uint8Array ( buffer ) ) as Record < string , unknown > ) ;
134- deserializedTranslations . set ( locale , messagesPromise ) ;
162+ deserializedTranslations . set ( translation , messagesPromise ) ;
135163 }
136164
137165 return messagesPromise ;
@@ -144,14 +172,16 @@ function loadTranslation(
144172 * @param request An InlineRequest object representing the options for inlining
145173 * @returns An object containing the inlined file and optional map content.
146174 */
147- export default async function inlineFile ( request : InlineFileRequest ) {
148- const { code, metadata } = await getFileData ( request . filename ) ;
175+ export default async function inlineFile ( request : InlineFileRequest ) : Promise < InlineFileResult > {
176+ const { code, metadata } = await getFileData ( request . filename , request . fileBlob ) ;
149177
150178 // Sourcemaps are parsed on demand per request rather than cached long-term to prevent
151179 // monotonic memory growth as a worker processes multiple files across the build.
152180 // When multi-locale batching is implemented, the sourcemap can be parsed once per batch and released
153181 // upon batch completion.
154- const rawMap = await files . get ( request . filename + '.map' ) ?. text ( ) ;
182+ const rawMap = request . mapBlob
183+ ? await request . mapBlob . text ( )
184+ : await files ?. get ( request . filename + '.map' ) ?. text ( ) ;
155185 const map = rawMap ? ( JSON . parse ( rawMap ) as SourceMapInput ) : undefined ;
156186
157187 const result = await inlineLocalize (
@@ -161,6 +191,7 @@ export default async function inlineFile(request: InlineFileRequest) {
161191 request . locale ,
162192 await loadTranslation ( request ) ,
163193 request . filename ,
194+ request . missingTranslation ?? missingTranslation ,
164195 ) ;
165196
166197 return {
@@ -178,7 +209,7 @@ export default async function inlineFile(request: InlineFileRequest) {
178209 * @param request An InlineRequest object representing the options for inlining
179210 * @returns An object containing the inlined code.
180211 */
181- export async function inlineCode ( request : InlineCodeRequest ) {
212+ export async function inlineCode ( request : InlineCodeRequest ) : Promise < InlineCodeResult > {
182213 const metadata = extractLocalizeMetadata ( request . filename , request . code ) ;
183214 const result = await inlineLocalize (
184215 request . code ,
@@ -187,6 +218,7 @@ export async function inlineCode(request: InlineCodeRequest) {
187218 request . locale ,
188219 await loadTranslation ( request ) ,
189220 request . filename ,
221+ request . missingTranslation ?? missingTranslation ,
190222 ) ;
191223
192224 return {
@@ -352,6 +384,41 @@ function extractLocalizeMetadata(filename: string, code: string): FileLocalizeMe
352384 return { callSites, localeInsertSites, diagnostics } ;
353385}
354386
387+ /**
388+ * Formats translated template parts and expressions into a JavaScript string
389+ * or template literal replacement.
390+ */
391+ function formatReplacement (
392+ translatedParts : readonly string [ ] ,
393+ translatedSubstitutions : readonly number [ ] ,
394+ expressions : readonly { start : number ; end : number } [ ] ,
395+ magicString : MagicString ,
396+ ) : string {
397+ if ( translatedSubstitutions . length === 0 ) {
398+ return JSON . stringify ( translatedParts [ 0 ] ) ;
399+ }
400+
401+ let replacement = '`' ;
402+ for ( let i = 0 ; i < translatedParts . length ; i ++ ) {
403+ const escapedPart = JSON . stringify ( translatedParts [ i ] )
404+ . slice ( 1 , - 1 )
405+ . replace ( / \\ " / g, '"' )
406+ . replace ( / ` / g, '\\`' )
407+ . replace ( / \$ \{ / g, '\\${' ) ;
408+ replacement += escapedPart ;
409+
410+ if ( i < translatedSubstitutions . length ) {
411+ const originalIndex = translatedSubstitutions [ i ] ;
412+ const expr = expressions [ originalIndex ] ;
413+ const exprCode = magicString . slice ( expr . start , expr . end ) ;
414+ replacement += '${' + exprCode + '}' ;
415+ }
416+ }
417+ replacement += '`' ;
418+
419+ return replacement ;
420+ }
421+
355422/**
356423 * Inlines translations into code using previously extracted localization metadata.
357424 *
@@ -370,6 +437,7 @@ async function inlineLocalize(
370437 locale : string ,
371438 translation : Record < string , unknown > | undefined ,
372439 filename : string ,
440+ missingTranslationOption : 'error' | 'warning' | 'ignore' = missingTranslation ?? 'ignore' ,
373441) {
374442 const magicString = new MagicString ( code ) ;
375443 const { Diagnostics, translate } = await loadLocalizeTools ( ) ;
@@ -391,32 +459,15 @@ async function inlineLocalize(
391459 translation || { } ,
392460 callSite . messageParts ,
393461 callSite . expressions . map ( ( _ , index ) => index ) ,
394- translation === undefined ? 'ignore' : missingTranslation ,
462+ translation === undefined ? 'ignore' : missingTranslationOption ,
395463 ) ;
396464
397- // Reconstruct the new template/string literal replacement
398- let replacement : string ;
399- if ( translatedSubstitutions . length === 0 ) {
400- replacement = JSON . stringify ( translatedParts [ 0 ] ) ;
401- } else {
402- replacement = '`' ;
403- for ( let i = 0 ; i < translatedParts . length ; i ++ ) {
404- const escapedPart = JSON . stringify ( translatedParts [ i ] )
405- . slice ( 1 , - 1 )
406- . replace ( / \\ " / g, '"' )
407- . replace ( / ` / g, '\\`' )
408- . replace ( / \$ \{ / g, '\\${' ) ;
409- replacement += escapedPart ;
410-
411- if ( i < translatedSubstitutions . length ) {
412- const originalIndex = translatedSubstitutions [ i ] ;
413- const expr = callSite . expressions [ originalIndex ] ;
414- const exprCode = magicString . slice ( expr . start , expr . end ) ;
415- replacement += '${' + exprCode + '}' ;
416- }
417- }
418- replacement += '`' ;
419- }
465+ const replacement = formatReplacement (
466+ translatedParts ,
467+ translatedSubstitutions ,
468+ callSite . expressions ,
469+ magicString ,
470+ ) ;
420471
421472 magicString . overwrite ( callSite . start , callSite . end , replacement ) ;
422473 }
0 commit comments