@intlify/unplugin-vue-i18n 11.0.7 → 11.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/lib/index.cjs +337 -50
- package/lib/index.d.cts +1 -1
- package/lib/index.d.mts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.mjs +337 -50
- package/lib/types.d.cts +26 -1
- package/lib/types.d.mts +26 -1
- package/lib/types.d.ts +26 -1
- package/package.json +2 -2
package/lib/index.mjs
CHANGED
|
@@ -37,6 +37,11 @@ function resolveOptions(options) {
|
|
|
37
37
|
const optimizeTranslationDirective = isString(options.optimizeTranslationDirective) || isArray(options.optimizeTranslationDirective) ? options.optimizeTranslationDirective : !!options.optimizeTranslationDirective;
|
|
38
38
|
const translationIdentifiers = /* @__PURE__ */ new Map();
|
|
39
39
|
const transformI18nBlock = typeof options.transformI18nBlock === "function" ? options.transformI18nBlock : null;
|
|
40
|
+
const treeShaking = options.treeShaking ? typeof options.treeShaking === "object" ? {
|
|
41
|
+
safelist: options.treeShaking.safelist || [],
|
|
42
|
+
dynamicKeyStrategy: options.treeShaking.dynamicKeyStrategy || "keep-all",
|
|
43
|
+
scanPatterns: options.treeShaking.scanPatterns
|
|
44
|
+
} : { safelist: [], dynamicKeyStrategy: "keep-all" } : null;
|
|
40
45
|
return {
|
|
41
46
|
include: normalizeGlobOption(options.include || []),
|
|
42
47
|
exclude: normalizeGlobOption(options.exclude),
|
|
@@ -55,7 +60,8 @@ function resolveOptions(options) {
|
|
|
55
60
|
escapeHtml,
|
|
56
61
|
optimizeTranslationDirective,
|
|
57
62
|
translationIdentifiers,
|
|
58
|
-
transformI18nBlock
|
|
63
|
+
transformI18nBlock,
|
|
64
|
+
treeShaking
|
|
59
65
|
};
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -167,7 +173,7 @@ const RE_VIRTUAL_PREFIXED_INTLIFY_BUNDLE_IMPORT_ID = new RegExp(
|
|
|
167
173
|
const RE_RESOURCE_FORMAT = /\.(json5?|ya?ml|[c|m]?[j|t]s)$/;
|
|
168
174
|
const RE_SFC_I18N_CUSTOM_BLOCK = /\?vue&type=i18n/;
|
|
169
175
|
const RE_SFC_I18N_WEBPACK_CUSTOM_BLOCK = /blockType=i18n/;
|
|
170
|
-
const debug$
|
|
176
|
+
const debug$3 = createDebug(resolveNamespace("resource"));
|
|
171
177
|
function resourcePlugin({
|
|
172
178
|
onlyLocales,
|
|
173
179
|
include,
|
|
@@ -185,7 +191,7 @@ function resourcePlugin({
|
|
|
185
191
|
allowDynamic,
|
|
186
192
|
escapeHtml,
|
|
187
193
|
transformI18nBlock
|
|
188
|
-
}, meta) {
|
|
194
|
+
}, meta, collector) {
|
|
189
195
|
let viteModule = null;
|
|
190
196
|
async function getViteModule() {
|
|
191
197
|
if (viteModule != null) {
|
|
@@ -213,15 +219,15 @@ function resourcePlugin({
|
|
|
213
219
|
return _filter;
|
|
214
220
|
}
|
|
215
221
|
if (meta.framework == "webpack") {
|
|
216
|
-
debug$
|
|
222
|
+
debug$3("Using filter for webpack");
|
|
217
223
|
_filter = createFilter(...resolveIncludeExclude());
|
|
218
224
|
} else {
|
|
219
225
|
const viteModule2 = await getViteModule();
|
|
220
226
|
if (viteModule2.rolldownVersion) {
|
|
221
|
-
debug$
|
|
227
|
+
debug$3("Using filter for rolldown-vite");
|
|
222
228
|
_filter = createFilter(...resolveIncludeExclude());
|
|
223
229
|
} else {
|
|
224
|
-
debug$
|
|
230
|
+
debug$3("Using filter for rollup-vite");
|
|
225
231
|
_filter = createFilter(...resolveIncludeExcludeForLegacy());
|
|
226
232
|
}
|
|
227
233
|
}
|
|
@@ -233,7 +239,7 @@ function resourcePlugin({
|
|
|
233
239
|
let isProduction = false;
|
|
234
240
|
let sourceMap = false;
|
|
235
241
|
const vueI18nAliasName = module;
|
|
236
|
-
debug$
|
|
242
|
+
debug$3(`vue-i18n alias name: ${vueI18nAliasName}`);
|
|
237
243
|
let vuePlugin = null;
|
|
238
244
|
const getSfcParser = () => {
|
|
239
245
|
return vuePlugin ? getVueCompiler(vuePlugin).parse : parse$1;
|
|
@@ -259,7 +265,7 @@ function resourcePlugin({
|
|
|
259
265
|
__VUE_I18N_PROD_DEVTOOLS__: false
|
|
260
266
|
}
|
|
261
267
|
};
|
|
262
|
-
debug$
|
|
268
|
+
debug$3("define Config:", defineConfig);
|
|
263
269
|
const aliasConfig = {
|
|
264
270
|
resolve: {
|
|
265
271
|
alias: {
|
|
@@ -270,7 +276,7 @@ function resourcePlugin({
|
|
|
270
276
|
}
|
|
271
277
|
}
|
|
272
278
|
};
|
|
273
|
-
debug$
|
|
279
|
+
debug$3("alias Config:", aliasConfig);
|
|
274
280
|
return assign(defineConfig, aliasConfig);
|
|
275
281
|
},
|
|
276
282
|
async configResolved(config) {
|
|
@@ -280,9 +286,9 @@ function resourcePlugin({
|
|
|
280
286
|
}
|
|
281
287
|
isProduction = config.isProduction;
|
|
282
288
|
sourceMap = config.command === "build" ? !!config.build.sourcemap : false;
|
|
283
|
-
debug$
|
|
289
|
+
debug$3(`configResolved: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
|
|
284
290
|
const isRolldownVite = !!(await getViteModule()).rolldownVersion;
|
|
285
|
-
debug$
|
|
291
|
+
debug$3(`Using ${isRolldownVite ? "rolldown-vite" : "vite"} for the build`);
|
|
286
292
|
if (!isRolldownVite) {
|
|
287
293
|
const jsonPlugin = getVitePlugin(config, "vite:json");
|
|
288
294
|
if (jsonPlugin) {
|
|
@@ -296,7 +302,7 @@ function resourcePlugin({
|
|
|
296
302
|
if (query.vue) {
|
|
297
303
|
return;
|
|
298
304
|
}
|
|
299
|
-
debug$
|
|
305
|
+
debug$3("org json plugin");
|
|
300
306
|
return orgTransform.apply(this, [code, id]);
|
|
301
307
|
}
|
|
302
308
|
jsonPlugin.transform = overrideJson;
|
|
@@ -321,7 +327,7 @@ function resourcePlugin({
|
|
|
321
327
|
webpack(compiler) {
|
|
322
328
|
isProduction = compiler.options.mode !== "development";
|
|
323
329
|
sourceMap = !!compiler.options.devtool;
|
|
324
|
-
debug$
|
|
330
|
+
debug$3(`webpack: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
|
|
325
331
|
compiler.options.resolve = normalizeConfigResolveAlias(
|
|
326
332
|
compiler.options.resolve,
|
|
327
333
|
meta.framework
|
|
@@ -330,7 +336,7 @@ function resourcePlugin({
|
|
|
330
336
|
ssr: ssrBuild,
|
|
331
337
|
runtimeOnly
|
|
332
338
|
});
|
|
333
|
-
debug$
|
|
339
|
+
debug$3(
|
|
334
340
|
`set ${vueI18nAliasName}: ${getVueI18nAliasPath({
|
|
335
341
|
ssr: ssrBuild,
|
|
336
342
|
runtimeOnly
|
|
@@ -343,15 +349,15 @@ function resourcePlugin({
|
|
|
343
349
|
__INTLIFY_PROD_DEVTOOLS__: "false"
|
|
344
350
|
})
|
|
345
351
|
);
|
|
346
|
-
debug$
|
|
347
|
-
debug$
|
|
352
|
+
debug$3(`set __VUE_I18N_LEGACY_API__ is '${compositionOnly}'`);
|
|
353
|
+
debug$3(`set __VUE_I18N_FULL_INSTALL__ is '${fullInstall}'`);
|
|
348
354
|
const filter = createFilter(...resolveIncludeExclude());
|
|
349
355
|
if (compiler.options.module) {
|
|
350
356
|
compiler.options.module.rules.push({
|
|
351
357
|
test: RE_RESOURCE_FORMAT,
|
|
352
358
|
type: "javascript/auto",
|
|
353
359
|
include(resource) {
|
|
354
|
-
debug$
|
|
360
|
+
debug$3("webpack resource include", resource);
|
|
355
361
|
return filter(resource);
|
|
356
362
|
}
|
|
357
363
|
});
|
|
@@ -370,7 +376,7 @@ function resourcePlugin({
|
|
|
370
376
|
id: RE_VIRTUAL_PREFIXED_INTLIFY_BUNDLE_IMPORT_ID
|
|
371
377
|
},
|
|
372
378
|
async handler(id) {
|
|
373
|
-
debug$
|
|
379
|
+
debug$3("load", id);
|
|
374
380
|
if (INTLIFY_BUNDLE_IMPORT_ID === getVirtualId(id, meta.framework) && include) {
|
|
375
381
|
let resourcePaths = [];
|
|
376
382
|
for (const inc of include) {
|
|
@@ -380,7 +386,8 @@ function resourcePlugin({
|
|
|
380
386
|
const code = await generateBundleResources(resourcePaths, isProduction, {
|
|
381
387
|
forceStringify,
|
|
382
388
|
strictMessage,
|
|
383
|
-
escapeHtml
|
|
389
|
+
escapeHtml,
|
|
390
|
+
usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
|
|
384
391
|
});
|
|
385
392
|
return {
|
|
386
393
|
moduleType: "js",
|
|
@@ -397,7 +404,7 @@ function resourcePlugin({
|
|
|
397
404
|
return;
|
|
398
405
|
}
|
|
399
406
|
const { filename, query } = parseVueRequest(id);
|
|
400
|
-
debug$
|
|
407
|
+
debug$3("transform", id, JSON.stringify(query), filename);
|
|
401
408
|
let langInfo = defaultSFCLang;
|
|
402
409
|
let inSourceMap;
|
|
403
410
|
if (!query.vue) {
|
|
@@ -416,13 +423,14 @@ function resourcePlugin({
|
|
|
416
423
|
escapeHtml,
|
|
417
424
|
jit: true,
|
|
418
425
|
onlyLocales,
|
|
419
|
-
forceStringify
|
|
426
|
+
forceStringify,
|
|
427
|
+
usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
|
|
420
428
|
}
|
|
421
429
|
);
|
|
422
|
-
debug$
|
|
430
|
+
debug$3("parseOptions", parseOptions);
|
|
423
431
|
const { code: generatedCode, map } = await generate(code, parseOptions);
|
|
424
|
-
debug$
|
|
425
|
-
debug$
|
|
432
|
+
debug$3("generated code", generatedCode);
|
|
433
|
+
debug$3("sourcemap", map, sourceMap);
|
|
426
434
|
if (code === generatedCode) return;
|
|
427
435
|
return {
|
|
428
436
|
moduleType: "js",
|
|
@@ -437,8 +445,9 @@ function resourcePlugin({
|
|
|
437
445
|
} else if (defaultSFCLang) {
|
|
438
446
|
langInfo = defaultSFCLang;
|
|
439
447
|
}
|
|
440
|
-
debug$
|
|
441
|
-
const generate =
|
|
448
|
+
debug$3("langInfo", langInfo);
|
|
449
|
+
const generate = getGenerator(langInfo, generateYAML);
|
|
450
|
+
const isGlobalBlock = globalSFCScope || !!query.global;
|
|
442
451
|
const parseOptions = getOptions(
|
|
443
452
|
filename,
|
|
444
453
|
isProduction,
|
|
@@ -447,14 +456,16 @@ function resourcePlugin({
|
|
|
447
456
|
{
|
|
448
457
|
inSourceMap,
|
|
449
458
|
isGlobal: globalSFCScope,
|
|
459
|
+
allowDynamic,
|
|
450
460
|
jit: true,
|
|
451
461
|
strictMessage,
|
|
452
462
|
escapeHtml,
|
|
453
463
|
onlyLocales,
|
|
454
|
-
forceStringify
|
|
464
|
+
forceStringify,
|
|
465
|
+
usedKeyFilter: collector && isGlobalBlock ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
|
|
455
466
|
}
|
|
456
467
|
);
|
|
457
|
-
debug$
|
|
468
|
+
debug$3("parseOptions", parseOptions);
|
|
458
469
|
let source = await getCode(
|
|
459
470
|
code,
|
|
460
471
|
filename,
|
|
@@ -471,9 +482,12 @@ function resourcePlugin({
|
|
|
471
482
|
warn("transformI18nBlock should return a string");
|
|
472
483
|
}
|
|
473
484
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
485
|
+
if (/\.?[cm]?[jt]s$/.test(langInfo)) {
|
|
486
|
+
source = `export default ${source.replace(/^[\s;]+/, "")}`;
|
|
487
|
+
}
|
|
488
|
+
const { code: generatedCode, map } = await generate(source, parseOptions);
|
|
489
|
+
debug$3("generated code", generatedCode);
|
|
490
|
+
debug$3("sourcemap", map, sourceMap);
|
|
477
491
|
if (code === generatedCode) return;
|
|
478
492
|
return {
|
|
479
493
|
moduleType: "js",
|
|
@@ -491,13 +505,13 @@ function getGenerator(ext, fallback = generateJSON) {
|
|
|
491
505
|
if (/\.?json5?$/.test(ext)) {
|
|
492
506
|
return generateJSON;
|
|
493
507
|
}
|
|
494
|
-
if (
|
|
508
|
+
if (/\.?ya?ml$/.test(ext)) {
|
|
495
509
|
return generateYAML;
|
|
496
510
|
}
|
|
497
|
-
if (
|
|
511
|
+
if (/\.?[cm]?js$/.test(ext)) {
|
|
498
512
|
return generateJavaScript;
|
|
499
513
|
}
|
|
500
|
-
if (
|
|
514
|
+
if (/\.?[cm]?ts$/.test(ext)) {
|
|
501
515
|
return generateTypescript;
|
|
502
516
|
}
|
|
503
517
|
return fallback;
|
|
@@ -529,11 +543,12 @@ async function generateBundleResources(resources, isProduction, {
|
|
|
529
543
|
strictMessage = true,
|
|
530
544
|
escapeHtml = false,
|
|
531
545
|
jit = true,
|
|
532
|
-
transformI18nBlock = void 0
|
|
546
|
+
transformI18nBlock = void 0,
|
|
547
|
+
usedKeyFilter = void 0
|
|
533
548
|
}) {
|
|
534
549
|
const codes = [];
|
|
535
550
|
for (const res of resources) {
|
|
536
|
-
debug$
|
|
551
|
+
debug$3(`${res} bundle loading ...`);
|
|
537
552
|
if (/\.(json5?|ya?ml)$/.test(res)) {
|
|
538
553
|
const { ext, name } = parse(res);
|
|
539
554
|
const source = await getRaw(res);
|
|
@@ -545,11 +560,12 @@ async function generateBundleResources(resources, isProduction, {
|
|
|
545
560
|
strictMessage,
|
|
546
561
|
escapeHtml,
|
|
547
562
|
forceStringify,
|
|
548
|
-
transformI18nBlock
|
|
563
|
+
transformI18nBlock,
|
|
564
|
+
usedKeyFilter
|
|
549
565
|
});
|
|
550
566
|
parseOptions.type = "bare";
|
|
551
567
|
const { code } = generate(source, parseOptions);
|
|
552
|
-
debug$
|
|
568
|
+
debug$3("generated code", code);
|
|
553
569
|
codes.push(`${JSON.stringify(name)}: ${code}`);
|
|
554
570
|
}
|
|
555
571
|
}
|
|
@@ -584,7 +600,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
|
|
|
584
600
|
}
|
|
585
601
|
if (framework === "webpack") {
|
|
586
602
|
if (issuerPath) {
|
|
587
|
-
debug$
|
|
603
|
+
debug$3(`getCode (webpack) ${index} via issuerPath`, issuerPath);
|
|
588
604
|
return await getRaw(filename);
|
|
589
605
|
} else {
|
|
590
606
|
const result = parser(await getRaw(filename), {
|
|
@@ -594,7 +610,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
|
|
|
594
610
|
const block = result.descriptor.customBlocks[index];
|
|
595
611
|
if (block) {
|
|
596
612
|
const code = block.src ? await getRaw(block.src) : block.content;
|
|
597
|
-
debug$
|
|
613
|
+
debug$3(`getCode (webpack) ${index} from SFC`, code);
|
|
598
614
|
return code;
|
|
599
615
|
} else {
|
|
600
616
|
return source;
|
|
@@ -618,7 +634,8 @@ function getOptions(filename, isProduction, query, sourceMap, {
|
|
|
618
634
|
strictMessage = true,
|
|
619
635
|
escapeHtml = false,
|
|
620
636
|
jit = true,
|
|
621
|
-
transformI18nBlock = null
|
|
637
|
+
transformI18nBlock = null,
|
|
638
|
+
usedKeyFilter = void 0
|
|
622
639
|
}) {
|
|
623
640
|
const mode = isProduction ? "production" : "development";
|
|
624
641
|
const baseOptions = {
|
|
@@ -631,6 +648,7 @@ function getOptions(filename, isProduction, query, sourceMap, {
|
|
|
631
648
|
escapeHtml,
|
|
632
649
|
jit,
|
|
633
650
|
onlyLocales,
|
|
651
|
+
usedKeyFilter,
|
|
634
652
|
env: mode,
|
|
635
653
|
transformI18nBlock,
|
|
636
654
|
onWarn: (msg) => {
|
|
@@ -675,7 +693,7 @@ async function getRaw(path) {
|
|
|
675
693
|
return promises.readFile(path, { encoding: "utf-8" });
|
|
676
694
|
}
|
|
677
695
|
|
|
678
|
-
const debug$
|
|
696
|
+
const debug$2 = createDebug(resolveNamespace("directive"));
|
|
679
697
|
const tsEstree = {
|
|
680
698
|
parse: void 0,
|
|
681
699
|
simpleTraverse: void 0,
|
|
@@ -709,7 +727,7 @@ function directivePlugin({
|
|
|
709
727
|
optimizeTranslationDirective,
|
|
710
728
|
translationIdentifiers
|
|
711
729
|
);
|
|
712
|
-
debug$
|
|
730
|
+
debug$2(`vite:vue options['template']:`, vuePlugin.api.options);
|
|
713
731
|
}
|
|
714
732
|
},
|
|
715
733
|
configResolved(config) {
|
|
@@ -752,7 +770,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
|
|
|
752
770
|
const vuePluginOptions = getVuePluginOptions(vuePlugin);
|
|
753
771
|
const normalizedFilename = normalizePath(path.relative(vuePluginOptions.root, filename));
|
|
754
772
|
const resolveIdentifier2 = translationIdentifiers.get(normalizedFilename);
|
|
755
|
-
debug$
|
|
773
|
+
debug$2("resolved vue-i18n Identifier", resolveIdentifier2);
|
|
756
774
|
if (resolveIdentifier2 == null) {
|
|
757
775
|
return void 0;
|
|
758
776
|
}
|
|
@@ -770,7 +788,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
|
|
|
770
788
|
}
|
|
771
789
|
function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
|
|
772
790
|
const source = descriptor.scriptSetup?.content || descriptor.script?.content;
|
|
773
|
-
debug$
|
|
791
|
+
debug$2("getDescriptor content", source);
|
|
774
792
|
if (!source) {
|
|
775
793
|
return;
|
|
776
794
|
}
|
|
@@ -788,11 +806,11 @@ function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
|
|
|
788
806
|
if (importLocalName == null) {
|
|
789
807
|
return;
|
|
790
808
|
}
|
|
791
|
-
debug$
|
|
809
|
+
debug$2("importLocalName", importLocalName);
|
|
792
810
|
const resolvedIdentifier = getVueI18nIdentifier(scope, importLocalName);
|
|
793
811
|
if (resolvedIdentifier) {
|
|
794
812
|
const normalizedFilename = normalizePath(path.relative(root, descriptor.filename));
|
|
795
|
-
debug$
|
|
813
|
+
debug$2("set vue-i18n resolved identifier: ", resolvedIdentifier);
|
|
796
814
|
translationIdentifiers.set(normalizedFilename, resolvedIdentifier);
|
|
797
815
|
}
|
|
798
816
|
}
|
|
@@ -840,9 +858,9 @@ function getVueI18nIdentifier(scope, local) {
|
|
|
840
858
|
return null;
|
|
841
859
|
}
|
|
842
860
|
const variableIdPairs = parseVariableId(id);
|
|
843
|
-
debug$
|
|
861
|
+
debug$2("variableIdPairs:", variableIdPairs);
|
|
844
862
|
const returnVariableIdPairs = parseReturnStatement(returnStatement);
|
|
845
|
-
debug$
|
|
863
|
+
debug$2("returnVariableIdPairs:", returnVariableIdPairs);
|
|
846
864
|
return resolveIdentifier(variableIdPairs, returnVariableIdPairs);
|
|
847
865
|
}
|
|
848
866
|
const EMPTY_NODE_RETURN = {
|
|
@@ -954,6 +972,270 @@ function resolveIdentifier(localVariables, returnVariable) {
|
|
|
954
972
|
}
|
|
955
973
|
}
|
|
956
974
|
|
|
975
|
+
function matchSafelistPattern(keyPath, pattern) {
|
|
976
|
+
const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^.]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
977
|
+
return new RegExp(`^${regexStr}$`).test(keyPath);
|
|
978
|
+
}
|
|
979
|
+
function createUsedKeysCollector(options) {
|
|
980
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
981
|
+
const safelistPatterns = options.safelist || [];
|
|
982
|
+
const dynamicKeyStrategy = options.dynamicKeyStrategy || "keep-all";
|
|
983
|
+
const removedKeys = /* @__PURE__ */ new Map();
|
|
984
|
+
let dynamicKeysDetected = false;
|
|
985
|
+
return {
|
|
986
|
+
get usedKeys() {
|
|
987
|
+
return usedKeys;
|
|
988
|
+
},
|
|
989
|
+
get dynamicKeysDetected() {
|
|
990
|
+
return dynamicKeysDetected;
|
|
991
|
+
},
|
|
992
|
+
set dynamicKeysDetected(val) {
|
|
993
|
+
dynamicKeysDetected = val;
|
|
994
|
+
},
|
|
995
|
+
safelistPatterns,
|
|
996
|
+
removedKeys,
|
|
997
|
+
addKey(key) {
|
|
998
|
+
usedKeys.add(key);
|
|
999
|
+
},
|
|
1000
|
+
markDynamic() {
|
|
1001
|
+
dynamicKeysDetected = true;
|
|
1002
|
+
},
|
|
1003
|
+
shouldKeepKey(keyPath) {
|
|
1004
|
+
if (dynamicKeysDetected && dynamicKeyStrategy === "keep-all") {
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
if (safelistPatterns.some((pattern) => matchSafelistPattern(keyPath, pattern))) {
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
1010
|
+
if (usedKeys.has(keyPath)) {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
for (const key of usedKeys) {
|
|
1014
|
+
if (key.startsWith(keyPath + ".")) {
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return false;
|
|
1019
|
+
},
|
|
1020
|
+
reportRemoved(filename, keyPath) {
|
|
1021
|
+
const existing = removedKeys.get(filename);
|
|
1022
|
+
if (existing) {
|
|
1023
|
+
existing.push(keyPath);
|
|
1024
|
+
} else {
|
|
1025
|
+
removedKeys.set(filename, [keyPath]);
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
getDiagnostics() {
|
|
1029
|
+
let totalRemoved = 0;
|
|
1030
|
+
for (const keys of removedKeys.values()) {
|
|
1031
|
+
totalRemoved += keys.length;
|
|
1032
|
+
}
|
|
1033
|
+
return { totalRemoved, byFile: removedKeys };
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const I18N_FUNCTIONS = /* @__PURE__ */ new Set(["t", "$t", "tc", "$tc", "te", "$te", "d", "$d", "n", "$n"]);
|
|
1039
|
+
const TEMPLATE_T_REGEX = /\$?t\s*\(\s*['"]([^'"]+)['"]/g;
|
|
1040
|
+
const VT_DIRECTIVE_STRING_REGEX = /v-t\s*=\s*"'([^']+)'"/g;
|
|
1041
|
+
const VT_DIRECTIVE_PATH_REGEX = /v-t\s*=\s*"\{[^}]*path\s*:\s*'([^']+)'/g;
|
|
1042
|
+
const TEMPLATE_DYNAMIC_T_REGEX = /\$?t\s*\(\s*[a-zA-Z_$]/g;
|
|
1043
|
+
async function extractKeysFromScript(source) {
|
|
1044
|
+
const keys = [];
|
|
1045
|
+
let hasDynamic = false;
|
|
1046
|
+
let tsEstree;
|
|
1047
|
+
try {
|
|
1048
|
+
tsEstree = await import('@typescript-eslint/typescript-estree');
|
|
1049
|
+
} catch {
|
|
1050
|
+
return extractKeysFromScriptRegex(source);
|
|
1051
|
+
}
|
|
1052
|
+
try {
|
|
1053
|
+
const ast = tsEstree.parse(source, {
|
|
1054
|
+
range: true,
|
|
1055
|
+
jsx: true,
|
|
1056
|
+
allowInvalidAST: true,
|
|
1057
|
+
suppressDeprecatedPropertyWarnings: true
|
|
1058
|
+
});
|
|
1059
|
+
tsEstree.simpleTraverse(ast, {
|
|
1060
|
+
enter(node) {
|
|
1061
|
+
if (node.type !== tsEstree.AST_NODE_TYPES.CallExpression) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const calleeName = getCalleeName(node, tsEstree.AST_NODE_TYPES);
|
|
1065
|
+
if (!calleeName || !I18N_FUNCTIONS.has(calleeName)) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const firstArg = node.arguments[0];
|
|
1069
|
+
if (!firstArg) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (firstArg.type === tsEstree.AST_NODE_TYPES.Literal && typeof firstArg.value === "string") {
|
|
1073
|
+
keys.push(firstArg.value);
|
|
1074
|
+
} else if (firstArg.type === tsEstree.AST_NODE_TYPES.TemplateLiteral && firstArg.expressions.length === 0 && firstArg.quasis.length === 1) {
|
|
1075
|
+
const value = firstArg.quasis[0].value.cooked;
|
|
1076
|
+
if (value) {
|
|
1077
|
+
keys.push(value);
|
|
1078
|
+
}
|
|
1079
|
+
} else {
|
|
1080
|
+
hasDynamic = true;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
} catch {
|
|
1085
|
+
return extractKeysFromScriptRegex(source);
|
|
1086
|
+
}
|
|
1087
|
+
return { keys, hasDynamic };
|
|
1088
|
+
}
|
|
1089
|
+
function getCalleeName(node, AST_NODE_TYPES) {
|
|
1090
|
+
const callee = node.callee;
|
|
1091
|
+
if (callee.type === AST_NODE_TYPES.Identifier) {
|
|
1092
|
+
return callee.name;
|
|
1093
|
+
}
|
|
1094
|
+
if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) {
|
|
1095
|
+
return callee.property.name;
|
|
1096
|
+
}
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
function extractKeysFromScriptRegex(source) {
|
|
1100
|
+
const keys = [];
|
|
1101
|
+
let match;
|
|
1102
|
+
const regex = /\b(?:\$?t|tc|\$tc|te|\$te|d|\$d|n|\$n)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
1103
|
+
while ((match = regex.exec(source)) !== null) {
|
|
1104
|
+
keys.push(match[1]);
|
|
1105
|
+
}
|
|
1106
|
+
return { keys, hasDynamic: false };
|
|
1107
|
+
}
|
|
1108
|
+
function extractKeysFromTemplate(templateContent) {
|
|
1109
|
+
const keys = [];
|
|
1110
|
+
let hasDynamic = false;
|
|
1111
|
+
let match;
|
|
1112
|
+
TEMPLATE_T_REGEX.lastIndex = 0;
|
|
1113
|
+
VT_DIRECTIVE_STRING_REGEX.lastIndex = 0;
|
|
1114
|
+
VT_DIRECTIVE_PATH_REGEX.lastIndex = 0;
|
|
1115
|
+
TEMPLATE_DYNAMIC_T_REGEX.lastIndex = 0;
|
|
1116
|
+
while ((match = TEMPLATE_T_REGEX.exec(templateContent)) !== null) {
|
|
1117
|
+
keys.push(match[1]);
|
|
1118
|
+
}
|
|
1119
|
+
while ((match = VT_DIRECTIVE_STRING_REGEX.exec(templateContent)) !== null) {
|
|
1120
|
+
keys.push(match[1]);
|
|
1121
|
+
}
|
|
1122
|
+
while ((match = VT_DIRECTIVE_PATH_REGEX.exec(templateContent)) !== null) {
|
|
1123
|
+
keys.push(match[1]);
|
|
1124
|
+
}
|
|
1125
|
+
if (TEMPLATE_DYNAMIC_T_REGEX.test(templateContent)) {
|
|
1126
|
+
hasDynamic = true;
|
|
1127
|
+
}
|
|
1128
|
+
return { keys, hasDynamic };
|
|
1129
|
+
}
|
|
1130
|
+
async function analyzeVueSFC(content) {
|
|
1131
|
+
const keys = [];
|
|
1132
|
+
let hasDynamic = false;
|
|
1133
|
+
let parse;
|
|
1134
|
+
try {
|
|
1135
|
+
const compilerSfc = await import('vue/compiler-sfc');
|
|
1136
|
+
parse = compilerSfc.parse;
|
|
1137
|
+
} catch {
|
|
1138
|
+
const templateResult = extractKeysFromTemplate(content);
|
|
1139
|
+
const scriptResult = await extractKeysFromScript(content);
|
|
1140
|
+
return {
|
|
1141
|
+
keys: [...templateResult.keys, ...scriptResult.keys],
|
|
1142
|
+
hasDynamic: templateResult.hasDynamic || scriptResult.hasDynamic
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const { descriptor } = parse(content);
|
|
1146
|
+
if (descriptor.template?.content) {
|
|
1147
|
+
const templateResult = extractKeysFromTemplate(descriptor.template.content);
|
|
1148
|
+
keys.push(...templateResult.keys);
|
|
1149
|
+
if (templateResult.hasDynamic) {
|
|
1150
|
+
hasDynamic = true;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content;
|
|
1154
|
+
if (scriptContent) {
|
|
1155
|
+
const scriptResult = await extractKeysFromScript(scriptContent);
|
|
1156
|
+
keys.push(...scriptResult.keys);
|
|
1157
|
+
if (scriptResult.hasDynamic) {
|
|
1158
|
+
hasDynamic = true;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return { keys, hasDynamic };
|
|
1162
|
+
}
|
|
1163
|
+
async function analyzeFile(content, filePath, collector) {
|
|
1164
|
+
let result;
|
|
1165
|
+
if (filePath.endsWith(".vue")) {
|
|
1166
|
+
result = await analyzeVueSFC(content);
|
|
1167
|
+
} else {
|
|
1168
|
+
result = await extractKeysFromScript(content);
|
|
1169
|
+
}
|
|
1170
|
+
for (const key of result.keys) {
|
|
1171
|
+
collector.addKey(key);
|
|
1172
|
+
}
|
|
1173
|
+
if (result.hasDynamic) {
|
|
1174
|
+
collector.markDynamic();
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const debug$1 = createDebug(resolveNamespace("tree-shaking"));
|
|
1179
|
+
function treeShakingPlugin(resolvedOptions, collector) {
|
|
1180
|
+
let projectRoot = "";
|
|
1181
|
+
return {
|
|
1182
|
+
name: resolveNamespace("tree-shaking"),
|
|
1183
|
+
enforce: "pre",
|
|
1184
|
+
vite: {
|
|
1185
|
+
configResolved(config) {
|
|
1186
|
+
projectRoot = config.root;
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
webpack(compiler) {
|
|
1190
|
+
projectRoot = compiler.options.context || process.cwd();
|
|
1191
|
+
},
|
|
1192
|
+
async buildStart() {
|
|
1193
|
+
const treeShaking = resolvedOptions.treeShaking;
|
|
1194
|
+
const patterns = treeShaking?.scanPatterns || [`${projectRoot}/src/**/*.{vue,ts,js,tsx,jsx}`];
|
|
1195
|
+
debug$1("scanning patterns:", patterns);
|
|
1196
|
+
const files = await fg(patterns, {
|
|
1197
|
+
ignore: ["**/node_modules/**"],
|
|
1198
|
+
absolute: true
|
|
1199
|
+
});
|
|
1200
|
+
debug$1(`found ${files.length} source files to scan`);
|
|
1201
|
+
for (const file of files) {
|
|
1202
|
+
try {
|
|
1203
|
+
const content = await promises.readFile(file, "utf-8");
|
|
1204
|
+
await analyzeFile(content, file, collector);
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
debug$1(`failed to analyze file: ${file}`, err);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
debug$1(
|
|
1210
|
+
`scan complete: ${collector.usedKeys.size} used keys found, dynamic: ${collector.dynamicKeysDetected}`
|
|
1211
|
+
);
|
|
1212
|
+
if (collector.dynamicKeysDetected) {
|
|
1213
|
+
const strategy = treeShaking?.dynamicKeyStrategy || "keep-all";
|
|
1214
|
+
if (strategy === "keep-all") {
|
|
1215
|
+
warn(
|
|
1216
|
+
"Tree-shaking: dynamic key usage detected. All keys will be preserved (dynamicKeyStrategy: keep-all)."
|
|
1217
|
+
);
|
|
1218
|
+
} else {
|
|
1219
|
+
warn(
|
|
1220
|
+
"Tree-shaking: dynamic key usage detected. Unused keys will still be removed (dynamicKeyStrategy: ignore)."
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1225
|
+
buildEnd() {
|
|
1226
|
+
const diagnostics = collector.getDiagnostics();
|
|
1227
|
+
if (diagnostics.totalRemoved > 0) {
|
|
1228
|
+
debug$1(`tree-shaking: removed ${diagnostics.totalRemoved} unused message keys`);
|
|
1229
|
+
for (const [file, keys] of diagnostics.byFile) {
|
|
1230
|
+
debug$1(` ${file}: removed ${keys.length} keys (${keys.join(", ")})`);
|
|
1231
|
+
}
|
|
1232
|
+
} else {
|
|
1233
|
+
debug$1("tree-shaking: no keys removed");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
957
1239
|
const debug = createDebug(resolveNamespace("root"));
|
|
958
1240
|
const unpluginFactory = (options = {}, meta) => {
|
|
959
1241
|
debug("meta framework", meta.framework);
|
|
@@ -963,7 +1245,12 @@ const unpluginFactory = (options = {}, meta) => {
|
|
|
963
1245
|
debug("plugin options (resolving):", options);
|
|
964
1246
|
const resolvedOptions = resolveOptions(options);
|
|
965
1247
|
debug("plugin options (resolved):", resolvedOptions);
|
|
966
|
-
const
|
|
1248
|
+
const collector = resolvedOptions.treeShaking ? createUsedKeysCollector(resolvedOptions.treeShaking) : null;
|
|
1249
|
+
const plugins = [];
|
|
1250
|
+
if (resolvedOptions.treeShaking && collector) {
|
|
1251
|
+
plugins.push(treeShakingPlugin(resolvedOptions, collector));
|
|
1252
|
+
}
|
|
1253
|
+
plugins.push(resourcePlugin(resolvedOptions, meta, collector));
|
|
967
1254
|
if (resolvedOptions.optimizeTranslationDirective) {
|
|
968
1255
|
if (meta.framework === "webpack") {
|
|
969
1256
|
raiseError(
|
package/lib/types.d.cts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
type SFCLangFormat = 'json' | 'json5' | 'yml' | 'yaml';
|
|
2
2
|
type VueI18nModule = 'vue-i18n' | 'petite-vue-i18n';
|
|
3
|
+
interface TreeShakingOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Message key patterns to always keep (never tree-shake).
|
|
6
|
+
* Supports glob-like patterns (e.g., `'errors.*'`, `'validation.**'`).
|
|
7
|
+
*/
|
|
8
|
+
safelist?: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Strategy when dynamic key usage (e.g., `t(variable)`) is detected.
|
|
11
|
+
* - `'keep-all'`: Disable tree-shaking and keep all keys (safe default).
|
|
12
|
+
* - `'ignore'`: Continue tree-shaking despite dynamic keys (risky, use with `safelist`).
|
|
13
|
+
* @default 'keep-all'
|
|
14
|
+
*/
|
|
15
|
+
dynamicKeyStrategy?: 'keep-all' | 'ignore';
|
|
16
|
+
/**
|
|
17
|
+
* Glob patterns for source files to scan for used message keys.
|
|
18
|
+
* Defaults to `${projectRoot}/src/**\/*.{vue,ts,js,tsx,jsx}` when not specified.
|
|
19
|
+
*/
|
|
20
|
+
scanPatterns?: string[];
|
|
21
|
+
}
|
|
3
22
|
interface PluginOptions {
|
|
4
23
|
include?: string | string[];
|
|
5
24
|
exclude?: string | string[];
|
|
@@ -18,6 +37,12 @@ interface PluginOptions {
|
|
|
18
37
|
escapeHtml?: boolean;
|
|
19
38
|
optimizeTranslationDirective?: boolean | string | string[];
|
|
20
39
|
transformI18nBlock?: (src: string | Buffer) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Enable message key tree-shaking to remove unused locale message keys.
|
|
42
|
+
* Only effective in production builds.
|
|
43
|
+
* @default false
|
|
44
|
+
*/
|
|
45
|
+
treeShaking?: boolean | TreeShakingOptions;
|
|
21
46
|
}
|
|
22
47
|
|
|
23
|
-
export type { PluginOptions, SFCLangFormat, VueI18nModule };
|
|
48
|
+
export type { PluginOptions, SFCLangFormat, TreeShakingOptions, VueI18nModule };
|