@@ -23,16 +23,66 @@ const PACKAGE_LIST = [
23
23
24
24
const CALLER_LIST = [ 'useIntlayer' , 'getIntlayer' ] as const ;
25
25
26
+ /**
27
+ * Packages that support dynamic import
28
+ */
29
+ const PACKAGE_LIST_DYNAMIC = [
30
+ 'react-intlayer' ,
31
+ 'react-intlayer/client' ,
32
+ 'react-intlayer/server' ,
33
+ 'next-intlayer' ,
34
+ 'next-intlayer/client' ,
35
+ 'next-intlayer/server' ,
36
+ ] as const ;
37
+
38
+ const STATIC_IMPORT_FUNCTION = {
39
+ getIntlayer : 'getDictionary' ,
40
+ useIntlayer : 'useDictionary' ,
41
+ } as const ;
42
+
43
+ const DYNAMIC_IMPORT_FUNCTION = {
44
+ useIntlayer : 'useDictionaryDynamic' ,
45
+ } as const ;
46
+
26
47
/* ────────────────────────────────────────── types ───────────────────────── */
27
48
28
49
type State = PluginPass & {
29
- opts : { dictionariesDir : string ; dictionariesEntryPath : string } ;
30
- /** map key → generated ident (per-file) */
31
- _newImports ?: Map < string , t . Identifier > ;
50
+ opts : {
51
+ /**
52
+ * The path to the dictionaries directory.
53
+ */
54
+ dictionariesDir : string ;
55
+ /**
56
+ * The path to the dictionaries entry file.
57
+ */
58
+ dictionariesEntryPath : string ;
59
+ /**
60
+ * The path to the dictionaries directory.
61
+ */
62
+ dynamicDictionariesDir : string ;
63
+ /**
64
+ * The path to the dynamic dictionaries entry file.
65
+ */
66
+ dynamicDictionariesEntryPath : string ;
67
+ /**
68
+ * If true, the plugin will activate the dynamic import of the dictionaries.
69
+ */
70
+ activateDynamicImport ?: boolean ;
71
+ /**
72
+ * Files list to traverse.
73
+ */
74
+ filesList ?: string [ ] ;
75
+ } ;
76
+ /** map key → generated ident (per-file) for static imports */
77
+ _newStaticImports ?: Map < string , t . Identifier > ;
78
+ /** map key → generated ident (per-file) for dynamic imports */
79
+ _newDynamicImports ?: Map < string , t . Identifier > ;
32
80
/** whether the current file imported *any* intlayer package */
33
81
_hasValidImport ?: boolean ;
34
82
/** whether the current file *is* the dictionaries entry file */
35
83
_isDictEntry ?: boolean ;
84
+ /** whether dynamic helpers are active for this file */
85
+ _useDynamicHelpers ?: boolean ;
36
86
} ;
37
87
38
88
/* ────────────────────────────────────────── helpers ─────────────────────── */
@@ -49,10 +99,15 @@ const makeIdent = (key: string): t.Identifier => {
49
99
50
100
const computeRelativeImport = (
51
101
fromFile : string ,
52
- dictDir : string ,
53
- key : string
102
+ dictionariesDir : string ,
103
+ dynamicDictionariesDir : string ,
104
+ key : string ,
105
+ isDynamic = false
54
106
) : string => {
55
- const jsonPath = join ( dictDir , `${ key } .json` ) ;
107
+ const jsonPath = isDynamic
108
+ ? join ( dynamicDictionariesDir , `${ key } .mjs` )
109
+ : join ( dictionariesDir , `${ key } .json` ) ;
110
+
56
111
let rel = relative ( dirname ( fromFile ) , jsonPath ) . replace ( / \\ / g, '/' ) ; // win →
57
112
if ( ! rel . startsWith ( './' ) && ! rel . startsWith ( '../' ) ) rel = `./${ rel } ` ;
58
113
return rel ;
@@ -64,28 +119,70 @@ const computeRelativeImport = (
64
119
* Babel plugin that transforms `useIntlayer/getIntlayer` calls into
65
120
* `useDictionary/getDictionary` and auto-imports the required JSON dictionaries.
66
121
*
67
- * **New behaviour**: if the currently processed file matches `dictionariesEntryPath`,
68
- * its entire contents are replaced with a simple `export default {}` so that it
69
- * never contains stale or circular references.
70
- *
71
- * The **critical detail** (bug-fix) is that we still **only rewrite** an import
72
- * specifier when its *imported* name is `useIntlayer`/`getIntlayer`.
73
122
*
74
123
* This means cases like:
124
+ *
125
+ * ```ts
126
+ * import { getIntlayer } from 'intlayer';
127
+ * import { useIntlayer } from 'react-intlayer';
128
+ *
129
+ * // ...
130
+ *
131
+ * const content1 = getIntlayer('app');
132
+ * const content2 = useIntlayer('app');
133
+ * ```
134
+ *
135
+ * will be transformed into:
136
+ *
75
137
* ```ts
76
- * import { useDictionary as useIntlayer } from 'react-intlayer';
138
+ * import _dicHash from '../../.intlayer/dictionaries/app.mjs';
139
+ * import { getDictionary as getIntlayer } from 'intlayer';
140
+ * import { useDictionaryDynamic as useIntlayer } from 'react-intlayer';
141
+ *
142
+ * // ...
143
+ *
144
+ * const content1 = getIntlayer(_dicHash);
145
+ * const content2 = useIntlayer(_dicHash)
146
+ * ```
147
+ *
148
+ * Or if the `activateDynamicImport` option is enabled:
149
+ *
150
+ * ```ts
151
+ * import _dicHash from '../../.intlayer/dynamic_dictionaries/app.mjs';
152
+ * import _dicHash_dyn from '../../.intlayer/dictionaries/app.mjs';
153
+ *
154
+ * import { useDictionary as getIntlayer } from 'intlayer';
155
+ * import { useDictionaryDynamic as useIntlayer } from 'react-intlayer';
156
+ *
157
+ * // ...
158
+ *
159
+ * const content1 = getIntlayer(_dicHash);
160
+ * const content2 = useIntlayer(_dicHash_dyn, 'app');
77
161
* ```
78
- * —where `useIntlayer` is merely an *alias* or re-export—are left untouched
79
- * because `imported.name` is `useDictionary`.
80
162
*/
81
163
export const intlayerBabelPlugin = ( ) : PluginObj < State > => {
82
164
return {
83
165
name : 'babel-plugin-intlayer-transform' ,
84
166
85
167
pre ( ) {
86
- this . _newImports = new Map ( ) ;
168
+ this . _newStaticImports = new Map ( ) ;
169
+ this . _newDynamicImports = new Map ( ) ;
170
+ this . _isIncluded = false ;
87
171
this . _hasValidImport = false ;
88
172
this . _isDictEntry = false ;
173
+ this . _useDynamicHelpers = false ;
174
+
175
+ // If filesList is provided, check if current file is included
176
+ const filename = this . file . opts . filename ;
177
+ if ( this . opts . filesList && filename ) {
178
+ const isIncluded = this . opts . filesList . includes ( filename ) ;
179
+
180
+ if ( ! isIncluded ) {
181
+ // Force _isIncluded to false to skip processing
182
+ this . _isIncluded = false ;
183
+ return ;
184
+ }
185
+ }
89
186
} ,
90
187
91
188
visitor : {
@@ -108,13 +205,39 @@ export const intlayerBabelPlugin = (): PluginObj<State> => {
108
205
exit ( programPath , state ) {
109
206
if ( state . _isDictEntry ) return ; // nothing else to do – already replaced
110
207
if ( ! state . _hasValidImport ) return ; // early-out if we touched nothing
208
+ if ( ! state . _isIncluded ) return ; // early-out if file is not included
111
209
112
210
const file = state . file . opts . filename ! ;
113
- const dictDir = state . opts . dictionariesDir ;
211
+ const dictionariesDir = state . opts . dictionariesDir ;
212
+ const dynamicDictionariesDir = state . opts . dynamicDictionariesDir ;
114
213
const imports : t . ImportDeclaration [ ] = [ ] ;
115
214
116
- for ( const [ key , ident ] of state . _newImports ! ) {
117
- const rel = computeRelativeImport ( file , dictDir , key ) ;
215
+ // Generate static imports (for getIntlayer and useIntlayer when not using dynamic)
216
+ for ( const [ key , ident ] of state . _newStaticImports ! ) {
217
+ const rel = computeRelativeImport (
218
+ file ,
219
+ dictionariesDir ,
220
+ dynamicDictionariesDir ,
221
+ key ,
222
+ false // Always static
223
+ ) ;
224
+ imports . push (
225
+ t . importDeclaration (
226
+ [ t . importDefaultSpecifier ( t . identifier ( ident . name ) ) ] ,
227
+ t . stringLiteral ( rel )
228
+ )
229
+ ) ;
230
+ }
231
+
232
+ // Generate dynamic imports (for useIntlayer when using dynamic helpers)
233
+ for ( const [ key , ident ] of state . _newDynamicImports ! ) {
234
+ const rel = computeRelativeImport (
235
+ file ,
236
+ dictionariesDir ,
237
+ dynamicDictionariesDir ,
238
+ key ,
239
+ true // Always dynamic
240
+ ) ;
118
241
imports . push (
119
242
t . importDeclaration (
120
243
[ t . importDefaultSpecifier ( t . identifier ( ident . name ) ) ] ,
@@ -133,8 +256,8 @@ export const intlayerBabelPlugin = (): PluginObj<State> => {
133
256
if (
134
257
t . isExpressionStatement ( stmt ) &&
135
258
t . isStringLiteral ( stmt . expression ) &&
136
- ( stmt . expression . value === 'use client' ||
137
- stmt . expression . value === 'use server ')
259
+ ! stmt . expression . value . startsWith ( 'import' ) &&
260
+ ! stmt . expression . value . startsWith ( 'require ')
138
261
) {
139
262
insertPos += 1 ;
140
263
} else {
@@ -165,15 +288,40 @@ export const intlayerBabelPlugin = (): PluginObj<State> => {
165
288
? spec . imported . name
166
289
: ( spec . imported as t . StringLiteral ) . value ;
167
290
168
- if ( importedName === 'useIntlayer' ) {
169
- spec . imported = t . identifier ( 'useDictionary' ) ;
170
- } else if ( importedName === 'getIntlayer' ) {
171
- spec . imported = t . identifier ( 'getDictionary' ) ;
291
+ const activateDynamicImport = state . opts . activateDynamicImport ;
292
+ // Determine whether this import should use the dynamic helpers. We
293
+ // only switch to the dynamic helpers when (1) the option is turned
294
+ // on AND (2) the package we are importing from supports the dynamic
295
+ // helpers.
296
+ const shouldUseDynamicHelpers =
297
+ activateDynamicImport && PACKAGE_LIST_DYNAMIC . includes ( src as any ) ;
298
+
299
+ // Remember for later (CallExpression) whether we are using the dynamic helpers
300
+ if ( shouldUseDynamicHelpers ) {
301
+ state . _useDynamicHelpers = true ;
302
+ }
303
+
304
+ const helperMap = shouldUseDynamicHelpers
305
+ ? ( {
306
+ ...STATIC_IMPORT_FUNCTION ,
307
+ ...DYNAMIC_IMPORT_FUNCTION ,
308
+ } as Record < string , string > )
309
+ : ( STATIC_IMPORT_FUNCTION as Record < string , string > ) ;
310
+
311
+ const newIdentifier = helperMap [ importedName ] ;
312
+
313
+ // Only rewrite when we actually have a mapping for the imported
314
+ // specifier (ignore unrelated named imports).
315
+ if ( newIdentifier ) {
316
+ // Keep the local alias intact (so calls remain `useIntlayer` /
317
+ // `getIntlayer`), but rewrite the imported identifier so it
318
+ // points to our helper implementation.
319
+ spec . imported = t . identifier ( newIdentifier ) ;
172
320
}
173
321
}
174
322
} ,
175
323
176
- /* 2. Replace calls: useIntlayer("foo") → useDictionary(_hash) */
324
+ /* 2. Replace calls: useIntlayer("foo") → useDictionary(_hash) or useDictionaryDynamic(_hash, "foo") */
177
325
CallExpression ( path , state ) {
178
326
if ( state . _isDictEntry ) return ; // skip if entry file – already handled
179
327
@@ -182,23 +330,50 @@ export const intlayerBabelPlugin = (): PluginObj<State> => {
182
330
if ( ! CALLER_LIST . includes ( callee . name as any ) ) return ;
183
331
184
332
// Ensure we ultimately emit helper imports for files that *invoke*
185
- // the hooks, even if they didn’ t import them directly (edge cases with
333
+ // the hooks, even if they didn' t import them directly (edge cases with
186
334
// re-exports).
187
335
state . _hasValidImport = true ;
188
336
189
337
const arg = path . node . arguments [ 0 ] ;
190
338
if ( ! arg || ! t . isStringLiteral ( arg ) ) return ; // must be literal
191
339
192
340
const key = arg . value ;
193
- // per-file cache
194
- let ident = state . _newImports ! . get ( key ) ;
195
- if ( ! ident ) {
196
- ident = makeIdent ( key ) ;
197
- state . _newImports ! . set ( key , ident ) ;
198
- }
341
+ const useDynamic = Boolean ( state . _useDynamicHelpers ) ;
199
342
200
- // replace first arg with ident
201
- path . node . arguments [ 0 ] = t . identifier ( ident . name ) ;
343
+ // Determine if this specific call should use dynamic imports
344
+ const shouldUseDynamicForThisCall =
345
+ callee . name === 'useIntlayer' && useDynamic ;
346
+
347
+ let ident : t . Identifier ;
348
+
349
+ if ( shouldUseDynamicForThisCall ) {
350
+ // Use dynamic imports for useIntlayer when dynamic helpers are enabled
351
+ let dynamicIdent = state . _newDynamicImports ! . get ( key ) ;
352
+ if ( ! dynamicIdent ) {
353
+ // Create a unique identifier for dynamic imports by appending a suffix
354
+ const hash = getFileHash ( key ) ;
355
+ dynamicIdent = t . identifier ( `_${ hash } _dyn` ) ;
356
+ state . _newDynamicImports ! . set ( key , dynamicIdent ) ;
357
+ }
358
+ ident = dynamicIdent ;
359
+
360
+ // Dynamic helper: first argument is the dictionary, second is the key.
361
+ path . node . arguments = [
362
+ t . identifier ( ident . name ) ,
363
+ ...path . node . arguments ,
364
+ ] ;
365
+ } else {
366
+ // Use static imports for getIntlayer or useIntlayer when not using dynamic helpers
367
+ let staticIdent = state . _newStaticImports ! . get ( key ) ;
368
+ if ( ! staticIdent ) {
369
+ staticIdent = makeIdent ( key ) ;
370
+ state . _newStaticImports ! . set ( key , staticIdent ) ;
371
+ }
372
+ ident = staticIdent ;
373
+
374
+ // Static helper (useDictionary / getDictionary): replace key with ident.
375
+ path . node . arguments [ 0 ] = t . identifier ( ident . name ) ;
376
+ }
202
377
} ,
203
378
} ,
204
379
} ;
0 commit comments