@intlayer/babel 9.4.4 → 9.5.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 +0 -2
- package/dist/cjs/analyzeContentUsage.cjs +301 -0
- package/dist/cjs/analyzeContentUsage.cjs.map +1 -0
- package/dist/cjs/babel-plugin-intlayer-field-rename.cjs +5 -4
- package/dist/cjs/babel-plugin-intlayer-field-rename.cjs.map +1 -1
- package/dist/cjs/babel-plugin-intlayer-optimize.cjs +171 -282
- package/dist/cjs/babel-plugin-intlayer-optimize.cjs.map +1 -1
- package/dist/cjs/babel-plugin-intlayer-purge.cjs +4 -3
- package/dist/cjs/babel-plugin-intlayer-purge.cjs.map +1 -1
- package/dist/cjs/babel-plugin-intlayer-usage-analyzer.cjs +49 -699
- package/dist/cjs/babel-plugin-intlayer-usage-analyzer.cjs.map +1 -1
- package/dist/cjs/compat/namespaceResolution.cjs +124 -0
- package/dist/cjs/compat/namespaceResolution.cjs.map +1 -0
- package/dist/cjs/compat/optimizePass.cjs +144 -0
- package/dist/cjs/compat/optimizePass.cjs.map +1 -0
- package/dist/cjs/compat/usageAnalysis.cjs +195 -0
- package/dist/cjs/compat/usageAnalysis.cjs.map +1 -0
- package/dist/cjs/dictionaryImports.cjs +40 -0
- package/dist/cjs/dictionaryImports.cjs.map +1 -0
- package/dist/cjs/extractContent/utils/extractDictionaryInfo.cjs +1 -1
- package/dist/cjs/getExtractPluginOptions.cjs +1 -1
- package/dist/cjs/getOptimizePluginOptions.cjs +2 -6
- package/dist/cjs/getOptimizePluginOptions.cjs.map +1 -1
- package/dist/cjs/getPurgePluginOptions.cjs +1 -1
- package/dist/cjs/index.cjs +9 -5
- package/dist/cjs/nativeCallers.cjs +41 -0
- package/dist/cjs/nativeCallers.cjs.map +1 -0
- package/dist/cjs/pruneContext.cjs +79 -0
- package/dist/cjs/pruneContext.cjs.map +1 -0
- package/dist/cjs/staticAstReaders.cjs +140 -0
- package/dist/cjs/staticAstReaders.cjs.map +1 -0
- package/dist/cjs/transformers.cjs +2 -1
- package/dist/cjs/transformers.cjs.map +1 -1
- package/dist/esm/analyzeContentUsage.mjs +300 -0
- package/dist/esm/analyzeContentUsage.mjs.map +1 -0
- package/dist/esm/babel-plugin-intlayer-field-rename.mjs +3 -2
- package/dist/esm/babel-plugin-intlayer-field-rename.mjs.map +1 -1
- package/dist/esm/babel-plugin-intlayer-optimize.mjs +171 -282
- package/dist/esm/babel-plugin-intlayer-optimize.mjs.map +1 -1
- package/dist/esm/babel-plugin-intlayer-purge.mjs +3 -2
- package/dist/esm/babel-plugin-intlayer-purge.mjs.map +1 -1
- package/dist/esm/babel-plugin-intlayer-usage-analyzer.mjs +43 -694
- package/dist/esm/babel-plugin-intlayer-usage-analyzer.mjs.map +1 -1
- package/dist/esm/compat/namespaceResolution.mjs +120 -0
- package/dist/esm/compat/namespaceResolution.mjs.map +1 -0
- package/dist/esm/compat/optimizePass.mjs +143 -0
- package/dist/esm/compat/optimizePass.mjs.map +1 -0
- package/dist/esm/compat/usageAnalysis.mjs +193 -0
- package/dist/esm/compat/usageAnalysis.mjs.map +1 -0
- package/dist/esm/dictionaryImports.mjs +38 -0
- package/dist/esm/dictionaryImports.mjs.map +1 -0
- package/dist/esm/extractContent/utils/extractDictionaryInfo.mjs +1 -1
- package/dist/esm/getExtractPluginOptions.mjs +1 -1
- package/dist/esm/getOptimizePluginOptions.mjs +2 -6
- package/dist/esm/getOptimizePluginOptions.mjs.map +1 -1
- package/dist/esm/getPurgePluginOptions.mjs +1 -1
- package/dist/esm/index.mjs +5 -2
- package/dist/esm/nativeCallers.mjs +38 -0
- package/dist/esm/nativeCallers.mjs.map +1 -0
- package/dist/esm/pruneContext.mjs +76 -0
- package/dist/esm/pruneContext.mjs.map +1 -0
- package/dist/esm/staticAstReaders.mjs +129 -0
- package/dist/esm/staticAstReaders.mjs.map +1 -0
- package/dist/esm/transformers.mjs +2 -1
- package/dist/esm/transformers.mjs.map +1 -1
- package/dist/types/analyzeContentUsage.d.ts +18 -0
- package/dist/types/analyzeContentUsage.d.ts.map +1 -0
- package/dist/types/babel-plugin-intlayer-field-rename.d.ts +2 -1
- package/dist/types/babel-plugin-intlayer-field-rename.d.ts.map +1 -1
- package/dist/types/babel-plugin-intlayer-optimize.d.ts +9 -15
- package/dist/types/babel-plugin-intlayer-optimize.d.ts.map +1 -1
- package/dist/types/babel-plugin-intlayer-purge.d.ts +3 -2
- package/dist/types/babel-plugin-intlayer-purge.d.ts.map +1 -1
- package/dist/types/babel-plugin-intlayer-usage-analyzer.d.ts +20 -187
- package/dist/types/babel-plugin-intlayer-usage-analyzer.d.ts.map +1 -1
- package/dist/types/compat/namespaceResolution.d.ts +66 -0
- package/dist/types/compat/namespaceResolution.d.ts.map +1 -0
- package/dist/types/compat/optimizePass.d.ts +58 -0
- package/dist/types/compat/optimizePass.d.ts.map +1 -0
- package/dist/types/compat/usageAnalysis.d.ts +75 -0
- package/dist/types/compat/usageAnalysis.d.ts.map +1 -0
- package/dist/types/dictionaryImports.d.ts +46 -0
- package/dist/types/dictionaryImports.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/nativeCallers.d.ts +33 -0
- package/dist/types/nativeCallers.d.ts.map +1 -0
- package/dist/types/pruneContext.d.ts +152 -0
- package/dist/types/pruneContext.d.ts.map +1 -0
- package/dist/types/staticAstReaders.d.ts +84 -0
- package/dist/types/staticAstReaders.d.ts.map +1 -0
- package/dist/types/transformers.d.ts +2 -1
- package/dist/types/transformers.d.ts.map +1 -1
- package/package.json +10 -10
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { recordFieldUsage } from "../pruneContext.mjs";
|
|
2
|
+
import { ABSENT_VALUE, firstPathSegment, readJsxAttributeString, readObjectProperty, readStaticFirstSegment, readStaticString, unwrapAwait } from "../staticAstReaders.mjs";
|
|
3
|
+
import { analyzeCallExpressionUsage } from "../analyzeContentUsage.mjs";
|
|
4
|
+
import { DEFAULT_COMPAT_NAMESPACE, resolveKeyPrefixForAnalysis, resolveNamespaceForAnalysis } from "./namespaceResolution.mjs";
|
|
5
|
+
|
|
6
|
+
//#region src/compat/usageAnalysis.ts
|
|
7
|
+
/**
|
|
8
|
+
* Analyses how the translation function produced by a compat namespace caller
|
|
9
|
+
* (`useTranslation`, `useTranslations`, `getTranslations`, `getFixedT`,
|
|
10
|
+
* `useI18n`) is consumed, then records the accessed top-level dictionary fields
|
|
11
|
+
* into `pruneContext`.
|
|
12
|
+
*
|
|
13
|
+
* Dictionaries consumed this way are always added to
|
|
14
|
+
* `dictionariesSkippingFieldRename`: the field accesses are string-literal
|
|
15
|
+
* dot-paths inside `t()` calls, which the field-rename plugin cannot rewrite,
|
|
16
|
+
* so renaming the compiled JSON keys would break runtime lookups. Pruning
|
|
17
|
+
* (top-level field removal) remains safe because it preserves field names.
|
|
18
|
+
*/
|
|
19
|
+
const analyzeNamespaceCallerUsage = (babelTypes, pruneContext, callArguments, callerConfig, isSfcFile, currentSourceFilePath, callExpressionPath, namespaceOverride) => {
|
|
20
|
+
const resolvedNamespace = namespaceOverride ?? resolveNamespaceForAnalysis(babelTypes, callArguments, callerConfig.namespaceSources);
|
|
21
|
+
if (resolvedNamespace === void 0) return;
|
|
22
|
+
const namespaceString = resolvedNamespace === "__default__" ? DEFAULT_COMPAT_NAMESPACE : resolvedNamespace;
|
|
23
|
+
const namespaceSegments = namespaceString.split(".");
|
|
24
|
+
const dictionaryKey = namespaceSegments[0] ?? namespaceString;
|
|
25
|
+
const namespacePrefix = namespaceSegments.length > 1 ? namespaceSegments.slice(1).join(".") : null;
|
|
26
|
+
pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);
|
|
27
|
+
if (isSfcFile) {
|
|
28
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (callerConfig.translationFunction === "all") {
|
|
32
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (callerConfig.translationFunction === "content") {
|
|
36
|
+
if (callExpressionPath) analyzeCallExpressionUsage(babelTypes, pruneContext, callExpressionPath, dictionaryKey, currentSourceFilePath, isSfcFile);
|
|
37
|
+
else recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (callerConfig.translationFunction === "self") {
|
|
41
|
+
const firstArg = callArguments[0];
|
|
42
|
+
if (!firstArg) {
|
|
43
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (callerConfig.namespaceSources.some((source) => source.from === "path-first-segment")) {
|
|
47
|
+
let fullId;
|
|
48
|
+
const staticString = readStaticString(babelTypes, firstArg);
|
|
49
|
+
if (staticString !== void 0) fullId = staticString;
|
|
50
|
+
else if (babelTypes.isObjectExpression(firstArg)) {
|
|
51
|
+
const idValue = readObjectProperty(babelTypes, firstArg, "id");
|
|
52
|
+
if (idValue !== void 0 && idValue !== "__default__") fullId = idValue;
|
|
53
|
+
}
|
|
54
|
+
if (fullId !== void 0) {
|
|
55
|
+
const field = fullId.split(".")[1];
|
|
56
|
+
if (field !== void 0) recordFieldUsage(pruneContext, dictionaryKey, /* @__PURE__ */ new Set([field]));
|
|
57
|
+
else recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const segment = callerConfig.flatKey ? readStaticString(babelTypes, firstArg) : readStaticFirstSegment(babelTypes, firstArg);
|
|
64
|
+
if (segment !== void 0) {
|
|
65
|
+
recordFieldUsage(pruneContext, dictionaryKey, /* @__PURE__ */ new Set([segment]));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (babelTypes.isObjectExpression(firstArg)) {
|
|
69
|
+
const idValue = readObjectProperty(babelTypes, firstArg, "id");
|
|
70
|
+
if (idValue !== void 0 && idValue !== "__default__") {
|
|
71
|
+
recordFieldUsage(pruneContext, dictionaryKey, /* @__PURE__ */ new Set([callerConfig.flatKey ? idValue : firstPathSegment(idValue)]));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!callExpressionPath) {
|
|
79
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const explicitKeyPrefix = resolveKeyPrefixForAnalysis(babelTypes, callArguments, callerConfig.keyPrefixSources);
|
|
83
|
+
if (explicitKeyPrefix === void 0) {
|
|
84
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const effectivePrefix = namespacePrefix ?? explicitKeyPrefix;
|
|
88
|
+
if (effectivePrefix !== null) {
|
|
89
|
+
recordFieldUsage(pruneContext, dictionaryKey, /* @__PURE__ */ new Set([firstPathSegment(effectivePrefix)]));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
let translationBinding = null;
|
|
93
|
+
if (callerConfig.translationFunction === "destructured-t") {
|
|
94
|
+
const parentNode = callExpressionPath.parent;
|
|
95
|
+
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id)) {
|
|
96
|
+
for (const property of parentNode.id.properties) if (babelTypes.isObjectProperty(property) && babelTypes.isIdentifier(property.key) && property.key.name === "t" && babelTypes.isIdentifier(property.value)) translationBinding = callExpressionPath.scope.getBinding(property.value.name) ?? null;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
const parentNode = unwrapAwait(babelTypes, callExpressionPath).parent;
|
|
100
|
+
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isIdentifier(parentNode.id)) translationBinding = callExpressionPath.scope.getBinding(parentNode.id.name) ?? null;
|
|
101
|
+
}
|
|
102
|
+
if (!translationBinding) {
|
|
103
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const accessedFields = /* @__PURE__ */ new Set();
|
|
107
|
+
let hasUntrackedUsage = false;
|
|
108
|
+
for (const referencePath of translationBinding.referencePaths) {
|
|
109
|
+
const parentNode = referencePath.parent;
|
|
110
|
+
if (!((babelTypes.isCallExpression(parentNode) || babelTypes.isOptionalCallExpression(parentNode)) && parentNode.callee === referencePath.node)) {
|
|
111
|
+
hasUntrackedUsage = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const firstArgument = parentNode.arguments[0];
|
|
115
|
+
const segment = readStaticFirstSegment(babelTypes, firstArgument);
|
|
116
|
+
if (segment === void 0) {
|
|
117
|
+
hasUntrackedUsage = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
accessedFields.add(segment);
|
|
121
|
+
}
|
|
122
|
+
if (hasUntrackedUsage) {
|
|
123
|
+
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (accessedFields.size > 0) recordFieldUsage(pruneContext, dictionaryKey, accessedFields);
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Builds the compat analyser for one source file, or `null` when no compat
|
|
130
|
+
* caller was injected — the base intlayer pipeline then runs untouched.
|
|
131
|
+
*
|
|
132
|
+
* @param babelTypes - Babel's type helpers.
|
|
133
|
+
* @param pruneContext - Shared analysis state to write into.
|
|
134
|
+
* @param compatCallers - Descriptors injected by the compat packages' plugins.
|
|
135
|
+
* @param fileContext - Path of the analysed file and whether it is an SFC.
|
|
136
|
+
*/
|
|
137
|
+
const createCompatUsageAnalyzer = (babelTypes, pruneContext, compatCallers, fileContext) => {
|
|
138
|
+
if (compatCallers.length === 0) return null;
|
|
139
|
+
const { sourceFilePath, isSfcFile } = fileContext;
|
|
140
|
+
/** Local alias → the descriptor it was imported as. */
|
|
141
|
+
const callersByLocalName = /* @__PURE__ */ new Map();
|
|
142
|
+
/** Module specifiers imported by the file, gating `requiresImport` callers. */
|
|
143
|
+
const fileImportSources = /* @__PURE__ */ new Set();
|
|
144
|
+
const analyze = (callArguments, descriptor, callExpressionPath, namespaceOverride) => analyzeNamespaceCallerUsage(babelTypes, pruneContext, callArguments, descriptor, isSfcFile, sourceFilePath, callExpressionPath, namespaceOverride);
|
|
145
|
+
/**
|
|
146
|
+
* Method-matched callers (`i18n.getFixedT(…)`, `intl.formatMessage(…)`) are
|
|
147
|
+
* recognised by method name on any object, so they are resolved lazily once
|
|
148
|
+
* every import has been seen.
|
|
149
|
+
*/
|
|
150
|
+
let methodCallers;
|
|
151
|
+
const getMethodCallers = () => {
|
|
152
|
+
methodCallers ??= compatCallers.filter((descriptor) => descriptor.matchAsMethod && (!descriptor.requiresImport || descriptor.importSources.some((source) => fileImportSources.has(source))));
|
|
153
|
+
return methodCallers;
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
noteImport: (importSource, importedName, localName) => {
|
|
157
|
+
fileImportSources.add(importSource);
|
|
158
|
+
const descriptor = compatCallers.find((caller) => caller.callerName === importedName && caller.importSources.includes(importSource));
|
|
159
|
+
if (descriptor) callersByLocalName.set(localName, descriptor);
|
|
160
|
+
},
|
|
161
|
+
hasMatchableCallers: () => callersByLocalName.size > 0 || getMethodCallers().length > 0,
|
|
162
|
+
analyzeCall: (callExpressionPath, localCallerName, isMethodCall) => {
|
|
163
|
+
const importedDescriptor = callersByLocalName.get(localCallerName);
|
|
164
|
+
if (importedDescriptor && !isMethodCall) {
|
|
165
|
+
analyze(callExpressionPath.node.arguments, importedDescriptor, callExpressionPath);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (!isMethodCall) return false;
|
|
169
|
+
const methodDescriptor = getMethodCallers().find((descriptor) => descriptor.callerName === localCallerName);
|
|
170
|
+
if (!methodDescriptor) return false;
|
|
171
|
+
analyze(callExpressionPath.node.arguments, methodDescriptor, callExpressionPath);
|
|
172
|
+
return true;
|
|
173
|
+
},
|
|
174
|
+
analyzeJsxElement: (jsxOpeningElementPath) => {
|
|
175
|
+
const nameNode = jsxOpeningElementPath.node.name;
|
|
176
|
+
if (!babelTypes.isJSXIdentifier(nameNode)) return;
|
|
177
|
+
const descriptor = callersByLocalName.get(nameNode.name);
|
|
178
|
+
if (!descriptor?.jsxIdAttribute) return;
|
|
179
|
+
const idNode = readJsxAttributeString(babelTypes, jsxOpeningElementPath.node, descriptor.jsxIdAttribute);
|
|
180
|
+
let namespaceOverride;
|
|
181
|
+
if (descriptor.jsxNamespaceAttribute) {
|
|
182
|
+
const namespaceNode = readJsxAttributeString(babelTypes, jsxOpeningElementPath.node, descriptor.jsxNamespaceAttribute);
|
|
183
|
+
if (!namespaceNode) return;
|
|
184
|
+
namespaceOverride = namespaceNode.value;
|
|
185
|
+
}
|
|
186
|
+
analyze(idNode ? [idNode] : [], descriptor, void 0, namespaceOverride);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
//#endregion
|
|
192
|
+
export { analyzeNamespaceCallerUsage, createCompatUsageAnalyzer };
|
|
193
|
+
//# sourceMappingURL=usageAnalysis.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usageAnalysis.mjs","names":[],"sources":["../../../src/compat/usageAnalysis.ts"],"sourcesContent":["import type { NodePath } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport type { CallerDescriptor } from '@intlayer/config/callers';\nimport { analyzeCallExpressionUsage } from '../analyzeContentUsage';\nimport { type PruneContext, recordFieldUsage } from '../pruneContext';\nimport {\n ABSENT_VALUE,\n firstPathSegment,\n readJsxAttributeString,\n readObjectProperty,\n readStaticFirstSegment,\n readStaticString,\n unwrapAwait,\n} from '../staticAstReaders';\nimport {\n DEFAULT_COMPAT_NAMESPACE,\n resolveKeyPrefixForAnalysis,\n resolveNamespaceForAnalysis,\n} from './namespaceResolution';\n\n/**\n * Analyses how the translation function produced by a compat namespace caller\n * (`useTranslation`, `useTranslations`, `getTranslations`, `getFixedT`,\n * `useI18n`) is consumed, then records the accessed top-level dictionary fields\n * into `pruneContext`.\n *\n * Dictionaries consumed this way are always added to\n * `dictionariesSkippingFieldRename`: the field accesses are string-literal\n * dot-paths inside `t()` calls, which the field-rename plugin cannot rewrite,\n * so renaming the compiled JSON keys would break runtime lookups. Pruning\n * (top-level field removal) remains safe because it preserves field names.\n */\nexport const analyzeNamespaceCallerUsage = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n callArguments: BabelTypes.CallExpression['arguments'],\n callerConfig: CallerDescriptor,\n isSfcFile: boolean,\n /** Absolute path of the analysed file (for untracked-binding reporting). */\n currentSourceFilePath: string,\n /**\n * The call-expression path, when the caller was matched as a call. Omitted\n * for JSX-element matches (`<FormattedMessage id>`), where only the static\n * `'self'` / `'all'` analysis paths apply — the binding-based\n * `'destructured-t'` / `'return-value'` paths require a call site.\n */\n callExpressionPath?: NodePath<BabelTypes.CallExpression>,\n /**\n * Pre-resolved namespace bypassing `namespaceSources`, e.g. the `ns`\n * attribute of react-i18next's `<Trans ns=\"home\" i18nKey=\"title\" />`.\n */\n namespaceOverride?: string\n): void => {\n // 1. Resolve the dictionary key (namespace).\n const resolvedNamespace =\n namespaceOverride ??\n resolveNamespaceForAnalysis(\n babelTypes,\n callArguments,\n callerConfig.namespaceSources\n );\n\n if (resolvedNamespace === undefined) return; // dynamic key – cannot attribute\n\n const namespaceString =\n resolvedNamespace === ABSENT_VALUE\n ? DEFAULT_COMPAT_NAMESPACE\n : resolvedNamespace;\n\n // next-intl scopes nested objects through a dotted namespace\n // (`'about.counter'`): the dictionary key is the first segment and the\n // remainder is an implicit key prefix applied to every t() lookup.\n const namespaceSegments = namespaceString.split('.');\n const dictionaryKey = namespaceSegments[0] ?? namespaceString;\n const namespacePrefix =\n namespaceSegments.length > 1 ? namespaceSegments.slice(1).join('.') : null;\n\n // Compat string-path access is never renamable.\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n // 2. SFC files (Vue / Svelte / Astro): the translation function is typically\n // invoked from the template, which Babel cannot see. Conservatively keep\n // every field to avoid pruning a template-only access.\n if (isSfcFile) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3a. Mark the entire dictionary as used — static field analysis is not\n // applicable (e.g. lingui hashed IDs, Angular templates).\n if (callerConfig.translationFunction === 'all') {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3a-bis. The caller returns the dictionary content object itself\n // (`useIntlayer`-like shape): reuse the native member-access /\n // destructuring analysis. Without a call site, keep every field.\n if (callerConfig.translationFunction === 'content') {\n if (callExpressionPath) {\n analyzeCallExpressionUsage(\n babelTypes,\n pruneContext,\n callExpressionPath,\n dictionaryKey,\n currentSourceFilePath,\n isSfcFile\n );\n } else {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n }\n return;\n }\n\n // 3b. The caller IS the translation call (`translationFunction: 'self'`).\n // The first argument of the current call expression is the message key.\n // Used for lingui's `i18n._('key')` / `i18n.t('key')` and react-intl's\n // `intl.formatMessage({ id: 'key' })` patterns.\n if (callerConfig.translationFunction === 'self') {\n const firstArg = callArguments[0];\n if (!firstArg) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n if (\n callerConfig.namespaceSources.some(\n (source) => source.from === 'path-first-segment'\n )\n ) {\n // The dictionary key is already the first segment of the descriptor id.\n // The field to record is the SECOND segment (first level inside the dict).\n // e.g. formatMessage({ id: 'home.title' }) → dictionaryKey='home', field='title'\n let fullId: string | undefined;\n\n const staticString = readStaticString(babelTypes, firstArg);\n if (staticString !== undefined) {\n fullId = staticString;\n } else if (babelTypes.isObjectExpression(firstArg)) {\n const idValue = readObjectProperty(babelTypes, firstArg, 'id');\n if (idValue !== undefined && idValue !== ABSENT_VALUE) {\n fullId = idValue;\n }\n }\n\n if (fullId !== undefined) {\n const segments = fullId.split('.');\n const field = segments[1];\n if (field !== undefined) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([field]));\n } else {\n // Single-segment id — the whole value is the dict key; no sub-field.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n }\n return;\n }\n\n // Dynamic id — cannot prune.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // For 'fixed' / 'argument' / 'option' namespaces: the field is the first\n // dot-segment of the first argument (the message key itself) — unless\n // `flatKey` is set, in which case the whole dotted key is the field.\n // String form: i18n._('home.title', values)\n const segment = callerConfig.flatKey\n ? readStaticString(babelTypes, firstArg)\n : readStaticFirstSegment(babelTypes, firstArg);\n if (segment !== undefined) {\n recordFieldUsage(pruneContext, dictionaryKey, new Set([segment]));\n return;\n }\n\n // Descriptor form: i18n._({ id: 'home.title', message: '...' }, values)\n if (babelTypes.isObjectExpression(firstArg)) {\n const idValue = readObjectProperty(babelTypes, firstArg, 'id');\n if (idValue !== undefined && idValue !== ABSENT_VALUE) {\n recordFieldUsage(\n pruneContext,\n dictionaryKey,\n new Set([callerConfig.flatKey ? idValue : firstPathSegment(idValue)])\n );\n return;\n }\n }\n\n // Dynamic key — cannot prune.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // The remaining strategies (`'destructured-t'` / `'return-value'`) resolve\n // the `t` binding from the call site, so they require a call-expression path.\n // JSX-element matches never reach here (they use `'self'` / `'all'`); guard\n // defensively so a misconfigured JSX caller keeps every field instead of\n // throwing.\n if (!callExpressionPath) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 3. Resolve an optional explicit keyPrefix (e.g. react-i18next's\n // `{ keyPrefix }` option). A static prefix fixes the single consumed\n // top-level field regardless of the individual t() paths.\n const explicitKeyPrefix = resolveKeyPrefixForAnalysis(\n babelTypes,\n callArguments,\n callerConfig.keyPrefixSources\n );\n if (explicitKeyPrefix === undefined) {\n // Prefix present but dynamic → unknown field set.\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // The namespace-derived prefix (next-intl) and the explicit keyPrefix option\n // (react-i18next / i18next) never coexist in practice; prefer whichever is\n // present. Either way, the consumed top-level field is the prefix's first\n // segment.\n const effectivePrefix = namespacePrefix ?? explicitKeyPrefix;\n if (effectivePrefix !== null) {\n recordFieldUsage(\n pruneContext,\n dictionaryKey,\n new Set([firstPathSegment(effectivePrefix)])\n );\n return;\n }\n\n // 4. Locate the `t` function binding.\n let translationBinding: ReturnType<NodePath['scope']['getBinding']> | null =\n null;\n\n if (callerConfig.translationFunction === 'destructured-t') {\n // const { t } = useTranslation('ns')\n const parentNode = callExpressionPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isObjectPattern(parentNode.id)\n ) {\n for (const property of parentNode.id.properties) {\n if (\n babelTypes.isObjectProperty(property) &&\n babelTypes.isIdentifier(property.key) &&\n property.key.name === 't' &&\n babelTypes.isIdentifier(property.value)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(property.value.name) ?? null;\n }\n }\n }\n } else {\n // const t = useTranslations('ns') / const t = await getTranslations('ns')\n const resultPath = unwrapAwait(babelTypes, callExpressionPath);\n const parentNode = resultPath.parent;\n if (\n babelTypes.isVariableDeclarator(parentNode) &&\n babelTypes.isIdentifier(parentNode.id)\n ) {\n translationBinding =\n callExpressionPath.scope.getBinding(parentNode.id.name) ?? null;\n }\n }\n\n // Could not statically locate `t` (e.g. result stored whole, re-exported) →\n // conservatively keep all fields.\n if (!translationBinding) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // 5. Inspect every reference to `t`.\n const accessedFields = new Set<string>();\n let hasUntrackedUsage = false;\n\n for (const referencePath of translationBinding.referencePaths) {\n const parentNode = referencePath.parent;\n\n // Must be a direct call: t('path')\n const isDirectCall =\n (babelTypes.isCallExpression(parentNode) ||\n babelTypes.isOptionalCallExpression(parentNode)) &&\n (parentNode as BabelTypes.CallExpression).callee === referencePath.node;\n\n if (!isDirectCall) {\n // t passed as a prop / argument / reassigned → fields unknown.\n hasUntrackedUsage = true;\n break;\n }\n\n const firstArgument = (parentNode as BabelTypes.CallExpression)\n .arguments[0];\n const segment = readStaticFirstSegment(babelTypes, firstArgument);\n if (segment === undefined) {\n hasUntrackedUsage = true;\n break;\n }\n accessedFields.add(segment);\n }\n\n if (hasUntrackedUsage) {\n recordFieldUsage(pruneContext, dictionaryKey, 'all');\n return;\n }\n\n // Only record a finite field set when at least one field was actually\n // accessed. Recording an empty set would prune every field even though the\n // dictionary may be consumed elsewhere.\n if (accessedFields.size > 0) {\n recordFieldUsage(pruneContext, dictionaryKey, accessedFields);\n }\n};\n\n/**\n * Compat half of the usage analyser, kept behind one object so the native\n * plugin body contains no adapter-specific branching.\n *\n * The analyser is created only when compat callers were injected, so a plain\n * intlayer build never allocates it and never pays for its lookups —\n * {@link createCompatUsageAnalyzer} returns `null` for an empty registry.\n */\nexport type CompatUsageAnalyzer = {\n /**\n * Records the local aliases one import declaration introduces. Called for\n * every import of the file, before any call site is inspected.\n */\n noteImport: (\n importSource: string,\n importedName: string,\n localName: string\n ) => void;\n /**\n * Whether any compat caller can match in this file — either an imported\n * alias was recorded, or a method-matched caller is active. Checked after\n * every import was noted, so the plugin can skip the call traversal.\n */\n hasMatchableCallers: () => boolean;\n /**\n * Analyses one call site. Returns `true` when the call belonged to a compat\n * caller and was handled, `false` when the native path should look at it.\n */\n analyzeCall: (\n callExpressionPath: NodePath<BabelTypes.CallExpression>,\n localCallerName: string,\n isMethodCall: boolean\n ) => boolean;\n /** Analyses one JSX element, e.g. `<Trans i18nKey=\"…\" />`. */\n analyzeJsxElement: (\n jsxOpeningElementPath: NodePath<BabelTypes.JSXOpeningElement>\n ) => void;\n};\n\n/**\n * Builds the compat analyser for one source file, or `null` when no compat\n * caller was injected — the base intlayer pipeline then runs untouched.\n *\n * @param babelTypes - Babel's type helpers.\n * @param pruneContext - Shared analysis state to write into.\n * @param compatCallers - Descriptors injected by the compat packages' plugins.\n * @param fileContext - Path of the analysed file and whether it is an SFC.\n */\nexport const createCompatUsageAnalyzer = (\n babelTypes: typeof BabelTypes,\n pruneContext: PruneContext,\n compatCallers: readonly CallerDescriptor[],\n fileContext: { sourceFilePath: string; isSfcFile: boolean }\n): CompatUsageAnalyzer | null => {\n if (compatCallers.length === 0) return null;\n\n const { sourceFilePath, isSfcFile } = fileContext;\n\n /** Local alias → the descriptor it was imported as. */\n const callersByLocalName = new Map<string, CallerDescriptor>();\n /** Module specifiers imported by the file, gating `requiresImport` callers. */\n const fileImportSources = new Set<string>();\n\n const analyze = (\n callArguments: BabelTypes.CallExpression['arguments'],\n descriptor: CallerDescriptor,\n callExpressionPath?: NodePath<BabelTypes.CallExpression>,\n namespaceOverride?: string\n ) =>\n analyzeNamespaceCallerUsage(\n babelTypes,\n pruneContext,\n callArguments,\n descriptor,\n isSfcFile,\n sourceFilePath,\n callExpressionPath,\n namespaceOverride\n );\n\n /**\n * Method-matched callers (`i18n.getFixedT(…)`, `intl.formatMessage(…)`) are\n * recognised by method name on any object, so they are resolved lazily once\n * every import has been seen.\n */\n let methodCallers: CallerDescriptor[] | undefined;\n const getMethodCallers = (): CallerDescriptor[] => {\n methodCallers ??= compatCallers.filter(\n (descriptor) =>\n descriptor.matchAsMethod &&\n (!descriptor.requiresImport ||\n descriptor.importSources.some((source) =>\n fileImportSources.has(source)\n ))\n );\n return methodCallers;\n };\n\n return {\n noteImport: (importSource, importedName, localName) => {\n fileImportSources.add(importSource);\n\n const descriptor = compatCallers.find(\n (caller) =>\n caller.callerName === importedName &&\n caller.importSources.includes(importSource)\n );\n if (descriptor) callersByLocalName.set(localName, descriptor);\n },\n\n hasMatchableCallers: () =>\n callersByLocalName.size > 0 || getMethodCallers().length > 0,\n\n analyzeCall: (callExpressionPath, localCallerName, isMethodCall) => {\n const importedDescriptor = callersByLocalName.get(localCallerName);\n\n if (importedDescriptor && !isMethodCall) {\n analyze(\n callExpressionPath.node.arguments,\n importedDescriptor,\n callExpressionPath\n );\n return true;\n }\n\n if (!isMethodCall) return false;\n\n const methodDescriptor = getMethodCallers().find(\n (descriptor) => descriptor.callerName === localCallerName\n );\n if (!methodDescriptor) return false;\n\n analyze(\n callExpressionPath.node.arguments,\n methodDescriptor,\n callExpressionPath\n );\n return true;\n },\n\n analyzeJsxElement: (jsxOpeningElementPath) => {\n const nameNode = jsxOpeningElementPath.node.name;\n if (!babelTypes.isJSXIdentifier(nameNode)) return;\n\n const descriptor = callersByLocalName.get(nameNode.name);\n if (!descriptor?.jsxIdAttribute) return;\n\n // Read the configured id attribute as a static string. A missing or\n // dynamic id yields an empty argument list, which resolves the namespace\n // to undefined and is skipped (consistent with how a dynamic\n // `useIntlayer(key)` call is handled).\n const idNode = readJsxAttributeString(\n babelTypes,\n jsxOpeningElementPath.node,\n descriptor.jsxIdAttribute\n );\n\n // Optional dedicated namespace attribute, e.g. react-i18next's\n // `<Trans ns=\"home\" i18nKey=\"title\" />`. When configured but\n // absent/dynamic, the dictionary cannot be attributed → skip.\n let namespaceOverride: string | undefined;\n if (descriptor.jsxNamespaceAttribute) {\n const namespaceNode = readJsxAttributeString(\n babelTypes,\n jsxOpeningElementPath.node,\n descriptor.jsxNamespaceAttribute\n );\n if (!namespaceNode) return;\n namespaceOverride = namespaceNode.value;\n }\n\n analyze(idNode ? [idNode] : [], descriptor, undefined, namespaceOverride);\n },\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getPathHash } from "@intlayer/engine/utils";
|
|
2
|
+
|
|
3
|
+
//#region src/dictionaryImports.ts
|
|
4
|
+
/** Identifier suffix that also tells the injection step which directory to read. */
|
|
5
|
+
const IDENT_SUFFIX = {
|
|
6
|
+
static: "",
|
|
7
|
+
dynamic: "_dyn",
|
|
8
|
+
fetch: "_fetch"
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Builds the generated identifier for a dictionary key.
|
|
12
|
+
*
|
|
13
|
+
* Replicates the xxHash64 → Base-62 algorithm used by the SWC version and
|
|
14
|
+
* prefixes an underscore so the generated identifiers never collide with
|
|
15
|
+
* user-defined ones.
|
|
16
|
+
*/
|
|
17
|
+
const makeDictionaryIdent = (babelTypes, key, mode) => babelTypes.identifier(`_${getPathHash(key)}${IDENT_SUFFIX[mode]}`);
|
|
18
|
+
/** Creates an empty registry for one source file. */
|
|
19
|
+
const createDictionaryImportRegistry = (babelTypes) => {
|
|
20
|
+
const staticImports = /* @__PURE__ */ new Map();
|
|
21
|
+
const dynamicImports = /* @__PURE__ */ new Map();
|
|
22
|
+
return {
|
|
23
|
+
staticImports,
|
|
24
|
+
dynamicImports,
|
|
25
|
+
identFor: (key, mode) => {
|
|
26
|
+
const map = mode === "static" ? staticImports : dynamicImports;
|
|
27
|
+
const existing = map.get(key);
|
|
28
|
+
if (existing) return existing;
|
|
29
|
+
const ident = makeDictionaryIdent(babelTypes, key, mode);
|
|
30
|
+
map.set(key, ident);
|
|
31
|
+
return ident;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { createDictionaryImportRegistry, makeDictionaryIdent };
|
|
38
|
+
//# sourceMappingURL=dictionaryImports.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dictionaryImports.mjs","names":[],"sources":["../../src/dictionaryImports.ts"],"sourcesContent":["import type * as BabelTypes from '@babel/types';\nimport { getPathHash } from '@intlayer/engine/utils';\n\n/**\n * How a dictionary reaches a call site.\n *\n * - `static` — the compiled JSON is imported directly.\n * - `dynamic` — a generated per-locale loader is imported and resolved at\n * runtime (Suspense).\n * - `fetch` — same as `dynamic`, but the loader fetches remote content.\n */\nexport type ImportMode = 'static' | 'dynamic' | 'fetch';\n\n/** Identifier suffix that also tells the injection step which directory to read. */\nconst IDENT_SUFFIX: Record<ImportMode, string> = {\n static: '',\n dynamic: '_dyn',\n fetch: '_fetch',\n};\n\n/**\n * Builds the generated identifier for a dictionary key.\n *\n * Replicates the xxHash64 → Base-62 algorithm used by the SWC version and\n * prefixes an underscore so the generated identifiers never collide with\n * user-defined ones.\n */\nexport const makeDictionaryIdent = (\n babelTypes: typeof BabelTypes,\n key: string,\n mode: ImportMode\n): BabelTypes.Identifier =>\n babelTypes.identifier(`_${getPathHash(key)}${IDENT_SUFFIX[mode]}`);\n\n/**\n * The dictionary imports one file needs, accumulated while its call sites are\n * rewritten and injected in one pass at the end.\n *\n * Both the native and the compat rewrites write here, so a dictionary read\n * through `useIntlayer('about')` and through `useTranslation('about')` in the\n * same file shares a single import.\n *\n * `dynamic` and `fetch` share one map because they occupy the same import\n * slot, distinguished only by their identifier suffix — mirroring\n * `InjectedImports` in the SWC plugin.\n */\nexport type DictionaryImportRegistry = {\n /** Dictionary key → identifier of the static JSON (or nested companion) import. */\n readonly staticImports: Map<string, BabelTypes.Identifier>;\n /** Dictionary key → identifier of the dynamic / fetch loader import. */\n readonly dynamicImports: Map<string, BabelTypes.Identifier>;\n /**\n * Returns the identifier for `key` in `mode`, creating and registering it on\n * first use.\n */\n identFor: (key: string, mode: ImportMode) => BabelTypes.Identifier;\n};\n\n/** Creates an empty registry for one source file. */\nexport const createDictionaryImportRegistry = (\n babelTypes: typeof BabelTypes\n): DictionaryImportRegistry => {\n const staticImports = new Map<string, BabelTypes.Identifier>();\n const dynamicImports = new Map<string, BabelTypes.Identifier>();\n\n return {\n staticImports,\n dynamicImports,\n identFor: (key, mode) => {\n const map = mode === 'static' ? staticImports : dynamicImports;\n\n const existing = map.get(key);\n if (existing) return existing;\n\n const ident = makeDictionaryIdent(babelTypes, key, mode);\n map.set(key, ident);\n return ident;\n },\n };\n};\n"],"mappings":";;;;AAcA,MAAM,eAA2C;CAC/C,QAAQ;CACR,SAAS;CACT,OAAO;AACT;;;;;;;;AASA,MAAa,uBACX,YACA,KACA,SAEA,WAAW,WAAW,IAAI,YAAY,GAAG,IAAI,aAAa,OAAO;;AA2BnE,MAAa,kCACX,eAC6B;CAC7B,MAAM,gCAAgB,IAAI,IAAmC;CAC7D,MAAM,iCAAiB,IAAI,IAAmC;CAE9D,OAAO;EACL;EACA;EACA,WAAW,KAAK,SAAS;GACvB,MAAM,MAAM,SAAS,WAAW,gBAAgB;GAEhD,MAAM,WAAW,IAAI,IAAI,GAAG;GAC5B,IAAI,UAAU,OAAO;GAErB,MAAM,QAAQ,oBAAoB,YAAY,KAAK,IAAI;GACvD,IAAI,IAAI,KAAK,KAAK;GAClB,OAAO;EACT;CACF;AACF"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { extractDictionaryKey } from "./extractDictionaryKey.mjs";
|
|
2
2
|
import { basename, dirname, extname, relative, resolve } from "node:path";
|
|
3
|
+
import { getFormatFromExtension, resolveRelativePath } from "@intlayer/engine/utils";
|
|
3
4
|
import * as ANSIColors from "@intlayer/config/colors";
|
|
4
5
|
import { colorize } from "@intlayer/config/logger";
|
|
5
|
-
import { getFormatFromExtension, resolveRelativePath } from "@intlayer/engine/utils";
|
|
6
6
|
import { assertPathWithin, parseStringPattern } from "@intlayer/config/utils";
|
|
7
7
|
import { getUnmergedDictionaries } from "@intlayer/unmerged-dictionaries-entry";
|
|
8
8
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { writeContentHelper } from "./extractContent/contentWriter.mjs";
|
|
2
|
+
import { buildComponentFilesList } from "@intlayer/engine/utils";
|
|
2
3
|
import * as ANSIColors from "@intlayer/config/colors";
|
|
3
4
|
import { COMPILER_ENABLED } from "@intlayer/config/defaultValues";
|
|
4
5
|
import { colorize, colorizeKey, getAppLogger } from "@intlayer/config/logger";
|
|
5
6
|
import { getConfiguration } from "@intlayer/config/node";
|
|
6
|
-
import { buildComponentFilesList } from "@intlayer/engine/utils";
|
|
7
7
|
|
|
8
8
|
//#region src/getExtractPluginOptions.ts
|
|
9
9
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { __require } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { getConfiguration } from "@intlayer/config/node";
|
|
4
3
|
import { buildComponentFilesList } from "@intlayer/engine/utils";
|
|
4
|
+
import { getConfiguration } from "@intlayer/config/node";
|
|
5
5
|
import { getNestedDictionaryGraph } from "@intlayer/core/dictionaryManipulator";
|
|
6
6
|
|
|
7
7
|
//#region src/getOptimizePluginOptions.ts
|
|
@@ -32,11 +32,7 @@ const getOptimizePluginOptions = (params) => {
|
|
|
32
32
|
const unmergedDictionariesEntryPath = join(mainDir, "unmerged_dictionaries.mjs");
|
|
33
33
|
const dynamicDictionariesEntryPath = join(mainDir, "dynamic_dictionaries.mjs");
|
|
34
34
|
const fetchDictionariesEntryPath = join(mainDir, "fetch_dictionaries.mjs");
|
|
35
|
-
const filesList = [
|
|
36
|
-
...buildComponentFilesList(config),
|
|
37
|
-
dictionariesEntryPath,
|
|
38
|
-
unmergedDictionariesEntryPath
|
|
39
|
-
];
|
|
35
|
+
const filesList = [...buildComponentFilesList(config), dictionariesEntryPath];
|
|
40
36
|
const dictionaries = providedDictionaries ?? loadDictionaries(config);
|
|
41
37
|
const dictionaryModeMap = {};
|
|
42
38
|
dictionaries.forEach((dictionary) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getOptimizePluginOptions.mjs","names":[],"sources":["../../src/getOptimizePluginOptions.ts"],"sourcesContent":["import { join } from 'node:path';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport { getNestedDictionaryGraph } from '@intlayer/core/dictionaryManipulator';\nimport { buildComponentFilesList } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport type { OptimizePluginOptions } from './babel-plugin-intlayer-optimize';\n\ntype GetOptimizePluginOptionsParams = {\n /**\n * Configuration options for loading intlayer config\n */\n configOptions?: GetConfigurationOptions;\n /**\n * Pre-loaded dictionaries (optional - will be loaded if not provided)\n */\n dictionaries?: Dictionary[];\n /**\n * Whether the current transform is for an SSR bundle. When using Babel\n * directly (e.g. in `babel.config.js`), forward the bundler's server flag\n * here so SSR-specific static dictionary transforms apply.\n */\n isServer?: boolean;\n /**\n * Override specific options\n */\n overrides?: Partial<OptimizePluginOptions>;\n};\n\n/**\n * Load dictionaries from the dictionaries-entry package\n */\nconst loadDictionaries = (config: IntlayerConfig): Dictionary[] => {\n try {\n // Dynamic require to avoid build-time dependency issues\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { getDictionaries } = require('@intlayer/dictionaries-entry');\n const dictionariesRecord = getDictionaries(config);\n\n return Object.values(dictionariesRecord);\n } catch {\n // If dictionaries-entry is not available, return empty array\n return [];\n }\n};\n\n/**\n * Get the options for the Intlayer Babel optimization plugin\n * This function loads the Intlayer configuration and returns the paths\n * needed for dictionary optimization and import rewriting.\n */\nexport const getOptimizePluginOptions = (\n params?: GetOptimizePluginOptionsParams\n): OptimizePluginOptions => {\n const {\n configOptions,\n dictionaries: providedDictionaries,\n isServer,\n overrides,\n } = params ?? {};\n\n const config = getConfiguration(configOptions);\n const {\n mainDir,\n dictionariesDir,\n unmergedDictionariesDir,\n dynamicDictionariesDir,\n fetchDictionariesDir,\n } = config.system;\n const importMode = config.build.importMode ?? config.dictionary?.importMode;\n const optimize = config.build.optimize;\n\n const dictionariesEntryPath = join(mainDir, 'dictionaries.mjs');\n const unmergedDictionariesEntryPath = join(\n mainDir,\n 'unmerged_dictionaries.mjs'\n );\n const dynamicDictionariesEntryPath = join(\n mainDir,\n 'dynamic_dictionaries.mjs'\n );\n const fetchDictionariesEntryPath = join(mainDir, 'fetch_dictionaries.mjs');\n\n const filesListPattern = buildComponentFilesList(config);\n\n const filesList = [\n ...filesListPattern,\n dictionariesEntryPath, // should add dictionariesEntryPath to replace it by an empty object if import made dynamic\n
|
|
1
|
+
{"version":3,"file":"getOptimizePluginOptions.mjs","names":[],"sources":["../../src/getOptimizePluginOptions.ts"],"sourcesContent":["import { join } from 'node:path';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport { getNestedDictionaryGraph } from '@intlayer/core/dictionaryManipulator';\nimport { buildComponentFilesList } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport type { OptimizePluginOptions } from './babel-plugin-intlayer-optimize';\n\ntype GetOptimizePluginOptionsParams = {\n /**\n * Configuration options for loading intlayer config\n */\n configOptions?: GetConfigurationOptions;\n /**\n * Pre-loaded dictionaries (optional - will be loaded if not provided)\n */\n dictionaries?: Dictionary[];\n /**\n * Whether the current transform is for an SSR bundle. When using Babel\n * directly (e.g. in `babel.config.js`), forward the bundler's server flag\n * here so SSR-specific static dictionary transforms apply.\n */\n isServer?: boolean;\n /**\n * Override specific options\n */\n overrides?: Partial<OptimizePluginOptions>;\n};\n\n/**\n * Load dictionaries from the dictionaries-entry package\n */\nconst loadDictionaries = (config: IntlayerConfig): Dictionary[] => {\n try {\n // Dynamic require to avoid build-time dependency issues\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { getDictionaries } = require('@intlayer/dictionaries-entry');\n const dictionariesRecord = getDictionaries(config);\n\n return Object.values(dictionariesRecord);\n } catch {\n // If dictionaries-entry is not available, return empty array\n return [];\n }\n};\n\n/**\n * Get the options for the Intlayer Babel optimization plugin\n * This function loads the Intlayer configuration and returns the paths\n * needed for dictionary optimization and import rewriting.\n */\nexport const getOptimizePluginOptions = (\n params?: GetOptimizePluginOptionsParams\n): OptimizePluginOptions => {\n const {\n configOptions,\n dictionaries: providedDictionaries,\n isServer,\n overrides,\n } = params ?? {};\n\n const config = getConfiguration(configOptions);\n const {\n mainDir,\n dictionariesDir,\n unmergedDictionariesDir,\n dynamicDictionariesDir,\n fetchDictionariesDir,\n } = config.system;\n const importMode = config.build.importMode ?? config.dictionary?.importMode;\n const optimize = config.build.optimize;\n\n const dictionariesEntryPath = join(mainDir, 'dictionaries.mjs');\n const unmergedDictionariesEntryPath = join(\n mainDir,\n 'unmerged_dictionaries.mjs'\n );\n const dynamicDictionariesEntryPath = join(\n mainDir,\n 'dynamic_dictionaries.mjs'\n );\n const fetchDictionariesEntryPath = join(mainDir, 'fetch_dictionaries.mjs');\n\n const filesListPattern = buildComponentFilesList(config);\n\n const filesList = [\n ...filesListPattern,\n dictionariesEntryPath, // should add dictionariesEntryPath to replace it by an empty object if import made dynamic\n ];\n\n // Load dictionaries if not provided\n const dictionaries = providedDictionaries ?? loadDictionaries(config);\n\n const dictionaryModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch' | undefined\n > = {};\n\n dictionaries.forEach((dictionary) => {\n dictionaryModeMap[dictionary.key] = dictionary.importMode ?? importMode;\n });\n\n // Dictionaries holding `nest()` references: their static import is re-pointed\n // at the companion module carrying the nest targets.\n const nestingDictionaryKeys = [\n ...getNestedDictionaryGraph(dictionaries).keys(),\n ];\n\n return {\n optimize,\n dictionariesDir,\n dictionariesEntryPath,\n unmergedDictionariesDir,\n unmergedDictionariesEntryPath,\n dynamicDictionariesDir,\n dynamicDictionariesEntryPath,\n fetchDictionariesDir,\n fetchDictionariesEntryPath,\n replaceDictionaryEntry: true,\n nestingDictionaryKeys,\n importMode,\n dictionaryModeMap,\n filesList,\n isServer,\n ...overrides,\n };\n};\n"],"mappings":";;;;;;;;;;AAmCA,MAAM,oBAAoB,WAAyC;CACjE,IAAI;EAGF,MAAM,EAAE,8BAA4B,8BAA8B;EAClE,MAAM,qBAAqB,gBAAgB,MAAM;EAEjD,OAAO,OAAO,OAAO,kBAAkB;CACzC,QAAQ;EAEN,OAAO,CAAC;CACV;AACF;;;;;;AAOA,MAAa,4BACX,WAC0B;CAC1B,MAAM,EACJ,eACA,cAAc,sBACd,UACA,cACE,UAAU,CAAC;CAEf,MAAM,SAAS,iBAAiB,aAAa;CAC7C,MAAM,EACJ,SACA,iBACA,yBACA,wBACA,yBACE,OAAO;CACX,MAAM,aAAa,OAAO,MAAM,cAAc,OAAO,YAAY;CACjE,MAAM,WAAW,OAAO,MAAM;CAE9B,MAAM,wBAAwB,KAAK,SAAS,kBAAkB;CAC9D,MAAM,gCAAgC,KACpC,SACA,2BACF;CACA,MAAM,+BAA+B,KACnC,SACA,0BACF;CACA,MAAM,6BAA6B,KAAK,SAAS,wBAAwB;CAIzE,MAAM,YAAY,CAChB,GAHuB,wBAAwB,MAG7B,GAClB,qBACF;CAGA,MAAM,eAAe,wBAAwB,iBAAiB,MAAM;CAEpE,MAAM,oBAGF,CAAC;CAEL,aAAa,SAAS,eAAe;EACnC,kBAAkB,WAAW,OAAO,WAAW,cAAc;CAC/D,CAAC;CAQD,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,wBAAwB;EACxB,wBAdA,GAAG,yBAAyB,YAAY,CAAC,CAAC,KAAK,CAc3B;EACpB;EACA;EACA;EACA;EACA,GAAG;CACL;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { __require } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { buildComponentFilesList } from "@intlayer/engine/utils";
|
|
2
3
|
import { IMPORT_MODE } from "@intlayer/config/defaultValues";
|
|
3
4
|
import { getConfiguration } from "@intlayer/config/node";
|
|
4
|
-
import { buildComponentFilesList } from "@intlayer/engine/utils";
|
|
5
5
|
import { getNestedDictionaryReferences } from "@intlayer/core/dictionaryManipulator";
|
|
6
6
|
|
|
7
7
|
//#region src/getPurgePluginOptions.ts
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { CHAINABLE_RUNTIME_METHOD_NAMES,
|
|
1
|
+
import { CHAINABLE_RUNTIME_METHOD_NAMES, INTLAYER_CALLER_NAMES, NATIVE_CALLER_NAME_SET } from "./nativeCallers.mjs";
|
|
2
|
+
import { createPruneContext, preserveNestedDictionaryFields } from "./pruneContext.mjs";
|
|
3
|
+
import { unwrapAwait } from "./staticAstReaders.mjs";
|
|
4
|
+
import { DEFAULT_COMPAT_CALLERS, makeUsageAnalyzerBabelPlugin } from "./babel-plugin-intlayer-usage-analyzer.mjs";
|
|
2
5
|
import { extractScriptBlocks, injectScriptBlocks } from "./extractScriptBlocks.mjs";
|
|
3
6
|
import { extractDictionaryKey, extractDictionaryKeyFromPath } from "./extractContent/utils/extractDictionaryKey.mjs";
|
|
4
7
|
import { extractDictionaryInfo, getOutput, resolveContentFilePaths } from "./extractContent/utils/extractDictionaryInfo.mjs";
|
|
@@ -18,4 +21,4 @@ import { intlayerMinifyBabelPlugin } from "./babel-plugin-intlayer-minify.mjs";
|
|
|
18
21
|
import { getOptimizePluginOptions } from "./getOptimizePluginOptions.mjs";
|
|
19
22
|
import { getMinifyPluginOptions, getPurgePluginOptions } from "./getPurgePluginOptions.mjs";
|
|
20
23
|
|
|
21
|
-
export { BABEL_PARSER_OPTIONS, CHAINABLE_RUNTIME_METHOD_NAMES, DEFAULT_COMPAT_CALLERS, INTLAYER_CALLER_NAMES, INTLAYER_USAGE_REGEX, SERVER_CAPABLE_PACKAGES, SOURCE_FILE_REGEX, analyzeFieldUsageInFile, buildNestedRenameMapFromContent, buildUsageCheckRegex, createPruneContext, detectPackageName, extractContent, extractContentSync, extractDictionaryInfo, extractDictionaryKey, extractDictionaryKeyFromPath, extractScriptBlocks, generateKey, generateShortFieldName, getComponentName, getExtractPluginOptions, getMinifyPluginOptions, getNestedRenameEntryAtPath, getOptimizePluginOptions, getOutput, getPurgePluginOptions, getSharedPruneContext, getUsageCheckRegex, injectScriptBlocks, intlayerExtractBabelPlugin, intlayerMinifyBabelPlugin, intlayerOptimizeBabelPlugin, intlayerPurgeBabelPlugin, makeFieldRenameBabelPlugin, makeUsageAnalyzerBabelPlugin, mergeWithExistingMultilingualDictionary, mergeWithExistingPerLocaleDictionary, optimizeSourceFile, packageList, preserveNestedDictionaryFields, renameFieldsInCode, renameFieldsInSourceFile, renameIntlayerFieldAccesses, resolveContentFilePaths, runIntlayerPurgePipeline, serializeFieldRenameMap, unwrapAwait, writeContentHelper };
|
|
24
|
+
export { BABEL_PARSER_OPTIONS, CHAINABLE_RUNTIME_METHOD_NAMES, DEFAULT_COMPAT_CALLERS, INTLAYER_CALLER_NAMES, INTLAYER_USAGE_REGEX, NATIVE_CALLER_NAME_SET, SERVER_CAPABLE_PACKAGES, SOURCE_FILE_REGEX, analyzeFieldUsageInFile, buildNestedRenameMapFromContent, buildUsageCheckRegex, createPruneContext, detectPackageName, extractContent, extractContentSync, extractDictionaryInfo, extractDictionaryKey, extractDictionaryKeyFromPath, extractScriptBlocks, generateKey, generateShortFieldName, getComponentName, getExtractPluginOptions, getMinifyPluginOptions, getNestedRenameEntryAtPath, getOptimizePluginOptions, getOutput, getPurgePluginOptions, getSharedPruneContext, getUsageCheckRegex, injectScriptBlocks, intlayerExtractBabelPlugin, intlayerMinifyBabelPlugin, intlayerOptimizeBabelPlugin, intlayerPurgeBabelPlugin, makeFieldRenameBabelPlugin, makeUsageAnalyzerBabelPlugin, mergeWithExistingMultilingualDictionary, mergeWithExistingPerLocaleDictionary, optimizeSourceFile, packageList, preserveNestedDictionaryFields, renameFieldsInCode, renameFieldsInSourceFile, renameIntlayerFieldAccesses, resolveContentFilePaths, runIntlayerPurgePipeline, serializeFieldRenameMap, unwrapAwait, writeContentHelper };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/nativeCallers.ts
|
|
2
|
+
/**
|
|
3
|
+
* The intlayer getters the build pipeline recognises without any adapter, and
|
|
4
|
+
* the runtime methods their result carries.
|
|
5
|
+
*
|
|
6
|
+
* Kept apart from the compat registry (`@intlayer/config/callers`) so the
|
|
7
|
+
* native passes never have to reason about adapter descriptors.
|
|
8
|
+
*/
|
|
9
|
+
/** Canonical intlayer caller names that trigger usage analysis. */
|
|
10
|
+
const INTLAYER_CALLER_NAMES = [
|
|
11
|
+
"useIntlayer",
|
|
12
|
+
"getIntlayer",
|
|
13
|
+
"getIntlayerAsync"
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* {@link INTLAYER_CALLER_NAMES} as a set — the import scan tests every
|
|
17
|
+
* specifier of every file against it, so membership must not be a linear scan.
|
|
18
|
+
*/
|
|
19
|
+
const NATIVE_CALLER_NAME_SET = new Set(INTLAYER_CALLER_NAMES);
|
|
20
|
+
/**
|
|
21
|
+
* Methods the runtime attaches to a resolved content object rather than fields
|
|
22
|
+
* declared in a `.content` file.
|
|
23
|
+
*
|
|
24
|
+
* `vanilla-intlayer`'s `useIntlayer` / `useDictionary` assign `onChange` onto
|
|
25
|
+
* the content they return, so `useIntlayer('app').onChange(cb)` is a
|
|
26
|
+
* subscription — not a read of a field named `onChange`. Without this list the
|
|
27
|
+
* analyser records `onChange` as the only consumed field and the purge pass
|
|
28
|
+
* strips every real field from the compiled dictionary.
|
|
29
|
+
*
|
|
30
|
+
* The analyser looks *through* these calls: the callback parameter is analysed
|
|
31
|
+
* as a fresh content root, and the call result is analysed again because the
|
|
32
|
+
* helpers return the content object for chaining.
|
|
33
|
+
*/
|
|
34
|
+
const CHAINABLE_RUNTIME_METHOD_NAMES = /* @__PURE__ */ new Set(["onChange"]);
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { CHAINABLE_RUNTIME_METHOD_NAMES, INTLAYER_CALLER_NAMES, NATIVE_CALLER_NAME_SET };
|
|
38
|
+
//# sourceMappingURL=nativeCallers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nativeCallers.mjs","names":[],"sources":["../../src/nativeCallers.ts"],"sourcesContent":["/**\n * The intlayer getters the build pipeline recognises without any adapter, and\n * the runtime methods their result carries.\n *\n * Kept apart from the compat registry (`@intlayer/config/callers`) so the\n * native passes never have to reason about adapter descriptors.\n */\n\n/** Canonical intlayer caller names that trigger usage analysis. */\nexport const INTLAYER_CALLER_NAMES = [\n 'useIntlayer',\n 'getIntlayer',\n 'getIntlayerAsync',\n] as const;\n\nexport type IntlayerCallerName = (typeof INTLAYER_CALLER_NAMES)[number];\n\n/**\n * {@link INTLAYER_CALLER_NAMES} as a set — the import scan tests every\n * specifier of every file against it, so membership must not be a linear scan.\n */\nexport const NATIVE_CALLER_NAME_SET: ReadonlySet<string> = new Set(\n INTLAYER_CALLER_NAMES\n);\n\n/**\n * Methods the runtime attaches to a resolved content object rather than fields\n * declared in a `.content` file.\n *\n * `vanilla-intlayer`'s `useIntlayer` / `useDictionary` assign `onChange` onto\n * the content they return, so `useIntlayer('app').onChange(cb)` is a\n * subscription — not a read of a field named `onChange`. Without this list the\n * analyser records `onChange` as the only consumed field and the purge pass\n * strips every real field from the compiled dictionary.\n *\n * The analyser looks *through* these calls: the callback parameter is analysed\n * as a fresh content root, and the call result is analysed again because the\n * helpers return the content object for chaining.\n */\nexport const CHAINABLE_RUNTIME_METHOD_NAMES = new Set(['onChange']);\n"],"mappings":";;;;;;;;;AASA,MAAa,wBAAwB;CACnC;CACA;CACA;AACF;;;;;AAQA,MAAa,yBAA8C,IAAI,IAC7D,qBACF;;;;;;;;;;;;;;;AAgBA,MAAa,iDAAiC,IAAI,IAAI,CAAC,UAAU,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/pruneContext.ts
|
|
2
|
+
const createPruneContext = () => ({
|
|
3
|
+
dictionaryKeyToFieldUsageMap: /* @__PURE__ */ new Map(),
|
|
4
|
+
dictionariesWithEdgeCases: /* @__PURE__ */ new Set(),
|
|
5
|
+
hasUnparsableSourceFiles: false,
|
|
6
|
+
dictionaryKeysWithUntrackedBindings: /* @__PURE__ */ new Map(),
|
|
7
|
+
dictionaryKeyToFieldRenameMap: /* @__PURE__ */ new Map(),
|
|
8
|
+
dictionaryKeysWithOpaqueFields: /* @__PURE__ */ new Map(),
|
|
9
|
+
dictionariesSkippingFieldRename: /* @__PURE__ */ new Set(),
|
|
10
|
+
pendingFrameworkAnalysis: /* @__PURE__ */ new Map()
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Records `nest()` references in a prune context so nest targets stay
|
|
14
|
+
* resolvable — and no larger than they need to be — after purge and minify.
|
|
15
|
+
*
|
|
16
|
+
* A nested node stores the ORIGINAL dot path of the field it reads
|
|
17
|
+
* (`nest('common', 'period')` → `'period'`) and `getNesting` resolves it at
|
|
18
|
+
* runtime against the compiled JSON. That drives three adjustments:
|
|
19
|
+
*
|
|
20
|
+
* - Nest targets never take part in field renaming, since the stored path
|
|
21
|
+
* would no longer match the renamed keys.
|
|
22
|
+
* - A target already consumed by a call site keeps its analysed fields plus
|
|
23
|
+
* the nested ones, so neither consumer loses content.
|
|
24
|
+
* - A target reached *only* through `nest()` is given a usage entry holding
|
|
25
|
+
* exactly the nested fields. Without one, the purge pass leaves the whole
|
|
26
|
+
* dictionary untouched; with one, it trims the dictionary down to what the
|
|
27
|
+
* nest references actually read. This matters most for `dynamic` / `fetch`
|
|
28
|
+
* dictionaries, whose full static snapshot would otherwise be pulled into
|
|
29
|
+
* the eager bundle just to satisfy a single nested field.
|
|
30
|
+
*
|
|
31
|
+
* Seeding is skipped when a source file failed to parse: that file could
|
|
32
|
+
* reference the dictionary in a way the analysis never saw, which is the same
|
|
33
|
+
* guard the purge pass applies to keys with no usage entry.
|
|
34
|
+
*
|
|
35
|
+
* @param pruneContext - Shared analysis state to update in place.
|
|
36
|
+
* @param nestedDictionaryReferences - Nest-target key → referenced top-level
|
|
37
|
+
* fields, as returned by `getNestedDictionaryReferences`.
|
|
38
|
+
*/
|
|
39
|
+
const preserveNestedDictionaryFields = (pruneContext, nestedDictionaryReferences) => {
|
|
40
|
+
for (const [dictionaryKey, nestedFields] of nestedDictionaryReferences.entries()) {
|
|
41
|
+
pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);
|
|
42
|
+
const currentFieldUsage = pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);
|
|
43
|
+
if (nestedFields === "all") {
|
|
44
|
+
pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, "all");
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!currentFieldUsage) {
|
|
48
|
+
if (pruneContext.hasUnparsableSourceFiles) continue;
|
|
49
|
+
pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, new Set(nestedFields));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (currentFieldUsage === "all") continue;
|
|
53
|
+
for (const nestedField of nestedFields) currentFieldUsage.add(nestedField);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Records the usage of a specific dictionary key's fields into `pruneContext`.
|
|
58
|
+
* Merges with any previously recorded usage for the same key.
|
|
59
|
+
*/
|
|
60
|
+
const recordFieldUsage = (pruneContext, dictionaryKey, fieldUsage) => {
|
|
61
|
+
const existingUsage = pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);
|
|
62
|
+
if (existingUsage === "all") return;
|
|
63
|
+
if (fieldUsage === "all") {
|
|
64
|
+
pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, "all");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (existingUsage instanceof Set) {
|
|
68
|
+
for (const fieldName of fieldUsage) existingUsage.add(fieldName);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, new Set(fieldUsage));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { createPruneContext, preserveNestedDictionaryFields, recordFieldUsage };
|
|
76
|
+
//# sourceMappingURL=pruneContext.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pruneContext.mjs","names":[],"sources":["../../src/pruneContext.ts"],"sourcesContent":["import type { NestedFieldReferences } from '@intlayer/core/dictionaryManipulator';\n\n/**\n * Dictionary field usage result for a single dictionary key.\n *\n * 'all' → could not determine statically which fields are used;\n * keep every field (no pruning possible).\n * Set<string> → the exact top-level content field names that were accessed.\n */\nexport type DictionaryFieldUsage = Set<string> | 'all';\n\n/**\n * One node in the nested field-rename tree.\n *\n * shortName – the compact alias assigned to this field name.\n * children – rename table for the next level of user-defined keys inside\n * this field's value (empty when the value is a leaf / primitive).\n */\nexport type NestedRenameEntry = {\n shortName: string;\n children: NestedRenameMap;\n};\n\n/** A level of the field-rename tree, mapping original field names to entries. */\nexport type NestedRenameMap = Map<string, NestedRenameEntry>;\n\n/**\n * One opaque consumption of a dictionary content value: the value at\n * `fieldPath` escapes static analysis (passed as-is to a child component or a\n * function argument), so its children must keep their original key names in\n * the compiled JSON.\n */\nexport type OpaqueFieldOccurrence = {\n /**\n * Original (pre-rename) user-defined field names from the content root to\n * the escaping value, e.g. `['sections', 'hero']` for\n * `<Card data={content.sections.hero} />`.\n */\n fieldPath: string[];\n\n /** Source locations (`filePath:line`) where the value escapes. */\n locations: string[];\n};\n\n/**\n * Shared mutable state created once by the vite plugin and passed by reference\n * to the usage-analyzer (writer) and the prune/minify plugins (readers).\n *\n * All mutations happen during the usage-analysis `buildStart` phase; readers\n * only access this state during the subsequent `transform` phase.\n */\nexport type PruneContext = {\n /**\n * Maps every dictionary key seen in source files to the set of top-level\n * content fields statically accessed, or `'all'` when the access pattern\n * could not be determined.\n */\n dictionaryKeyToFieldUsageMap: Map<string, DictionaryFieldUsage>;\n\n /**\n * Dictionary keys for which the prune/minify step must be skipped entirely\n * because an edge case was detected during analysis or structure recognition.\n */\n dictionariesWithEdgeCases: Set<string>;\n\n /**\n * True if at least one source file failed to parse during the analysis phase.\n * The prune plugin uses this flag conservatively: any dictionary key without\n * a usage entry might have been referenced by the unparsable file.\n */\n hasUnparsableSourceFiles: boolean;\n\n /**\n * Maps dictionary keys to the source file paths where the result of\n * `useIntlayer` / `getIntlayer` was assigned to a plain variable, making\n * static field analysis impossible.\n */\n dictionaryKeysWithUntrackedBindings: Map<string, string[]>;\n\n /**\n * Maps each dictionary key to a nested field-rename tree built after the\n * usage analysis phase (only populated when `build.minify` is active and\n * the field usage for that dictionary is a finite `Set<string>`).\n */\n dictionaryKeyToFieldRenameMap: Map<string, NestedRenameMap>;\n\n /**\n * Maps each dictionary key to the content field paths whose values are\n * consumed \"opaquely\" (passed as-is to a child component or function\n * argument). When an opaque value has nested user-defined structure, its\n * children must not be renamed.\n *\n * The inner map is keyed by the dot-joined field path for de-duplication\n * only; the authoritative path is {@link OpaqueFieldOccurrence.fieldPath}.\n *\n * Structure: dictionaryKey → joinedFieldPath → { fieldPath, locations }\n */\n dictionaryKeysWithOpaqueFields: Map<\n string,\n Map<string, OpaqueFieldOccurrence>\n >;\n\n /**\n * Dictionary keys for which field-key renaming must be skipped even if a\n * finite field-usage set was determined.\n *\n * Populated for dictionaries whose plain-variable bindings were resolved by\n * the framework-specific extractor (Vue / Svelte SFCs), because the Babel\n * rename plugin cannot update the source-code property accesses for those\n * indirect patterns (Vue `.value.field` / Svelte `$store.field`).\n *\n * Pruning and basic minification still apply; only field-key renaming is\n * suppressed.\n */\n dictionariesSkippingFieldRename: Set<string>;\n\n /**\n * Plain variable bindings that require a framework-specific secondary pass.\n *\n * Populated during the Babel analysis phase for `.vue` and `.svelte` source\n * files where direct field access is not visible to Babel scope analysis:\n * - Vue: `content.value.fieldName` – the `.value` ref-accessor is hidden\n * - Svelte: `$varName.fieldName` – the `$` prefix creates a new identifier\n *\n * Structure: filePath → [{variableName, dictionaryKey}, …]\n */\n pendingFrameworkAnalysis: Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >;\n};\n\nexport const createPruneContext = (): PruneContext => ({\n dictionaryKeyToFieldUsageMap: new Map(),\n dictionariesWithEdgeCases: new Set(),\n hasUnparsableSourceFiles: false,\n dictionaryKeysWithUntrackedBindings: new Map(),\n dictionaryKeyToFieldRenameMap: new Map(),\n dictionaryKeysWithOpaqueFields: new Map<\n string,\n Map<string, OpaqueFieldOccurrence>\n >(),\n dictionariesSkippingFieldRename: new Set(),\n pendingFrameworkAnalysis: new Map(),\n});\n\n/**\n * Records `nest()` references in a prune context so nest targets stay\n * resolvable — and no larger than they need to be — after purge and minify.\n *\n * A nested node stores the ORIGINAL dot path of the field it reads\n * (`nest('common', 'period')` → `'period'`) and `getNesting` resolves it at\n * runtime against the compiled JSON. That drives three adjustments:\n *\n * - Nest targets never take part in field renaming, since the stored path\n * would no longer match the renamed keys.\n * - A target already consumed by a call site keeps its analysed fields plus\n * the nested ones, so neither consumer loses content.\n * - A target reached *only* through `nest()` is given a usage entry holding\n * exactly the nested fields. Without one, the purge pass leaves the whole\n * dictionary untouched; with one, it trims the dictionary down to what the\n * nest references actually read. This matters most for `dynamic` / `fetch`\n * dictionaries, whose full static snapshot would otherwise be pulled into\n * the eager bundle just to satisfy a single nested field.\n *\n * Seeding is skipped when a source file failed to parse: that file could\n * reference the dictionary in a way the analysis never saw, which is the same\n * guard the purge pass applies to keys with no usage entry.\n *\n * @param pruneContext - Shared analysis state to update in place.\n * @param nestedDictionaryReferences - Nest-target key → referenced top-level\n * fields, as returned by `getNestedDictionaryReferences`.\n */\nexport const preserveNestedDictionaryFields = (\n pruneContext: PruneContext,\n nestedDictionaryReferences: Map<string, NestedFieldReferences>\n): void => {\n for (const [\n dictionaryKey,\n nestedFields,\n ] of nestedDictionaryReferences.entries()) {\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n const currentFieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n // A whole-dictionary reference (`nest('common')`) reads every field, so no\n // field set can describe it — leave the dictionary unpruned.\n if (nestedFields === 'all') {\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, 'all');\n continue;\n }\n\n if (!currentFieldUsage) {\n if (pruneContext.hasUnparsableSourceFiles) continue;\n\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n new Set(nestedFields)\n );\n continue;\n }\n\n // Already consumed by a call site that reads every field.\n if (currentFieldUsage === 'all') continue;\n\n for (const nestedField of nestedFields) {\n currentFieldUsage.add(nestedField);\n }\n }\n};\n\n/**\n * Records the usage of a specific dictionary key's fields into `pruneContext`.\n * Merges with any previously recorded usage for the same key.\n */\nexport const recordFieldUsage = (\n pruneContext: PruneContext,\n dictionaryKey: string,\n fieldUsage: DictionaryFieldUsage\n): void => {\n const existingUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n if (existingUsage === 'all') return; // already saturated\n\n if (fieldUsage === 'all') {\n pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, 'all');\n return;\n }\n\n if (existingUsage instanceof Set) {\n // Merge in place — the set is owned by the map.\n for (const fieldName of fieldUsage) existingUsage.add(fieldName);\n return;\n }\n\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n new Set(fieldUsage)\n );\n};\n"],"mappings":";AAoIA,MAAa,4BAA0C;CACrD,8CAA8B,IAAI,IAAI;CACtC,2CAA2B,IAAI,IAAI;CACnC,0BAA0B;CAC1B,qDAAqC,IAAI,IAAI;CAC7C,+CAA+B,IAAI,IAAI;CACvC,gDAAgC,IAAI,IAGlC;CACF,iDAAiC,IAAI,IAAI;CACzC,0CAA0B,IAAI,IAAI;AACpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,kCACX,cACA,+BACS;CACT,KAAK,MAAM,CACT,eACA,iBACG,2BAA2B,QAAQ,GAAG;EACzC,aAAa,gCAAgC,IAAI,aAAa;EAE9D,MAAM,oBACJ,aAAa,6BAA6B,IAAI,aAAa;EAI7D,IAAI,iBAAiB,OAAO;GAC1B,aAAa,6BAA6B,IAAI,eAAe,KAAK;GAClE;EACF;EAEA,IAAI,CAAC,mBAAmB;GACtB,IAAI,aAAa,0BAA0B;GAE3C,aAAa,6BAA6B,IACxC,eACA,IAAI,IAAI,YAAY,CACtB;GACA;EACF;EAGA,IAAI,sBAAsB,OAAO;EAEjC,KAAK,MAAM,eAAe,cACxB,kBAAkB,IAAI,WAAW;CAErC;AACF;;;;;AAMA,MAAa,oBACX,cACA,eACA,eACS;CACT,MAAM,gBACJ,aAAa,6BAA6B,IAAI,aAAa;CAE7D,IAAI,kBAAkB,OAAO;CAE7B,IAAI,eAAe,OAAO;EACxB,aAAa,6BAA6B,IAAI,eAAe,KAAK;EAClE;CACF;CAEA,IAAI,yBAAyB,KAAK;EAEhC,KAAK,MAAM,aAAa,YAAY,cAAc,IAAI,SAAS;EAC/D;CACF;CAEA,aAAa,6BAA6B,IACxC,eACA,IAAI,IAAI,UAAU,CACpB;AACF"}
|