@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
|
@@ -1,694 +1,62 @@
|
|
|
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 { readStaticString, unwrapAwait } from "./staticAstReaders.mjs";
|
|
4
|
+
import { analyzeCallExpressionUsage } from "./analyzeContentUsage.mjs";
|
|
5
|
+
import { createCompatUsageAnalyzer } from "./compat/usageAnalysis.mjs";
|
|
6
|
+
|
|
1
7
|
//#region src/babel-plugin-intlayer-usage-analyzer.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
8
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
* Methods the runtime attaches to a resolved content object rather than fields
|
|
58
|
-
* declared in a `.content` file.
|
|
59
|
-
*
|
|
60
|
-
* `vanilla-intlayer`'s `useIntlayer` / `useDictionary` assign `onChange` onto
|
|
61
|
-
* the content they return, so `useIntlayer('app').onChange(cb)` is a
|
|
62
|
-
* subscription — not a read of a field named `onChange`. Without this list the
|
|
63
|
-
* analyser records `onChange` as the only consumed field and the purge pass
|
|
64
|
-
* strips every real field from the compiled dictionary.
|
|
65
|
-
*
|
|
66
|
-
* The analyser looks *through* these calls: the callback parameter is analysed
|
|
67
|
-
* as a fresh content root, and the call result is analysed again because the
|
|
68
|
-
* helpers return the content object for chaining.
|
|
69
|
-
*/
|
|
70
|
-
const CHAINABLE_RUNTIME_METHOD_NAMES = /* @__PURE__ */ new Set(["onChange"]);
|
|
71
|
-
/** Canonical intlayer caller names that trigger usage analysis. */
|
|
72
|
-
const INTLAYER_CALLER_NAMES = [
|
|
73
|
-
"useIntlayer",
|
|
74
|
-
"getIntlayer",
|
|
75
|
-
"getIntlayerAsync"
|
|
76
|
-
];
|
|
77
|
-
/**
|
|
78
|
-
* Default registry of compat namespace callers.
|
|
79
|
-
*
|
|
80
|
-
* Intentionally empty — compat-specific caller configurations belong in their
|
|
81
|
-
* respective adapter packages (e.g. `@intlayer/react-i18next/plugin`,
|
|
82
|
-
* `@intlayer/vue-i18n/plugin`) and are injected into `makeUsageAnalyzerBabelPlugin`
|
|
83
|
-
* via the `compatCallers` option. Centralising them here would couple the
|
|
84
|
-
* core `@intlayer/babel` package to every compat adapter, which violates the
|
|
85
|
-
* design principle that compat logic lives in compat packages.
|
|
9
|
+
* @deprecated Always empty. Compat callers are injected per build through
|
|
10
|
+
* `options.compatCallers`; there is no default registry to fall back to.
|
|
86
11
|
*/
|
|
87
12
|
const DEFAULT_COMPAT_CALLERS = [];
|
|
88
|
-
/** Default namespace used by compat callers when no namespace argument is given. */
|
|
89
|
-
const DEFAULT_COMPAT_NAMESPACE = "translation";
|
|
90
|
-
/**
|
|
91
|
-
* Climbs past an enclosing `await` expression so that
|
|
92
|
-
* `const t = await getTranslations('ns')` — or `await getIntlayerAsync('key')`
|
|
93
|
-
* — is resolved to its variable declarator the same way the synchronous form
|
|
94
|
-
* is.
|
|
95
|
-
*/
|
|
96
|
-
const unwrapAwait = (babelTypes, path) => {
|
|
97
|
-
const parentPath = path.parentPath;
|
|
98
|
-
if (parentPath && babelTypes.isAwaitExpression(parentPath.node)) return parentPath;
|
|
99
|
-
return path;
|
|
100
|
-
};
|
|
101
|
-
/**
|
|
102
|
-
* Records the usage of a specific dictionary key's fields into `pruneContext`.
|
|
103
|
-
* Merges with any previously recorded usage for the same key.
|
|
104
|
-
*/
|
|
105
|
-
const recordFieldUsage = (pruneContext, dictionaryKey, fieldUsage) => {
|
|
106
|
-
const existingUsage = pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);
|
|
107
|
-
if (existingUsage === "all") return;
|
|
108
|
-
if (fieldUsage === "all") {
|
|
109
|
-
pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, "all");
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (existingUsage instanceof Set) {
|
|
113
|
-
for (const fieldName of fieldUsage) existingUsage.add(fieldName);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
pruneContext.dictionaryKeyToFieldUsageMap.set(dictionaryKey, new Set(fieldUsage));
|
|
117
|
-
};
|
|
118
|
-
/**
|
|
119
|
-
* Analyses how the result of a single `useIntlayer('key')` / `getIntlayer('key')`
|
|
120
|
-
* call expression is consumed, then records the field usage into `pruneContext`.
|
|
121
|
-
*
|
|
122
|
-
* Recognised patterns:
|
|
123
|
-
* const { fieldA, fieldB } = useIntlayer('key') → records {fieldA, fieldB}
|
|
124
|
-
* useIntlayer('key').fieldA → records {fieldA}
|
|
125
|
-
* useIntlayer('key')['fieldA'] → records {fieldA}
|
|
126
|
-
* const { ...rest } = useIntlayer('key') → records 'all' (spread)
|
|
127
|
-
* const result = useIntlayer('key') → records 'all' (untracked binding)
|
|
128
|
-
*/
|
|
129
|
-
const analyzeCallExpressionUsage = (babelTypes, pruneContext, callExpressionPath, dictionaryKey, currentSourceFilePath, isSfcFile) => {
|
|
130
|
-
/** Mark the dictionary key as having an untracked binding in this file. */
|
|
131
|
-
const markUntrackedBinding = () => {
|
|
132
|
-
const existingPaths = pruneContext.dictionaryKeysWithUntrackedBindings.get(dictionaryKey) ?? [];
|
|
133
|
-
if (!existingPaths.includes(currentSourceFilePath)) pruneContext.dictionaryKeysWithUntrackedBindings.set(dictionaryKey, [...existingPaths, currentSourceFilePath]);
|
|
134
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
135
|
-
};
|
|
136
|
-
/** Record that the content value at `fieldPath` is consumed opaquely. */
|
|
137
|
-
const markOpaqueField = (fieldPath, line) => {
|
|
138
|
-
const pathToOccurrence = pruneContext.dictionaryKeysWithOpaqueFields.get(dictionaryKey) ?? /* @__PURE__ */ new Map();
|
|
139
|
-
const location = line !== void 0 ? `${currentSourceFilePath}:${line}` : currentSourceFilePath;
|
|
140
|
-
const joinedFieldPath = fieldPath.join(".");
|
|
141
|
-
const occurrence = pathToOccurrence.get(joinedFieldPath) ?? {
|
|
142
|
-
fieldPath: [...fieldPath],
|
|
143
|
-
locations: []
|
|
144
|
-
};
|
|
145
|
-
if (!occurrence.locations.includes(location)) occurrence.locations.push(location);
|
|
146
|
-
pathToOccurrence.set(joinedFieldPath, occurrence);
|
|
147
|
-
pruneContext.dictionaryKeysWithOpaqueFields.set(dictionaryKey, pathToOccurrence);
|
|
148
|
-
};
|
|
149
|
-
/** Register a plain variable binding in an SFC file for a second-pass analysis. */
|
|
150
|
-
const deferFrameworkAnalysis = (variableName) => {
|
|
151
|
-
const existing = pruneContext.pendingFrameworkAnalysis.get(currentSourceFilePath) ?? [];
|
|
152
|
-
if (!existing.some((e) => e.variableName === variableName && e.dictionaryKey === dictionaryKey)) existing.push({
|
|
153
|
-
variableName,
|
|
154
|
-
dictionaryKey
|
|
155
|
-
});
|
|
156
|
-
pruneContext.pendingFrameworkAnalysis.set(currentSourceFilePath, existing);
|
|
157
|
-
};
|
|
158
|
-
/**
|
|
159
|
-
* Analyses how the content value at `fieldPath` (currently referenced by
|
|
160
|
-
* `refPath`) is consumed, to detect opaque consumption (passing a dictionary
|
|
161
|
-
* value as-is to a prop or function argument).
|
|
162
|
-
*
|
|
163
|
-
* Member-access chains and destructuring are followed recursively with an
|
|
164
|
-
* extended field path, so the *terminal* consumption of a chain like
|
|
165
|
-
* `content.sections.hero` decides opacity — mirroring how far the
|
|
166
|
-
* source-code renamer can rewrite accesses. When the terminal consumption
|
|
167
|
-
* escapes static tracking, the deepest reached field path is marked opaque
|
|
168
|
-
* so the children of that value keep their original key names.
|
|
169
|
-
*/
|
|
170
|
-
const analyzeOpaqueUsage = (refPath, fieldPath) => {
|
|
171
|
-
const parentNode = refPath.parent;
|
|
172
|
-
const parentPath = refPath.parentPath;
|
|
173
|
-
if (parentPath && (babelTypes.isMemberExpression(parentNode) || babelTypes.isOptionalMemberExpression(parentNode)) && parentNode.object === refPath.node) {
|
|
174
|
-
const memberNode = parentNode;
|
|
175
|
-
if (memberNode.computed && babelTypes.isNumericLiteral(memberNode.property)) {
|
|
176
|
-
analyzeOpaqueUsage(parentPath, fieldPath);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (!memberNode.computed && babelTypes.isIdentifier(memberNode.property)) {
|
|
180
|
-
analyzeOpaqueUsage(parentPath, [...fieldPath, memberNode.property.name]);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (memberNode.computed && babelTypes.isStringLiteral(memberNode.property)) {
|
|
184
|
-
analyzeOpaqueUsage(parentPath, [...fieldPath, memberNode.property.value]);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
markOpaqueField(fieldPath, refPath.node.loc?.start.line);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id) && parentNode.init === refPath.node) {
|
|
191
|
-
analyzeOpaqueDestructuring(parentNode.id, refPath, fieldPath);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (babelTypes.isArrayExpression(parentNode)) return;
|
|
195
|
-
markOpaqueField(fieldPath, refPath.node.loc?.start.line);
|
|
196
|
-
};
|
|
197
|
-
/**
|
|
198
|
-
* Analyses a destructuring pattern applied to the content value at
|
|
199
|
-
* `fieldPath`, recursively following each bound local variable's references
|
|
200
|
-
* via {@link analyzeOpaqueUsage}.
|
|
201
|
-
*
|
|
202
|
-
* Patterns the source-code renamer cannot rewrite (rest elements, dynamic
|
|
203
|
-
* computed keys, array patterns) mark the corresponding path opaque.
|
|
204
|
-
*/
|
|
205
|
-
const analyzeOpaqueDestructuring = (objectPattern, scopePath, fieldPath) => {
|
|
206
|
-
for (const property of objectPattern.properties) {
|
|
207
|
-
if (babelTypes.isRestElement(property)) {
|
|
208
|
-
markOpaqueField(fieldPath, property.loc?.start.line);
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (!babelTypes.isObjectProperty(property)) continue;
|
|
212
|
-
if (property.computed && !babelTypes.isStringLiteral(property.key)) {
|
|
213
|
-
markOpaqueField(fieldPath, property.loc?.start.line);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
const keyName = babelTypes.isIdentifier(property.key) ? property.key.name : babelTypes.isStringLiteral(property.key) ? property.key.value : null;
|
|
217
|
-
if (!keyName) continue;
|
|
218
|
-
const childFieldPath = [...fieldPath, keyName];
|
|
219
|
-
const bindingTarget = babelTypes.isAssignmentPattern(property.value) ? property.value.left : property.value;
|
|
220
|
-
if (babelTypes.isObjectPattern(bindingTarget)) {
|
|
221
|
-
analyzeOpaqueDestructuring(bindingTarget, scopePath, childFieldPath);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (babelTypes.isIdentifier(bindingTarget)) {
|
|
225
|
-
const localVarBinding = scopePath.scope.getBinding(bindingTarget.name);
|
|
226
|
-
if (!localVarBinding) continue;
|
|
227
|
-
for (const referencePath of localVarBinding.referencePaths) analyzeOpaqueUsage(referencePath, childFieldPath);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
markOpaqueField(childFieldPath, property.loc?.start.line);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
/**
|
|
234
|
-
* Helper to collect field names from an ObjectPattern (destructuring).
|
|
235
|
-
* Returns true if successful, false when the accessed fields cannot be
|
|
236
|
-
* determined statically (rest element or dynamic computed key → 'all').
|
|
237
|
-
*/
|
|
238
|
-
const collectFieldsFromObjectPattern = (pattern, initPath, targetSet) => {
|
|
239
|
-
if (pattern.properties.some((prop) => babelTypes.isRestElement(prop))) return false;
|
|
240
|
-
for (const property of pattern.properties) {
|
|
241
|
-
if (!babelTypes.isObjectProperty(property)) continue;
|
|
242
|
-
if (property.computed && !babelTypes.isStringLiteral(property.key)) return false;
|
|
243
|
-
let fieldName;
|
|
244
|
-
if (!property.computed && babelTypes.isIdentifier(property.key)) fieldName = property.key.name;
|
|
245
|
-
else if (babelTypes.isStringLiteral(property.key)) fieldName = property.key.value;
|
|
246
|
-
if (!fieldName) continue;
|
|
247
|
-
targetSet.add(fieldName);
|
|
248
|
-
const bindingTarget = babelTypes.isAssignmentPattern(property.value) ? property.value.left : property.value;
|
|
249
|
-
if (babelTypes.isIdentifier(bindingTarget)) {
|
|
250
|
-
const variableBinding = initPath.scope.getBinding(bindingTarget.name);
|
|
251
|
-
if (variableBinding) for (const refPath of variableBinding.referencePaths) analyzeOpaqueUsage(refPath, [fieldName]);
|
|
252
|
-
} else if (babelTypes.isObjectPattern(bindingTarget)) analyzeOpaqueDestructuring(bindingTarget, initPath, [fieldName]);
|
|
253
|
-
else markOpaqueField([fieldName], property.loc?.start.line);
|
|
254
|
-
}
|
|
255
|
-
return true;
|
|
256
|
-
};
|
|
257
|
-
/**
|
|
258
|
-
* Returns the call path when `memberExprPath` is a chainable runtime helper
|
|
259
|
-
* being invoked (`content.onChange(cb)`), otherwise `undefined`.
|
|
260
|
-
*
|
|
261
|
-
* See {@link CHAINABLE_RUNTIME_METHOD_NAMES}: a bare `content.onChange`
|
|
262
|
-
* reference that is never called is treated as an ordinary field access, so
|
|
263
|
-
* only the invoked form is looked through.
|
|
264
|
-
*/
|
|
265
|
-
const getChainedRuntimeCallPath = (memberExprPath, fieldName) => {
|
|
266
|
-
if (!CHAINABLE_RUNTIME_METHOD_NAMES.has(fieldName)) return void 0;
|
|
267
|
-
if (!memberExprPath) return void 0;
|
|
268
|
-
const callPath = memberExprPath.parentPath;
|
|
269
|
-
if (!callPath) return void 0;
|
|
270
|
-
if (!callPath.isCallExpression() && !callPath.isOptionalCallExpression()) return;
|
|
271
|
-
if (callPath.node.callee !== memberExprPath.node) return;
|
|
272
|
-
return callPath;
|
|
273
|
-
};
|
|
274
|
-
/**
|
|
275
|
-
* Analyses the callbacks handed to a chainable runtime helper.
|
|
276
|
-
*
|
|
277
|
-
* The callback receives the freshly-resolved content root, so its parameter
|
|
278
|
-
* is walked exactly like `const content = useIntlayer('key')` — either as a
|
|
279
|
-
* plain binding or as a destructuring pattern.
|
|
280
|
-
*/
|
|
281
|
-
const analyzeChainedCallbackArguments = (callPath) => {
|
|
282
|
-
const argumentPaths = callPath.get("arguments");
|
|
283
|
-
for (const argumentPath of argumentPaths) {
|
|
284
|
-
if (!argumentPath.isArrowFunctionExpression() && !argumentPath.isFunctionExpression()) continue;
|
|
285
|
-
const [firstParameterPath] = argumentPath.get("params");
|
|
286
|
-
if (!firstParameterPath) continue;
|
|
287
|
-
if (firstParameterPath.isIdentifier()) {
|
|
288
|
-
const parameterBinding = argumentPath.scope.getBinding(firstParameterPath.node.name);
|
|
289
|
-
if (!parameterBinding) {
|
|
290
|
-
markUntrackedBinding();
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
const accessedFieldNames = /* @__PURE__ */ new Set();
|
|
294
|
-
if (analyzeContentBindingReferences(parameterBinding, accessedFieldNames)) markUntrackedBinding();
|
|
295
|
-
else recordFieldUsage(pruneContext, dictionaryKey, accessedFieldNames);
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
if (firstParameterPath.isObjectPattern()) {
|
|
299
|
-
const accessedFieldNames = /* @__PURE__ */ new Set();
|
|
300
|
-
if (collectFieldsFromObjectPattern(firstParameterPath.node, argumentPath, accessedFieldNames)) recordFieldUsage(pruneContext, dictionaryKey, accessedFieldNames);
|
|
301
|
-
else recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
markUntrackedBinding();
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
/**
|
|
308
|
-
* Walks every reference of a local binding holding the content root,
|
|
309
|
-
* collecting the top-level fields it reads into `accessedTopLevelFieldNames`.
|
|
310
|
-
*
|
|
311
|
-
* @returns `true` when a reference escaped static tracking, meaning the whole
|
|
312
|
-
* dictionary must be kept.
|
|
313
|
-
*/
|
|
314
|
-
const analyzeContentBindingReferences = (variableBinding, accessedTopLevelFieldNames) => {
|
|
315
|
-
for (const variableReferencePath of variableBinding.referencePaths) {
|
|
316
|
-
const referenceParentNode = variableReferencePath.parent;
|
|
317
|
-
if ((babelTypes.isMemberExpression(referenceParentNode) || babelTypes.isOptionalMemberExpression(referenceParentNode)) && referenceParentNode.object === variableReferencePath.node) {
|
|
318
|
-
const memberExpressionNode = referenceParentNode;
|
|
319
|
-
let fieldName;
|
|
320
|
-
if (!memberExpressionNode.computed && babelTypes.isIdentifier(memberExpressionNode.property)) fieldName = memberExpressionNode.property.name;
|
|
321
|
-
else if (memberExpressionNode.computed && babelTypes.isStringLiteral(memberExpressionNode.property)) fieldName = memberExpressionNode.property.value;
|
|
322
|
-
if (fieldName) {
|
|
323
|
-
const memberExprPath = variableReferencePath.parentPath;
|
|
324
|
-
const chainedCallPath = getChainedRuntimeCallPath(memberExprPath, fieldName);
|
|
325
|
-
if (chainedCallPath) {
|
|
326
|
-
analyzeChainedCallbackArguments(chainedCallPath);
|
|
327
|
-
analyzeContentRoot(chainedCallPath);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
accessedTopLevelFieldNames.add(fieldName);
|
|
331
|
-
if (memberExprPath) analyzeOpaqueUsage(memberExprPath, [fieldName]);
|
|
332
|
-
} else return true;
|
|
333
|
-
} else if (babelTypes.isArrayExpression(referenceParentNode)) return true;
|
|
334
|
-
else if ((babelTypes.isCallExpression(referenceParentNode) || babelTypes.isOptionalCallExpression(referenceParentNode)) && referenceParentNode.callee === variableReferencePath.node) {
|
|
335
|
-
const callExprPath = variableReferencePath.parentPath;
|
|
336
|
-
const callParent = callExprPath?.parent;
|
|
337
|
-
if (callParent && (babelTypes.isMemberExpression(callParent) || babelTypes.isOptionalMemberExpression(callParent)) && callParent.object === callExprPath?.node) {
|
|
338
|
-
const memberExpr = callParent;
|
|
339
|
-
let fieldName;
|
|
340
|
-
if (!memberExpr.computed && babelTypes.isIdentifier(memberExpr.property)) fieldName = memberExpr.property.name;
|
|
341
|
-
else if (memberExpr.computed && babelTypes.isStringLiteral(memberExpr.property)) fieldName = memberExpr.property.value;
|
|
342
|
-
if (fieldName) {
|
|
343
|
-
const memberExprPath = callExprPath?.parentPath;
|
|
344
|
-
const chainedCallPath = getChainedRuntimeCallPath(memberExprPath, fieldName);
|
|
345
|
-
if (chainedCallPath) {
|
|
346
|
-
analyzeChainedCallbackArguments(chainedCallPath);
|
|
347
|
-
analyzeContentRoot(chainedCallPath);
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
accessedTopLevelFieldNames.add(fieldName);
|
|
351
|
-
if (memberExprPath) analyzeOpaqueUsage(memberExprPath, [fieldName]);
|
|
352
|
-
} else return true;
|
|
353
|
-
} else if (callParent && babelTypes.isVariableDeclarator(callParent) && babelTypes.isObjectPattern(callParent.id) && callExprPath && collectFieldsFromObjectPattern(callParent.id, callExprPath, accessedTopLevelFieldNames)) {} else return true;
|
|
354
|
-
} else return true;
|
|
355
|
-
}
|
|
356
|
-
return false;
|
|
357
|
-
};
|
|
358
|
-
/**
|
|
359
|
-
* Analyses how the content root referenced by `rootPath` is consumed.
|
|
360
|
-
*
|
|
361
|
-
* `rootPath` is the `useIntlayer('key')` call itself, or any expression that
|
|
362
|
-
* evaluates back to the same content object — currently the result of a
|
|
363
|
-
* chainable runtime helper such as `.onChange(…)`.
|
|
364
|
-
*/
|
|
365
|
-
const analyzeContentRoot = (rootPath) => {
|
|
366
|
-
const parentNode = rootPath.parent;
|
|
367
|
-
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id)) {
|
|
368
|
-
const accessedFieldNames = /* @__PURE__ */ new Set();
|
|
369
|
-
if (collectFieldsFromObjectPattern(parentNode.id, rootPath, accessedFieldNames)) recordFieldUsage(pruneContext, dictionaryKey, accessedFieldNames);
|
|
370
|
-
else recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
if ((babelTypes.isMemberExpression(parentNode) || babelTypes.isOptionalMemberExpression(parentNode)) && parentNode.object === rootPath.node) {
|
|
374
|
-
let fieldName;
|
|
375
|
-
if (!parentNode.computed && babelTypes.isIdentifier(parentNode.property)) fieldName = parentNode.property.name;
|
|
376
|
-
else if (parentNode.computed && babelTypes.isStringLiteral(parentNode.property)) fieldName = parentNode.property.value;
|
|
377
|
-
if (fieldName) {
|
|
378
|
-
const memberExprPath = rootPath.parentPath;
|
|
379
|
-
const chainedCallPath = getChainedRuntimeCallPath(memberExprPath, fieldName);
|
|
380
|
-
if (chainedCallPath) {
|
|
381
|
-
analyzeChainedCallbackArguments(chainedCallPath);
|
|
382
|
-
analyzeContentRoot(chainedCallPath);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
recordFieldUsage(pruneContext, dictionaryKey, /* @__PURE__ */ new Set([fieldName]));
|
|
386
|
-
if (memberExprPath) analyzeOpaqueUsage(memberExprPath, [fieldName]);
|
|
387
|
-
} else markUntrackedBinding();
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isIdentifier(parentNode.id)) {
|
|
391
|
-
const variableName = parentNode.id.name;
|
|
392
|
-
const variableBinding = rootPath.scope.getBinding(variableName);
|
|
393
|
-
if (!variableBinding) {
|
|
394
|
-
markUntrackedBinding();
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const accessedTopLevelFieldNames = /* @__PURE__ */ new Set();
|
|
398
|
-
if (analyzeContentBindingReferences(variableBinding, accessedTopLevelFieldNames)) markUntrackedBinding();
|
|
399
|
-
else if (isSfcFile) deferFrameworkAnalysis(variableName);
|
|
400
|
-
else if (variableBinding.referencePaths.length === 0) markUntrackedBinding();
|
|
401
|
-
else recordFieldUsage(pruneContext, dictionaryKey, accessedTopLevelFieldNames);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
if (babelTypes.isExpressionStatement(parentNode)) return;
|
|
405
|
-
markUntrackedBinding();
|
|
406
|
-
};
|
|
407
|
-
analyzeContentRoot(unwrapAwait(babelTypes, callExpressionPath));
|
|
408
|
-
};
|
|
409
|
-
/**
|
|
410
|
-
* Reads a fully-static string from an AST node. Returns `undefined` for
|
|
411
|
-
* dynamic values (identifiers, expressions, template literals with
|
|
412
|
-
* interpolations, …).
|
|
413
|
-
*/
|
|
414
|
-
const readStaticString = (babelTypes, node) => {
|
|
415
|
-
if (!node) return void 0;
|
|
416
|
-
if (babelTypes.isStringLiteral(node)) return node.value;
|
|
417
|
-
if (babelTypes.isTemplateLiteral(node) && node.expressions.length === 0 && node.quasis.length === 1) return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;
|
|
418
|
-
};
|
|
419
|
-
/** Returns the first dot-path segment of a key, e.g. `'a.b.c'` → `'a'`. */
|
|
420
|
-
const firstPathSegment = (path) => path.split(".")[0] ?? path;
|
|
421
|
-
/**
|
|
422
|
-
* Reads the static first dot-path segment from a `t()` first-argument node.
|
|
423
|
-
*
|
|
424
|
-
* t('counter.label') → 'counter'
|
|
425
|
-
* t(`counter.${x}`) → 'counter' (static prefix before the first dot)
|
|
426
|
-
* t(`${x}.label`) → undefined (dynamic first segment)
|
|
427
|
-
* t(someVariable) → undefined
|
|
428
|
-
*/
|
|
429
|
-
const readStaticFirstSegment = (babelTypes, node) => {
|
|
430
|
-
const staticString = readStaticString(babelTypes, node);
|
|
431
|
-
if (staticString !== void 0) return firstPathSegment(staticString);
|
|
432
|
-
if (babelTypes.isTemplateLiteral(node) && node.quasis.length > 0) {
|
|
433
|
-
const firstQuasi = node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;
|
|
434
|
-
if (firstQuasi?.includes(".")) return firstPathSegment(firstQuasi);
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
/**
|
|
438
|
-
* Reads a static string property from an object expression. Returns
|
|
439
|
-
* `'__default__'` when the property is absent and `undefined` when present but
|
|
440
|
-
* dynamic.
|
|
441
|
-
*/
|
|
442
|
-
const readObjectProperty = (babelTypes, objectExpression, propertyName) => {
|
|
443
|
-
for (const property of objectExpression.properties) {
|
|
444
|
-
if (!babelTypes.isObjectProperty(property)) continue;
|
|
445
|
-
if (!(babelTypes.isIdentifier(property.key) && property.key.name === propertyName || babelTypes.isStringLiteral(property.key) && property.key.value === propertyName)) continue;
|
|
446
|
-
return readStaticString(babelTypes, property.value) ?? void 0;
|
|
447
|
-
}
|
|
448
|
-
return "__default__";
|
|
449
|
-
};
|
|
450
|
-
/**
|
|
451
|
-
* Reads a named JSX attribute as a static string and returns it wrapped in a
|
|
452
|
-
* `StringLiteral` node so the result can be fed to the same namespace resolver
|
|
453
|
-
* as a call argument. Handles both `id="home.title"` and `id={'home.title'}`.
|
|
454
|
-
* Returns `undefined` when the attribute is absent or dynamic (`id={expr}`).
|
|
455
|
-
*/
|
|
456
|
-
const readJsxAttributeString = (babelTypes, openingElement, attributeName) => {
|
|
457
|
-
for (const attribute of openingElement.attributes) {
|
|
458
|
-
if (!babelTypes.isJSXAttribute(attribute)) continue;
|
|
459
|
-
if (!babelTypes.isJSXIdentifier(attribute.name) || attribute.name.name !== attributeName) continue;
|
|
460
|
-
const value = attribute.value;
|
|
461
|
-
if (babelTypes.isStringLiteral(value)) return value;
|
|
462
|
-
if (babelTypes.isJSXExpressionContainer(value)) {
|
|
463
|
-
const staticString = readStaticString(babelTypes, value.expression);
|
|
464
|
-
if (staticString !== void 0) return babelTypes.stringLiteral(staticString);
|
|
465
|
-
}
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
/**
|
|
470
|
-
* Resolves the namespace (dictionary key) for a compat caller call-site from
|
|
471
|
-
* one `CallerValueSource`. Returns the static key, or `'__default__'` when the
|
|
472
|
-
* configured argument is absent (caller falls back to its default namespace),
|
|
473
|
-
* or `undefined` when the value is present but dynamic.
|
|
474
|
-
*/
|
|
475
|
-
const resolveCompatNamespace = (babelTypes, callArguments, source) => {
|
|
476
|
-
if (source.from === "fixed") return source.value;
|
|
477
|
-
if (source.from === "path-first-segment") {
|
|
478
|
-
const firstArg = callArguments[0];
|
|
479
|
-
if (firstArg === void 0) return void 0;
|
|
480
|
-
const staticString = readStaticString(babelTypes, firstArg);
|
|
481
|
-
if (staticString !== void 0) return firstPathSegment(staticString);
|
|
482
|
-
if (babelTypes.isObjectExpression(firstArg)) {
|
|
483
|
-
const idValue = readObjectProperty(babelTypes, firstArg, "id");
|
|
484
|
-
if (idValue === "__default__") return "__default__";
|
|
485
|
-
if (idValue !== void 0) return firstPathSegment(idValue);
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
if (source.from === "argument") {
|
|
491
|
-
const argument = callArguments[source.index];
|
|
492
|
-
if (argument === void 0) return "__default__";
|
|
493
|
-
const staticString = readStaticString(babelTypes, argument);
|
|
494
|
-
if (staticString !== void 0) return staticString;
|
|
495
|
-
if (babelTypes.isObjectExpression(argument)) return readObjectProperty(babelTypes, argument, "namespace");
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
const optionsArgument = callArguments[source.argumentIndex];
|
|
499
|
-
if (optionsArgument === void 0) return "__default__";
|
|
500
|
-
if (!babelTypes.isObjectExpression(optionsArgument)) return void 0;
|
|
501
|
-
return readObjectProperty(babelTypes, optionsArgument, source.property);
|
|
502
|
-
};
|
|
503
|
-
/**
|
|
504
|
-
* Resolves the namespace from a list of `CallerValueSource`s, tried in
|
|
505
|
-
* declaration order. The first source yielding a static string wins.
|
|
506
|
-
* Returns `'__default__'` when every source reports an absent value, and
|
|
507
|
-
* `undefined` when the namespace is present somewhere but dynamic.
|
|
508
|
-
*/
|
|
509
|
-
const resolveNamespaceFromSources = (babelTypes, callArguments, sources) => {
|
|
510
|
-
let sawAbsentValue = false;
|
|
511
|
-
for (const source of sources) {
|
|
512
|
-
const resolved = resolveCompatNamespace(babelTypes, callArguments, source);
|
|
513
|
-
if (typeof resolved === "string" && resolved !== "__default__") return resolved;
|
|
514
|
-
if (resolved === "__default__") sawAbsentValue = true;
|
|
515
|
-
}
|
|
516
|
-
return sawAbsentValue ? "__default__" : void 0;
|
|
517
|
-
};
|
|
518
|
-
/**
|
|
519
|
-
* Resolves an optional `keyPrefix` for a compat caller. Returns the static
|
|
520
|
-
* prefix string, `null` when no prefix is configured/present, or `undefined`
|
|
521
|
-
* when a prefix is present but dynamic.
|
|
522
|
-
*/
|
|
523
|
-
const resolveCompatKeyPrefix = (babelTypes, callArguments, sources) => {
|
|
524
|
-
if (!sources || sources.length === 0) return null;
|
|
525
|
-
const resolved = resolveNamespaceFromSources(babelTypes, callArguments, sources);
|
|
526
|
-
if (resolved === "__default__") return null;
|
|
527
|
-
return resolved;
|
|
528
|
-
};
|
|
529
|
-
/**
|
|
530
|
-
* Analyses how the translation function produced by a compat namespace caller
|
|
531
|
-
* (`useTranslation`, `useTranslations`, `getTranslations`, `getFixedT`,
|
|
532
|
-
* `useI18n`) is consumed, then records the accessed top-level dictionary fields
|
|
533
|
-
* into `pruneContext`.
|
|
534
|
-
*
|
|
535
|
-
* Dictionaries consumed this way are always added to
|
|
536
|
-
* `dictionariesSkippingFieldRename`: the field accesses are string-literal
|
|
537
|
-
* dot-paths inside `t()` calls, which the field-rename plugin cannot rewrite,
|
|
538
|
-
* so renaming the compiled JSON keys would break runtime lookups. Pruning
|
|
539
|
-
* (top-level field removal) remains safe because it preserves field names.
|
|
540
|
-
*/
|
|
541
|
-
const analyzeNamespaceCallerUsage = (babelTypes, pruneContext, callArguments, callerConfig, isSfcFile, currentSourceFilePath, callExpressionPath, namespaceOverride) => {
|
|
542
|
-
const resolvedNamespace = namespaceOverride ?? resolveNamespaceFromSources(babelTypes, callArguments, callerConfig.namespaceSources);
|
|
543
|
-
if (resolvedNamespace === void 0) return;
|
|
544
|
-
const namespaceString = resolvedNamespace === "__default__" ? DEFAULT_COMPAT_NAMESPACE : resolvedNamespace;
|
|
545
|
-
const namespaceSegments = namespaceString.split(".");
|
|
546
|
-
const dictionaryKey = namespaceSegments[0] ?? namespaceString;
|
|
547
|
-
const namespacePrefix = namespaceSegments.length > 1 ? namespaceSegments.slice(1).join(".") : null;
|
|
548
|
-
pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);
|
|
549
|
-
if (isSfcFile) {
|
|
550
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (callerConfig.translationFunction === "all") {
|
|
554
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
if (callerConfig.translationFunction === "content") {
|
|
558
|
-
if (callExpressionPath) analyzeCallExpressionUsage(babelTypes, pruneContext, callExpressionPath, dictionaryKey, currentSourceFilePath, isSfcFile);
|
|
559
|
-
else recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (callerConfig.translationFunction === "self") {
|
|
563
|
-
const firstArg = callArguments[0];
|
|
564
|
-
if (!firstArg) {
|
|
565
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (callerConfig.namespaceSources.some((source) => source.from === "path-first-segment")) {
|
|
569
|
-
let fullId;
|
|
570
|
-
const staticString = readStaticString(babelTypes, firstArg);
|
|
571
|
-
if (staticString !== void 0) fullId = staticString;
|
|
572
|
-
else if (babelTypes.isObjectExpression(firstArg)) {
|
|
573
|
-
const idValue = readObjectProperty(babelTypes, firstArg, "id");
|
|
574
|
-
if (idValue !== void 0 && idValue !== "__default__") fullId = idValue;
|
|
575
|
-
}
|
|
576
|
-
if (fullId !== void 0) {
|
|
577
|
-
const field = fullId.split(".")[1];
|
|
578
|
-
if (field !== void 0) recordFieldUsage(pruneContext, dictionaryKey, /* @__PURE__ */ new Set([field]));
|
|
579
|
-
else recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
const segment = callerConfig.flatKey ? readStaticString(babelTypes, firstArg) : readStaticFirstSegment(babelTypes, firstArg);
|
|
586
|
-
if (segment !== void 0) {
|
|
587
|
-
recordFieldUsage(pruneContext, dictionaryKey, /* @__PURE__ */ new Set([segment]));
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
if (babelTypes.isObjectExpression(firstArg)) {
|
|
591
|
-
const idValue = readObjectProperty(babelTypes, firstArg, "id");
|
|
592
|
-
if (idValue !== void 0 && idValue !== "__default__") {
|
|
593
|
-
recordFieldUsage(pruneContext, dictionaryKey, /* @__PURE__ */ new Set([callerConfig.flatKey ? idValue : firstPathSegment(idValue)]));
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
if (!callExpressionPath) {
|
|
601
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
const explicitKeyPrefix = resolveCompatKeyPrefix(babelTypes, callArguments, callerConfig.keyPrefixSources);
|
|
605
|
-
if (explicitKeyPrefix === void 0) {
|
|
606
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
const effectivePrefix = namespacePrefix ?? explicitKeyPrefix;
|
|
610
|
-
if (effectivePrefix !== null) {
|
|
611
|
-
recordFieldUsage(pruneContext, dictionaryKey, /* @__PURE__ */ new Set([firstPathSegment(effectivePrefix)]));
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
let translationBinding = null;
|
|
615
|
-
if (callerConfig.translationFunction === "destructured-t") {
|
|
616
|
-
const parentNode = callExpressionPath.parent;
|
|
617
|
-
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isObjectPattern(parentNode.id)) {
|
|
618
|
-
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;
|
|
619
|
-
}
|
|
620
|
-
} else {
|
|
621
|
-
const parentNode = unwrapAwait(babelTypes, callExpressionPath).parent;
|
|
622
|
-
if (babelTypes.isVariableDeclarator(parentNode) && babelTypes.isIdentifier(parentNode.id)) translationBinding = callExpressionPath.scope.getBinding(parentNode.id.name) ?? null;
|
|
623
|
-
}
|
|
624
|
-
if (!translationBinding) {
|
|
625
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
const accessedFields = /* @__PURE__ */ new Set();
|
|
629
|
-
let hasUntrackedUsage = false;
|
|
630
|
-
for (const referencePath of translationBinding.referencePaths) {
|
|
631
|
-
const parentNode = referencePath.parent;
|
|
632
|
-
if (!((babelTypes.isCallExpression(parentNode) || babelTypes.isOptionalCallExpression(parentNode)) && parentNode.callee === referencePath.node)) {
|
|
633
|
-
hasUntrackedUsage = true;
|
|
634
|
-
break;
|
|
635
|
-
}
|
|
636
|
-
const firstArgument = parentNode.arguments[0];
|
|
637
|
-
const segment = readStaticFirstSegment(babelTypes, firstArgument);
|
|
638
|
-
if (segment === void 0) {
|
|
639
|
-
hasUntrackedUsage = true;
|
|
640
|
-
break;
|
|
641
|
-
}
|
|
642
|
-
accessedFields.add(segment);
|
|
643
|
-
}
|
|
644
|
-
if (hasUntrackedUsage) {
|
|
645
|
-
recordFieldUsage(pruneContext, dictionaryKey, "all");
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
if (accessedFields.size > 0) recordFieldUsage(pruneContext, dictionaryKey, accessedFields);
|
|
649
|
-
};
|
|
650
13
|
/**
|
|
651
14
|
* Creates a Babel plugin that traverses source files and records which
|
|
652
|
-
* top-level dictionary fields each `useIntlayer` / `getIntlayer` call-site
|
|
653
|
-
*
|
|
654
|
-
*
|
|
15
|
+
* top-level dictionary fields each `useIntlayer` / `getIntlayer` call-site
|
|
16
|
+
* accesses. Results are accumulated into `pruneContext`.
|
|
17
|
+
*
|
|
18
|
+
* Compat-adapter callers (`useTranslation`, `useTranslations`, `formatMessage`,
|
|
19
|
+
* …) are *not* known to this plugin. Each compat package's bundler plugin
|
|
20
|
+
* injects its own descriptors through `options.compatCallers`, and the whole
|
|
21
|
+
* compat analysis then runs behind {@link createCompatUsageAnalyzer}. With no
|
|
22
|
+
* descriptors injected — the default — none of that code is reached, so a
|
|
23
|
+
* plain intlayer build behaves exactly as if the adapters did not exist.
|
|
655
24
|
*
|
|
656
25
|
* This plugin is analysis-only: it does not transform the code (`code: false`
|
|
657
26
|
* should be passed to `transformAsync` when using it).
|
|
658
27
|
*
|
|
659
28
|
* @param pruneContext - Shared mutable state written by this plugin.
|
|
660
|
-
* @param options - Optional overrides. `compatCallers` defaults to
|
|
661
|
-
*
|
|
662
|
-
* compat-adapter analysis entirely.
|
|
29
|
+
* @param options - Optional overrides. `compatCallers` defaults to an
|
|
30
|
+
* empty registry, disabling compat-adapter analysis.
|
|
663
31
|
*/
|
|
664
32
|
const makeUsageAnalyzerBabelPlugin = (pruneContext, options) => ({ types: babelTypes }) => {
|
|
665
|
-
const compatCallers = options?.compatCallers ??
|
|
33
|
+
const compatCallers = options?.compatCallers ?? [];
|
|
666
34
|
return {
|
|
667
35
|
name: "intlayer-usage-analyzer",
|
|
668
36
|
visitor: { Program: { exit: (programPath, state) => {
|
|
669
|
-
const
|
|
670
|
-
const isSfcFile =
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
37
|
+
const sourceFilePath = state.file.opts.filename ?? "unknown file";
|
|
38
|
+
const isSfcFile = sourceFilePath.endsWith(".vue") || sourceFilePath.endsWith(".svelte") || sourceFilePath.endsWith(".astro");
|
|
39
|
+
const compatAnalyzer = createCompatUsageAnalyzer(babelTypes, pruneContext, compatCallers, {
|
|
40
|
+
sourceFilePath,
|
|
41
|
+
isSfcFile
|
|
42
|
+
});
|
|
43
|
+
const nativeCallerLocalNames = /* @__PURE__ */ new Set();
|
|
44
|
+
for (const statement of programPath.node.body) {
|
|
45
|
+
if (!babelTypes.isImportDeclaration(statement)) continue;
|
|
46
|
+
const importSource = statement.source.value;
|
|
47
|
+
for (const importSpecifier of statement.specifiers) {
|
|
678
48
|
if (!babelTypes.isImportSpecifier(importSpecifier)) continue;
|
|
679
49
|
const importedName = babelTypes.isIdentifier(importSpecifier.imported) ? importSpecifier.imported.name : importSpecifier.imported.value;
|
|
680
|
-
|
|
681
|
-
|
|
50
|
+
const localName = importSpecifier.local.name;
|
|
51
|
+
if (NATIVE_CALLER_NAME_SET.has(importedName)) {
|
|
52
|
+
nativeCallerLocalNames.add(localName);
|
|
682
53
|
continue;
|
|
683
54
|
}
|
|
684
|
-
|
|
685
|
-
if (compatCaller) compatCallerLocalNameMap.set(importSpecifier.local.name, compatCaller);
|
|
55
|
+
compatAnalyzer?.noteImport(importSource, importedName, localName);
|
|
686
56
|
}
|
|
687
|
-
}
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
const hasCompatCallers = compatCallerLocalNameMap.size > 0 || methodCompatCallers.length > 0;
|
|
691
|
-
if (!hasNativeCallers && !hasCompatCallers) return;
|
|
57
|
+
}
|
|
58
|
+
const hasCompatCallers = compatAnalyzer?.hasMatchableCallers() ?? false;
|
|
59
|
+
if (nativeCallerLocalNames.size === 0 && !hasCompatCallers) return;
|
|
692
60
|
programPath.traverse({
|
|
693
61
|
CallExpression: (callExpressionPath) => {
|
|
694
62
|
const calleeNode = callExpressionPath.node.callee;
|
|
@@ -700,37 +68,18 @@ const makeUsageAnalyzerBabelPlugin = (pruneContext, options) => ({ types: babelT
|
|
|
700
68
|
isMethodCall = true;
|
|
701
69
|
}
|
|
702
70
|
if (!localCallerName) return;
|
|
703
|
-
if (
|
|
71
|
+
if (nativeCallerLocalNames.has(localCallerName)) {
|
|
704
72
|
const callArguments = callExpressionPath.node.arguments;
|
|
705
73
|
if (callArguments.length === 0) return;
|
|
706
74
|
const dictionaryKey = readStaticString(babelTypes, callArguments[0]);
|
|
707
75
|
if (!dictionaryKey) return;
|
|
708
|
-
analyzeCallExpressionUsage(babelTypes, pruneContext, callExpressionPath, dictionaryKey,
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
const importedCompatCaller = compatCallerLocalNameMap.get(localCallerName);
|
|
712
|
-
if (importedCompatCaller && !isMethodCall) {
|
|
713
|
-
analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath.node.arguments, importedCompatCaller, isSfcFile, currentSourceFilePath, callExpressionPath);
|
|
76
|
+
analyzeCallExpressionUsage(babelTypes, pruneContext, callExpressionPath, dictionaryKey, sourceFilePath, isSfcFile);
|
|
714
77
|
return;
|
|
715
78
|
}
|
|
716
|
-
|
|
717
|
-
const methodCaller = methodCompatCallers.find((caller) => caller.callerName === localCallerName);
|
|
718
|
-
if (methodCaller) analyzeNamespaceCallerUsage(babelTypes, pruneContext, callExpressionPath.node.arguments, methodCaller, isSfcFile, currentSourceFilePath, callExpressionPath);
|
|
719
|
-
}
|
|
79
|
+
compatAnalyzer?.analyzeCall(callExpressionPath, localCallerName, isMethodCall);
|
|
720
80
|
},
|
|
721
81
|
JSXOpeningElement: (jsxOpeningElementPath) => {
|
|
722
|
-
|
|
723
|
-
if (!babelTypes.isJSXIdentifier(nameNode)) return;
|
|
724
|
-
const jsxCaller = compatCallerLocalNameMap.get(nameNode.name);
|
|
725
|
-
if (!jsxCaller?.jsxIdAttribute) return;
|
|
726
|
-
const idNode = readJsxAttributeString(babelTypes, jsxOpeningElementPath.node, jsxCaller.jsxIdAttribute);
|
|
727
|
-
let namespaceOverride;
|
|
728
|
-
if (jsxCaller.jsxNamespaceAttribute) {
|
|
729
|
-
const namespaceNode = readJsxAttributeString(babelTypes, jsxOpeningElementPath.node, jsxCaller.jsxNamespaceAttribute);
|
|
730
|
-
if (!namespaceNode) return;
|
|
731
|
-
namespaceOverride = namespaceNode.value;
|
|
732
|
-
}
|
|
733
|
-
analyzeNamespaceCallerUsage(babelTypes, pruneContext, idNode ? [idNode] : [], jsxCaller, isSfcFile, currentSourceFilePath, void 0, namespaceOverride);
|
|
82
|
+
compatAnalyzer?.analyzeJsxElement(jsxOpeningElementPath);
|
|
734
83
|
}
|
|
735
84
|
});
|
|
736
85
|
} } }
|
|
@@ -738,5 +87,5 @@ const makeUsageAnalyzerBabelPlugin = (pruneContext, options) => ({ types: babelT
|
|
|
738
87
|
};
|
|
739
88
|
|
|
740
89
|
//#endregion
|
|
741
|
-
export { CHAINABLE_RUNTIME_METHOD_NAMES, DEFAULT_COMPAT_CALLERS, INTLAYER_CALLER_NAMES, createPruneContext, makeUsageAnalyzerBabelPlugin, preserveNestedDictionaryFields, unwrapAwait };
|
|
90
|
+
export { CHAINABLE_RUNTIME_METHOD_NAMES, DEFAULT_COMPAT_CALLERS, INTLAYER_CALLER_NAMES, NATIVE_CALLER_NAME_SET, createPruneContext, makeUsageAnalyzerBabelPlugin, preserveNestedDictionaryFields, unwrapAwait };
|
|
742
91
|
//# sourceMappingURL=babel-plugin-intlayer-usage-analyzer.mjs.map
|