Skip to content

Commit 36e121d

Browse files
committed
feat(build): implement build optimization
1 parent c47237b commit 36e121d

File tree

25 files changed

+773
-206
lines changed

25 files changed

+773
-206
lines changed

examples/nextjs-15-app/intlayer.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ const config: IntlayerConfig = {
1010
strictMode: 'strict',
1111
},
1212
content: {
13-
contentDir: ['./', '../../apps'],
13+
// contentDir: ['./', '../../apps'],
1414
},
1515
editor: {
1616
applicationURL: 'http://localhost:3000',
1717
},
18+
build: {
19+
optimize: process.env.NODE_ENV === 'production',
20+
activateDynamicImport: true,
21+
},
1822
};
1923

2024
export default config;

examples/vite-react-app/intlayer.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const config: IntlayerConfig = {
1818
clientId: process.env.INTLAYER_CLIENT_ID,
1919
clientSecret: process.env.INTLAYER_CLIENT_SECRET,
2020
},
21+
build: {
22+
optimize: process.env.NODE_ENV === 'production',
23+
activateDynamicImport: true,
24+
},
2125
};
2226

2327
export default config;

packages/@intlayer/babel/src/babel-plugin-intlayer.ts

Lines changed: 210 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,66 @@ const PACKAGE_LIST = [
2323

2424
const CALLER_LIST = ['useIntlayer', 'getIntlayer'] as const;
2525

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+
2647
/* ────────────────────────────────────────── types ───────────────────────── */
2748

2849
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>;
3280
/** whether the current file imported *any* intlayer package */
3381
_hasValidImport?: boolean;
3482
/** whether the current file *is* the dictionaries entry file */
3583
_isDictEntry?: boolean;
84+
/** whether dynamic helpers are active for this file */
85+
_useDynamicHelpers?: boolean;
3686
};
3787

3888
/* ────────────────────────────────────────── helpers ─────────────────────── */
@@ -49,10 +99,15 @@ const makeIdent = (key: string): t.Identifier => {
4999

50100
const computeRelativeImport = (
51101
fromFile: string,
52-
dictDir: string,
53-
key: string
102+
dictionariesDir: string,
103+
dynamicDictionariesDir: string,
104+
key: string,
105+
isDynamic = false
54106
): string => {
55-
const jsonPath = join(dictDir, `${key}.json`);
107+
const jsonPath = isDynamic
108+
? join(dynamicDictionariesDir, `${key}.mjs`)
109+
: join(dictionariesDir, `${key}.json`);
110+
56111
let rel = relative(dirname(fromFile), jsonPath).replace(/\\/g, '/'); // win →
57112
if (!rel.startsWith('./') && !rel.startsWith('../')) rel = `./${rel}`;
58113
return rel;
@@ -64,28 +119,70 @@ const computeRelativeImport = (
64119
* Babel plugin that transforms `useIntlayer/getIntlayer` calls into
65120
* `useDictionary/getDictionary` and auto-imports the required JSON dictionaries.
66121
*
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`.
73122
*
74123
* 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+
*
75137
* ```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');
77161
* ```
78-
* —where `useIntlayer` is merely an *alias* or re-export—are left untouched
79-
* because `imported.name` is `useDictionary`.
80162
*/
81163
export const intlayerBabelPlugin = (): PluginObj<State> => {
82164
return {
83165
name: 'babel-plugin-intlayer-transform',
84166

85167
pre() {
86-
this._newImports = new Map();
168+
this._newStaticImports = new Map();
169+
this._newDynamicImports = new Map();
170+
this._isIncluded = false;
87171
this._hasValidImport = false;
88172
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+
}
89186
},
90187

91188
visitor: {
@@ -108,13 +205,39 @@ export const intlayerBabelPlugin = (): PluginObj<State> => {
108205
exit(programPath, state) {
109206
if (state._isDictEntry) return; // nothing else to do – already replaced
110207
if (!state._hasValidImport) return; // early-out if we touched nothing
208+
if (!state._isIncluded) return; // early-out if file is not included
111209

112210
const file = state.file.opts.filename!;
113-
const dictDir = state.opts.dictionariesDir;
211+
const dictionariesDir = state.opts.dictionariesDir;
212+
const dynamicDictionariesDir = state.opts.dynamicDictionariesDir;
114213
const imports: t.ImportDeclaration[] = [];
115214

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+
);
118241
imports.push(
119242
t.importDeclaration(
120243
[t.importDefaultSpecifier(t.identifier(ident.name))],
@@ -133,8 +256,8 @@ export const intlayerBabelPlugin = (): PluginObj<State> => {
133256
if (
134257
t.isExpressionStatement(stmt) &&
135258
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')
138261
) {
139262
insertPos += 1;
140263
} else {
@@ -165,15 +288,40 @@ export const intlayerBabelPlugin = (): PluginObj<State> => {
165288
? spec.imported.name
166289
: (spec.imported as t.StringLiteral).value;
167290

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);
172320
}
173321
}
174322
},
175323

176-
/* 2. Replace calls: useIntlayer("foo") → useDictionary(_hash) */
324+
/* 2. Replace calls: useIntlayer("foo") → useDictionary(_hash) or useDictionaryDynamic(_hash, "foo") */
177325
CallExpression(path, state) {
178326
if (state._isDictEntry) return; // skip if entry file – already handled
179327

@@ -182,23 +330,50 @@ export const intlayerBabelPlugin = (): PluginObj<State> => {
182330
if (!CALLER_LIST.includes(callee.name as any)) return;
183331

184332
// Ensure we ultimately emit helper imports for files that *invoke*
185-
// the hooks, even if they didnt import them directly (edge cases with
333+
// the hooks, even if they didn't import them directly (edge cases with
186334
// re-exports).
187335
state._hasValidImport = true;
188336

189337
const arg = path.node.arguments[0];
190338
if (!arg || !t.isStringLiteral(arg)) return; // must be literal
191339

192340
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);
199342

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+
}
202377
},
203378
},
204379
};

packages/@intlayer/chokidar/src/filterDictionaryLocales.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ const filterTranlationsPlugin = (locales: Locales[] | Locales): Plugins => ({
1717
const isSingleLocale = localesArray.length === 1;
1818

1919
if (isSingleLocale) {
20-
return translationMap[localesArray[0] as Locales];
20+
return deepTransformNode(
21+
translationMap[localesArray[0] as Locales],
22+
props
23+
);
2124
}
2225

2326
const filteredTranslationMap = Object.fromEntries(

packages/@intlayer/chokidar/src/getBuiltDynamicDictionariesPath.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ export const getBuiltDynamicDictionariesPath = (
1616
mkdirSync(mainDir, { recursive: true });
1717
}
1818

19+
const extension = format === 'cjs' ? 'cjs' : 'mjs';
20+
1921
const dictionariesPath: string[] = fg.sync(
20-
`${dynamicDictionariesDir}/**/*.${format}`
22+
`${dynamicDictionariesDir}/**/*.${extension}`
2123
);
2224

2325
return dictionariesPath;

0 commit comments

Comments
 (0)