@menapo/vi18n 0.0.0-stage → 0.1.0
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/LICENSE +49 -0
- package/README.md +110 -2
- package/dist/apply-Bre-lY9J.js +228 -0
- package/dist/captured-Bj8bO6vd.js +1034 -0
- package/dist/check-D34P-S--.js +1 -0
- package/dist/check-DyrAbv73.js +201 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +41 -0
- package/dist/codec-DuvU2VQy.js +207 -0
- package/dist/collect-Cx4_SKOe.js +13 -0
- package/dist/collisions-CfpZU8e4.js +217 -0
- package/dist/compile-B5Mz6WJm.js +245 -0
- package/dist/compile-Cc97JaW2.js +2502 -0
- package/dist/concurrency-C9UUWuDC.js +96 -0
- package/dist/concurrency-DONL7t4V.js +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +39 -0
- package/dist/dir-zw6bGMKQ.js +290 -0
- package/dist/duplicates-yuRljQ6h.js +185 -0
- package/dist/engine-B7p40M__.js +531 -0
- package/dist/engine.d.ts +43 -0
- package/dist/engine.js +1241 -0
- package/dist/exclude-Du-Zd_qI.js +26 -0
- package/dist/export-DGsMbcTZ.js +103 -0
- package/dist/extract-Dujlsgnk.js +298 -0
- package/dist/formats-KPIg-08V.js +781 -0
- package/dist/i18next-CrUnKFfh.js +1596 -0
- package/dist/i18next-Rgo-v9l0.js +1 -0
- package/dist/import-Cwdhcywt.js +225 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +239 -0
- package/dist/init-BecCTvKi.js +792 -0
- package/dist/library-D5CWvTQs.js +1882 -0
- package/dist/lingui--PcOOYry.js +833 -0
- package/dist/lint.d.ts +105 -0
- package/dist/lint.js +971 -0
- package/dist/load-CQRz4PpM.js +2950 -0
- package/dist/load-gW-oJt1Q.js +747 -0
- package/dist/locales-DX4MOBFE.js +827 -0
- package/dist/lsp-Cq1Nuu-Q.js +31 -0
- package/dist/match-BGr8EXB3.js +89 -0
- package/dist/mcp-Cf0T7qPW.js +28 -0
- package/dist/merge-driver-CpiiSVIc.js +422 -0
- package/dist/messages-Cxa-5tR0.js +166 -0
- package/dist/migrate-CntqvrW4.js +1 -0
- package/dist/migrate-dPgq9uzy.js +1484 -0
- package/dist/openai-CTaMDcl-.js +346 -0
- package/dist/output-DsaHKHEt.js +164 -0
- package/dist/parse.d.ts +62 -0
- package/dist/parse.js +156 -0
- package/dist/pipeline-C7g11THL.js +2031 -0
- package/dist/pipeline-wxS0SApG.js +1 -0
- package/dist/project-Dwx7iPm6.js +381 -0
- package/dist/project-pXipT8iG.js +1046 -0
- package/dist/pseudo-DRv30iN5.js +145 -0
- package/dist/raw-C4Y6vyGY.js +4708 -0
- package/dist/react-intl-CaBVwJ9u.js +519 -0
- package/dist/restore-Btq66Tid.js +175 -0
- package/dist/rewrite-CiTRAICl.js +803 -0
- package/dist/rich-DCUKl5HM.js +97 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/run-DYD_VXKy.js +694 -0
- package/dist/runtime-DTMmliT1.js +64 -0
- package/dist/scan-worker.d.ts +62 -0
- package/dist/scan-worker.js +1 -0
- package/dist/scoped-BMYpSthj.js +44 -0
- package/dist/server-BDkykMjb.js +11823 -0
- package/dist/server-qv3mz70d.js +10485 -0
- package/dist/server.d.ts +169 -0
- package/dist/server.js +1234 -0
- package/dist/sitemap-DV3o8QD7.js +159 -0
- package/dist/sources-D19czDSA.js +2242 -0
- package/dist/status-DZArW5aE.js +149 -0
- package/dist/stdio-CVaJfxA7.js +29 -0
- package/dist/stdio-FPOKQAgu.js +95 -0
- package/dist/sync-CcVzger_.js +658 -0
- package/dist/t-DC-c9zE5.js +454 -0
- package/dist/test.d.ts +116 -0
- package/dist/test.js +319 -0
- package/dist/translate-DZR3MkSA.js +299 -0
- package/dist/ts-BaU5yOf0.js +4863 -0
- package/dist/types-CgUCkFU1.d.ts +707 -0
- package/dist/types-D3QGM8XO.js +146 -0
- package/dist/types-DsdWBG0b.d.ts +558 -0
- package/dist/types-DtvQ7UdD.js +1948 -0
- package/dist/types-wSh9fCBY.js +11 -0
- package/dist/units-CQ5Q99RC.js +150 -0
- package/dist/url-CRfCOhUI.js +172 -0
- package/dist/url-CsBkRaJy.js +225 -0
- package/dist/validate-BxpupdAf.js +2615 -0
- package/dist/validate-D6DJepZv.js +365 -0
- package/dist/version-BlyLGlOa.js +28 -0
- package/dist/vite-scan-B_YW4X_a.js +242 -0
- package/dist/vite.d.ts +34 -0
- package/dist/vite.js +7558 -0
- package/dist/vue-i18n-CEMnpv7k.js +651 -0
- package/dist/which-C6PFI1Dm.js +116 -0
- package/dist/worker-84U6uYB3.js +1656 -0
- package/package.json +97 -3
- package/schemas/catalog.json +57 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
import { a as isIgnored, c as unwrap, d as listImports, o as markersOf, s as keyName } from "./sources-D19czDSA.js";
|
|
2
|
+
//#region src/core/extract/hooks.ts
|
|
3
|
+
/** `HELPER.useT` in `src/vite/types.ts`; core does not import the plugin layer. */
|
|
4
|
+
const USE_T = "__vi18nUseT";
|
|
5
|
+
/** `VIRTUAL.glue` in `src/vite/types.ts`. */
|
|
6
|
+
const GLUE = "virtual:vi18n/glue/";
|
|
7
|
+
/** Preferred local of the `__vi18nUseT` import: hook-shaped, see the module comment. */
|
|
8
|
+
const USE_T_LOCAL = "useVi18nT";
|
|
9
|
+
/** `HELPER.hookLocal` in `src/vite/types.ts`. */
|
|
10
|
+
const HOOK_LOCAL = "__t";
|
|
11
|
+
/** React Compiler's opt-out directives, which also opt out of the hook (PLAN §5.15). */
|
|
12
|
+
const NO_MEMO = /* @__PURE__ */ new Set(["use no memo", "use no forget"]);
|
|
13
|
+
const NO_HOOK_COMMENT = "vi18n-no-hook";
|
|
14
|
+
/** Modules whose `createElement`/`jsx` calls are compiled JSX (code that reached the pass already compiled). */
|
|
15
|
+
const JSX_MODULES = /* @__PURE__ */ new Set([
|
|
16
|
+
"react",
|
|
17
|
+
"react/jsx-runtime",
|
|
18
|
+
"react/jsx-dev-runtime",
|
|
19
|
+
"preact",
|
|
20
|
+
"preact/compat",
|
|
21
|
+
"preact/jsx-runtime",
|
|
22
|
+
"preact/jsx-dev-runtime"
|
|
23
|
+
]);
|
|
24
|
+
const JSX_FACTORIES = /* @__PURE__ */ new Set([
|
|
25
|
+
"jsx",
|
|
26
|
+
"jsxs",
|
|
27
|
+
"jsxDEV",
|
|
28
|
+
"createElement",
|
|
29
|
+
"h"
|
|
30
|
+
]);
|
|
31
|
+
/** Wrappers whose function argument is still the component (`memo(function Card() {…})`). */
|
|
32
|
+
const COMPONENT_WRAPPERS = /* @__PURE__ */ new Set(["memo", "forwardRef"]);
|
|
33
|
+
/**
|
|
34
|
+
* Array methods that call their callback as a plain function: a component
|
|
35
|
+
* passed to them (`rows.map(Row)`) is not rendered as an element.
|
|
36
|
+
*/
|
|
37
|
+
const PLAIN_CALLERS = /* @__PURE__ */ new Set([
|
|
38
|
+
"call",
|
|
39
|
+
"apply",
|
|
40
|
+
"map",
|
|
41
|
+
"flatMap",
|
|
42
|
+
"forEach",
|
|
43
|
+
"filter",
|
|
44
|
+
"find",
|
|
45
|
+
"findIndex",
|
|
46
|
+
"findLast",
|
|
47
|
+
"findLastIndex",
|
|
48
|
+
"some",
|
|
49
|
+
"every",
|
|
50
|
+
"reduce",
|
|
51
|
+
"reduceRight",
|
|
52
|
+
"from"
|
|
53
|
+
]);
|
|
54
|
+
/** Keys that hold only TypeScript types; nothing under them runs. */
|
|
55
|
+
const TYPE_KEYS$1 = /* @__PURE__ */ new Set([
|
|
56
|
+
"typeAnnotation",
|
|
57
|
+
"typeParameters",
|
|
58
|
+
"typeArguments",
|
|
59
|
+
"superTypeArguments",
|
|
60
|
+
"returnType",
|
|
61
|
+
"implements"
|
|
62
|
+
]);
|
|
63
|
+
/** `rich`-like members of a namespace import; `fmt` is an object whose members are called. */
|
|
64
|
+
const NAMESPACE_TRACKED = /* @__PURE__ */ new Set([
|
|
65
|
+
"rich",
|
|
66
|
+
"richHtml",
|
|
67
|
+
"richParts"
|
|
68
|
+
]);
|
|
69
|
+
/** Function methods: `rich.call(…)` is already the tracked shape, never a member to wrap. */
|
|
70
|
+
const FUNCTION_METHODS = /* @__PURE__ */ new Set([
|
|
71
|
+
"call",
|
|
72
|
+
"apply",
|
|
73
|
+
"bind"
|
|
74
|
+
]);
|
|
75
|
+
function isHookName(name) {
|
|
76
|
+
if (name.length < 4 || !name.startsWith("use")) return false;
|
|
77
|
+
const c = name.charCodeAt(3);
|
|
78
|
+
return c >= 65 && c <= 90 || c >= 48 && c <= 57;
|
|
79
|
+
}
|
|
80
|
+
function isComponentName(name) {
|
|
81
|
+
const c = name.charCodeAt(0);
|
|
82
|
+
return c >= 65 && c <= 90;
|
|
83
|
+
}
|
|
84
|
+
function isFunction$1(node) {
|
|
85
|
+
return node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
|
|
86
|
+
}
|
|
87
|
+
function isIdentifierChar(c) {
|
|
88
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 36 || c === 95 || c > 127;
|
|
89
|
+
}
|
|
90
|
+
/** True when `name` occurs in `code` as a whole identifier-like word (strings and comments included: a false hit only renames). */
|
|
91
|
+
function mentions(code, name) {
|
|
92
|
+
for (let at = code.indexOf(name); at !== -1; at = code.indexOf(name, at + 1)) if ((at === 0 || !isIdentifierChar(code.charCodeAt(at - 1))) && (at + name.length === code.length || !isIdentifierChar(code.charCodeAt(at + name.length)))) return true;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
function freeName(code, base) {
|
|
96
|
+
if (!mentions(code, base)) return base;
|
|
97
|
+
for (let i = 2;; i++) if (!mentions(code, base + i)) return base + i;
|
|
98
|
+
}
|
|
99
|
+
/** Offset of the first character after whitespace and comments, from `at`, never past `limit`. */
|
|
100
|
+
function skipTrivia(code, at, limit) {
|
|
101
|
+
let i = at;
|
|
102
|
+
while (i < limit) {
|
|
103
|
+
const c = code.charCodeAt(i);
|
|
104
|
+
if (c === 47 && code.charCodeAt(i + 1) === 42) {
|
|
105
|
+
const close = code.indexOf("*/", i + 2);
|
|
106
|
+
i = close === -1 ? limit : close + 2;
|
|
107
|
+
} else if (c === 47 && code.charCodeAt(i + 1) === 47) {
|
|
108
|
+
const close = code.indexOf("\n", i + 2);
|
|
109
|
+
i = close === -1 ? limit : close + 1;
|
|
110
|
+
} else if (c === 32 || c === 9 || c === 10 || c === 13 || c === 160 || c === 65279) i++;
|
|
111
|
+
else if (c === 8232 || c === 8233 || c > 127 && /\s/.test(code[i])) i++;
|
|
112
|
+
else break;
|
|
113
|
+
}
|
|
114
|
+
return i;
|
|
115
|
+
}
|
|
116
|
+
/** True when a comment in `code[from, to)` contains `vi18n-no-hook`. */
|
|
117
|
+
function optedOut(code, from, to) {
|
|
118
|
+
const region = code.slice(from, to);
|
|
119
|
+
let at = region.indexOf(NO_HOOK_COMMENT);
|
|
120
|
+
while (at !== -1) {
|
|
121
|
+
const open = region.lastIndexOf("/*", at);
|
|
122
|
+
if (open !== -1 && region.indexOf("*/", open + 2) >= at + 13) return true;
|
|
123
|
+
const line = region.lastIndexOf("\n", at) + 1;
|
|
124
|
+
if (region.slice(line, at).includes("//")) return true;
|
|
125
|
+
at = region.indexOf(NO_HOOK_COMMENT, at + 1);
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
/** The leading directives of a statement list (`"use client"`, `"use no memo"`). */
|
|
130
|
+
function directives(statements) {
|
|
131
|
+
let n = 0;
|
|
132
|
+
while (n < statements.length && typeof statements[n].directive === "string") n++;
|
|
133
|
+
return statements.slice(0, n);
|
|
134
|
+
}
|
|
135
|
+
function hasNoMemo(statements) {
|
|
136
|
+
for (const d of directives(statements)) if (NO_MEMO.has(d.directive)) return true;
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
/** The function a wrapper call (`memo(…)`, `React.forwardRef(…)`, nested) wraps, if any. */
|
|
140
|
+
function wrappedFunction(init) {
|
|
141
|
+
let node = unwrap(init);
|
|
142
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
143
|
+
if (isFunction$1(node)) return node;
|
|
144
|
+
if (node.type !== "CallExpression") return null;
|
|
145
|
+
const callee = unwrap(node.callee);
|
|
146
|
+
const name = callee.type === "Identifier" ? callee.name : callee.type === "MemberExpression" ? keyName(callee.property, callee.computed === true) : null;
|
|
147
|
+
if (name === null || !COMPONENT_WRAPPERS.has(name)) return null;
|
|
148
|
+
const first = node.arguments[0];
|
|
149
|
+
if (first === void 0 || first.type === "SpreadElement") return null;
|
|
150
|
+
node = unwrap(first);
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Named functions declared at the top level: function declarations
|
|
156
|
+
* (exported or not), `const X = <function>` and `const X = memo(<function>)`
|
|
157
|
+
* (also `forwardRef`, nested), `export default memo(function X() {…})`, and
|
|
158
|
+
* anonymous default exports (named `"default"`, flagged `anonymous`).
|
|
159
|
+
* `regionStart` is where comments leading the declaration may start (the end
|
|
160
|
+
* of the previous statement or declarator). Shared with `captured.ts`, which
|
|
161
|
+
* finds Solid components the same way.
|
|
162
|
+
*/
|
|
163
|
+
function topLevelFunctions(program) {
|
|
164
|
+
const out = [];
|
|
165
|
+
let previousEnd = 0;
|
|
166
|
+
for (const statement of program.body) {
|
|
167
|
+
const regionStart = previousEnd;
|
|
168
|
+
previousEnd = statement.end;
|
|
169
|
+
let node = statement;
|
|
170
|
+
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
171
|
+
const declaration = node.declaration;
|
|
172
|
+
if (declaration === null) continue;
|
|
173
|
+
node = declaration;
|
|
174
|
+
}
|
|
175
|
+
switch (node.type) {
|
|
176
|
+
case "FunctionDeclaration": {
|
|
177
|
+
const id = node.id;
|
|
178
|
+
if (node.body === null) break;
|
|
179
|
+
if (id !== null) out.push({
|
|
180
|
+
name: id.name,
|
|
181
|
+
fn: node,
|
|
182
|
+
regionStart
|
|
183
|
+
});
|
|
184
|
+
else out.push({
|
|
185
|
+
name: "default",
|
|
186
|
+
anonymous: true,
|
|
187
|
+
fn: node,
|
|
188
|
+
regionStart
|
|
189
|
+
});
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "VariableDeclaration": {
|
|
193
|
+
let start = regionStart;
|
|
194
|
+
for (const declarator of node.declarations) {
|
|
195
|
+
const id = declarator.id;
|
|
196
|
+
const init = declarator.init;
|
|
197
|
+
const fn = init === null ? null : wrappedFunction(init);
|
|
198
|
+
if (id.type === "Identifier" && fn !== null) out.push({
|
|
199
|
+
name: id.name,
|
|
200
|
+
fn,
|
|
201
|
+
regionStart: start
|
|
202
|
+
});
|
|
203
|
+
start = declarator.end;
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
default: {
|
|
208
|
+
if (statement.type !== "ExportDefaultDeclaration") break;
|
|
209
|
+
const fn = wrappedFunction(node);
|
|
210
|
+
if (fn === null) break;
|
|
211
|
+
const id = fn.type === "FunctionExpression" ? fn.id : null;
|
|
212
|
+
if (id !== null) out.push({
|
|
213
|
+
name: id.name,
|
|
214
|
+
fn,
|
|
215
|
+
regionStart
|
|
216
|
+
});
|
|
217
|
+
else out.push({
|
|
218
|
+
name: "default",
|
|
219
|
+
anonymous: true,
|
|
220
|
+
fn,
|
|
221
|
+
regionStart
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Top-level components (PascalCase, or an anonymous default export, which
|
|
230
|
+
* route files commonly are) and custom hooks (`use*`) that may hold a hook.
|
|
231
|
+
*/
|
|
232
|
+
function candidates(program) {
|
|
233
|
+
return topLevelFunctions(program).filter(({ name, fn, anonymous }) => (anonymous === true || isComponentName(name) || isHookName(name)) && fn.async !== true && fn.generator !== true);
|
|
234
|
+
}
|
|
235
|
+
/** Records which watched names a binding pattern declares in `scope`. */
|
|
236
|
+
function declare(pattern, scope, watched) {
|
|
237
|
+
if (pattern === null) return;
|
|
238
|
+
switch (pattern.type) {
|
|
239
|
+
case "Identifier":
|
|
240
|
+
if (watched.has(pattern.name)) (scope.shadowed ??= /* @__PURE__ */ new Set()).add(pattern.name);
|
|
241
|
+
return;
|
|
242
|
+
case "ObjectPattern":
|
|
243
|
+
for (const property of pattern.properties) declare(property.type === "RestElement" ? property.argument : property.value, scope, watched);
|
|
244
|
+
return;
|
|
245
|
+
case "ArrayPattern":
|
|
246
|
+
for (const element of pattern.elements) declare(element, scope, watched);
|
|
247
|
+
return;
|
|
248
|
+
case "AssignmentPattern":
|
|
249
|
+
declare(pattern.left, scope, watched);
|
|
250
|
+
return;
|
|
251
|
+
case "RestElement":
|
|
252
|
+
declare(pattern.argument, scope, watched);
|
|
253
|
+
return;
|
|
254
|
+
case "TSParameterProperty":
|
|
255
|
+
declare(pattern.parameter, scope, watched);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** `onClick`, `onChange`…: an event handler prop. */
|
|
260
|
+
function isHandlerName(name) {
|
|
261
|
+
if (name === null || name.length < 3 || !name.startsWith("on")) return false;
|
|
262
|
+
const c = name.charCodeAt(2);
|
|
263
|
+
return c >= 65 && c <= 90;
|
|
264
|
+
}
|
|
265
|
+
/** The name of the property a member callee reads, or null. */
|
|
266
|
+
function memberName(node) {
|
|
267
|
+
return node.type === "MemberExpression" ? keyName(node.property, node.computed === true) : null;
|
|
268
|
+
}
|
|
269
|
+
/** What a call to `callee` is to the hook pass: a locale call, or null. */
|
|
270
|
+
function classify(call, context) {
|
|
271
|
+
const callee = unwrap(call.callee);
|
|
272
|
+
if (callee.type === "Identifier") {
|
|
273
|
+
const name = callee.name;
|
|
274
|
+
if (context.tNames.has(name)) return {
|
|
275
|
+
kind: "t",
|
|
276
|
+
call,
|
|
277
|
+
callee,
|
|
278
|
+
root: name
|
|
279
|
+
};
|
|
280
|
+
if (context.trackNames.has(name)) return {
|
|
281
|
+
kind: "track",
|
|
282
|
+
call,
|
|
283
|
+
callee,
|
|
284
|
+
root: name
|
|
285
|
+
};
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const method = memberName(callee);
|
|
289
|
+
if (method === null || FUNCTION_METHODS.has(method)) return null;
|
|
290
|
+
const object = unwrap(callee.object);
|
|
291
|
+
if (object.type === "Identifier") {
|
|
292
|
+
const name = object.name;
|
|
293
|
+
if (context.namespaces.has(name)) {
|
|
294
|
+
if (method === "t") return {
|
|
295
|
+
kind: "t",
|
|
296
|
+
call,
|
|
297
|
+
callee,
|
|
298
|
+
root: name
|
|
299
|
+
};
|
|
300
|
+
if (NAMESPACE_TRACKED.has(method)) return {
|
|
301
|
+
kind: "track",
|
|
302
|
+
call,
|
|
303
|
+
callee,
|
|
304
|
+
root: name
|
|
305
|
+
};
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
if (context.trackNames.has(name)) return {
|
|
309
|
+
kind: "track",
|
|
310
|
+
call,
|
|
311
|
+
callee,
|
|
312
|
+
root: name
|
|
313
|
+
};
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (memberName(object) === "fmt") {
|
|
317
|
+
const owner = unwrap(object.object);
|
|
318
|
+
if (owner.type === "Identifier" && context.namespaces.has(owner.name)) return {
|
|
319
|
+
kind: "track",
|
|
320
|
+
call,
|
|
321
|
+
callee,
|
|
322
|
+
root: owner.name
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
/** True when `node` calls a hook (`useX(…)`, `React.useX(…)`), not counting the injected one. */
|
|
328
|
+
function isHookCall(node, context) {
|
|
329
|
+
const callee = unwrap(node.callee);
|
|
330
|
+
if (callee.type === "Identifier") {
|
|
331
|
+
const name = callee.name;
|
|
332
|
+
return name !== context.useTLocal && isHookName(name);
|
|
333
|
+
}
|
|
334
|
+
const name = memberName(callee);
|
|
335
|
+
return name !== null && isHookName(name);
|
|
336
|
+
}
|
|
337
|
+
/** Walks every child node of `node` (types excluded). */
|
|
338
|
+
function visitChildren(node, scope, analysis, context) {
|
|
339
|
+
for (const key in node) {
|
|
340
|
+
if (key === "type" || key === "start" || key === "end" || TYPE_KEYS$1.has(key)) continue;
|
|
341
|
+
const value = node[key];
|
|
342
|
+
if (value === null || typeof value !== "object") continue;
|
|
343
|
+
if (Array.isArray(value)) {
|
|
344
|
+
for (const item of value) if (item !== null && typeof item.type === "string") visit(item, scope, analysis, context);
|
|
345
|
+
} else if (typeof value.type === "string") visit(value, scope, analysis, context);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** A nested function: its own scope, whose calls run during render unless shadowed. */
|
|
349
|
+
function visitNested(fn, scope, analysis, context) {
|
|
350
|
+
const inner = {
|
|
351
|
+
calls: [],
|
|
352
|
+
shadowed: null,
|
|
353
|
+
children: []
|
|
354
|
+
};
|
|
355
|
+
scope.children.push(inner);
|
|
356
|
+
if (fn.type === "FunctionExpression") declare(fn.id, inner, context.watched);
|
|
357
|
+
for (const param of fn.params) {
|
|
358
|
+
declare(param, inner, context.watched);
|
|
359
|
+
visit(param, inner, analysis, context);
|
|
360
|
+
}
|
|
361
|
+
const body = fn.body;
|
|
362
|
+
if (body !== null) visit(body, inner, analysis, context);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Walks what runs while the function renders: its own body and the nested
|
|
366
|
+
* functions it may call during render. Classes are skipped, and so are
|
|
367
|
+
* functions handed to hooks and inline `on*` handlers, which run after
|
|
368
|
+
* render and keep the global `t` (see the module comment).
|
|
369
|
+
*/
|
|
370
|
+
function visit(node, scope, analysis, context) {
|
|
371
|
+
switch (node.type) {
|
|
372
|
+
case "FunctionDeclaration":
|
|
373
|
+
declare(node.id, scope, context.watched);
|
|
374
|
+
visitNested(node, scope, analysis, context);
|
|
375
|
+
return;
|
|
376
|
+
case "FunctionExpression":
|
|
377
|
+
case "ArrowFunctionExpression":
|
|
378
|
+
visitNested(node, scope, analysis, context);
|
|
379
|
+
return;
|
|
380
|
+
case "ClassDeclaration":
|
|
381
|
+
declare(node.id, scope, context.watched);
|
|
382
|
+
return;
|
|
383
|
+
case "ClassExpression": return;
|
|
384
|
+
case "JSXElement":
|
|
385
|
+
case "JSXFragment":
|
|
386
|
+
analysis.jsx = true;
|
|
387
|
+
break;
|
|
388
|
+
case "JSXAttribute": {
|
|
389
|
+
const name = node.name;
|
|
390
|
+
const value = node.value;
|
|
391
|
+
if (name.type === "JSXIdentifier" && isHandlerName(name.name) && value?.type === "JSXExpressionContainer" && isFunction$1(unwrap(value.expression))) return;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case "Property":
|
|
395
|
+
if (isHandlerName(keyName(node.key, node.computed === true)) && isFunction$1(unwrap(node.value))) return;
|
|
396
|
+
break;
|
|
397
|
+
case "VariableDeclarator":
|
|
398
|
+
declare(node.id, scope, context.watched);
|
|
399
|
+
break;
|
|
400
|
+
case "CatchClause":
|
|
401
|
+
declare(node.param, scope, context.watched);
|
|
402
|
+
break;
|
|
403
|
+
case "CallExpression": {
|
|
404
|
+
const found = classify(node, context);
|
|
405
|
+
if (found !== null) scope.calls.push(found);
|
|
406
|
+
const callee = unwrap(node.callee);
|
|
407
|
+
if (callee.type === "Identifier" && context.factories.has(callee.name)) analysis.jsx = true;
|
|
408
|
+
else if (JSX_FACTORIES.has(memberName(callee) ?? "")) {
|
|
409
|
+
const object = unwrap(callee.object);
|
|
410
|
+
if (object.type === "Identifier" && context.reactNamespaces.has(object.name)) analysis.jsx = true;
|
|
411
|
+
}
|
|
412
|
+
if (isHookCall(node, context)) {
|
|
413
|
+
if (scope === analysis.root) analysis.hooks = true;
|
|
414
|
+
visit(node.callee, scope, analysis, context);
|
|
415
|
+
for (const argument of node.arguments) if (!isFunction$1(unwrap(argument))) visit(argument, scope, analysis, context);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
visitChildren(node, scope, analysis, context);
|
|
422
|
+
}
|
|
423
|
+
/** The calls of `scope` and its nested functions that still reach the imports. */
|
|
424
|
+
function liveCalls(scope, inherited, out) {
|
|
425
|
+
let blocked = inherited;
|
|
426
|
+
if (scope.shadowed !== null) blocked = inherited === null ? scope.shadowed : /* @__PURE__ */ new Set([...inherited, ...scope.shadowed]);
|
|
427
|
+
for (const call of scope.calls) if (blocked === null || !blocked.has(call.root)) out.push(call);
|
|
428
|
+
for (const child of scope.children) liveCalls(child, blocked, out);
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Names the module calls as plain functions (`Row()`, `Row.call(…)`,
|
|
433
|
+
* `rows.map(Row)`), anywhere. A PascalCase function used that way is not
|
|
434
|
+
* rendered as an element, so a hook in it would run conditionally or a
|
|
435
|
+
* variable number of times (S13's `{cond && Row()}`).
|
|
436
|
+
*/
|
|
437
|
+
function plainlyCalled(program) {
|
|
438
|
+
const names = /* @__PURE__ */ new Set();
|
|
439
|
+
const stack = [program];
|
|
440
|
+
for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
|
|
441
|
+
if (node.type === "CallExpression") {
|
|
442
|
+
const callee = unwrap(node.callee);
|
|
443
|
+
if (callee.type === "Identifier") names.add(callee.name);
|
|
444
|
+
else if (callee.type === "MemberExpression") {
|
|
445
|
+
const method = keyName(callee.property, callee.computed === true);
|
|
446
|
+
if (method !== null && PLAIN_CALLERS.has(method)) {
|
|
447
|
+
const object = unwrap(callee.object);
|
|
448
|
+
if ((method === "call" || method === "apply") && object.type === "Identifier") names.add(object.name);
|
|
449
|
+
for (const argument of node.arguments) {
|
|
450
|
+
const value = unwrap(argument);
|
|
451
|
+
if (value.type === "Identifier") names.add(value.name);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const key in node) {
|
|
457
|
+
if (key === "type" || key === "start" || key === "end" || TYPE_KEYS$1.has(key)) continue;
|
|
458
|
+
const value = node[key];
|
|
459
|
+
if (value === null || typeof value !== "object") continue;
|
|
460
|
+
if (Array.isArray(value)) {
|
|
461
|
+
for (const item of value) if (item !== null && typeof item.type === "string") stack.push(item);
|
|
462
|
+
} else if (typeof value.type === "string") stack.push(value);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return names;
|
|
466
|
+
}
|
|
467
|
+
const NONE = /* @__PURE__ */ new Set();
|
|
468
|
+
function moduleContext(imports, options, glue) {
|
|
469
|
+
const { tNames, trackNames = NONE, namespaces = NONE } = options;
|
|
470
|
+
const context = {
|
|
471
|
+
tNames,
|
|
472
|
+
trackNames,
|
|
473
|
+
namespaces,
|
|
474
|
+
watched: /* @__PURE__ */ new Set([
|
|
475
|
+
...tNames,
|
|
476
|
+
...trackNames,
|
|
477
|
+
...namespaces
|
|
478
|
+
]),
|
|
479
|
+
useTLocal: null,
|
|
480
|
+
factories: /* @__PURE__ */ new Set(),
|
|
481
|
+
reactNamespaces: /* @__PURE__ */ new Set()
|
|
482
|
+
};
|
|
483
|
+
for (const record of imports) if (record.specifier === glue) {
|
|
484
|
+
for (const binding of record.bindings) if (binding.imported === USE_T) context.useTLocal = binding.local;
|
|
485
|
+
} else if (JSX_MODULES.has(record.specifier)) {
|
|
486
|
+
for (const { local, imported } of record.bindings) if (imported === "default" || imported === "*") context.reactNamespaces.add(local);
|
|
487
|
+
else if (JSX_FACTORIES.has(imported)) context.factories.add(local);
|
|
488
|
+
}
|
|
489
|
+
return context;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Where a concise arrow body's expression (or its opening parenthesis)
|
|
493
|
+
* starts: the first significant character after `=>`. -1 when no `=>` is
|
|
494
|
+
* found before the body, which a parsed arrow never allows.
|
|
495
|
+
*/
|
|
496
|
+
function conciseBodyHead(code, fn) {
|
|
497
|
+
const body = fn.body;
|
|
498
|
+
let from = fn.start;
|
|
499
|
+
const params = fn.params;
|
|
500
|
+
if (params.length > 0) from = params[params.length - 1].end;
|
|
501
|
+
for (const key of ["typeParameters", "returnType"]) {
|
|
502
|
+
const typed = fn[key];
|
|
503
|
+
if (typed !== null && typed !== void 0 && typed.end > from) from = typed.end;
|
|
504
|
+
}
|
|
505
|
+
let at = skipTrivia(code, from, body.start);
|
|
506
|
+
while (at < body.start && !code.startsWith("=>", at)) at = skipTrivia(code, at + 1, body.start);
|
|
507
|
+
if (at >= body.start) return -1;
|
|
508
|
+
return skipTrivia(code, at + 2, body.start);
|
|
509
|
+
}
|
|
510
|
+
/** `const <local> = <useT>()` as the first statement after the directives: an earlier pass's hook. */
|
|
511
|
+
function existingHookLocal(statements, useTLocal) {
|
|
512
|
+
if (useTLocal === null) return null;
|
|
513
|
+
const first = statements[directives(statements).length];
|
|
514
|
+
if (first?.type !== "VariableDeclaration") return null;
|
|
515
|
+
const declarations = first.declarations;
|
|
516
|
+
if (declarations.length !== 1) return null;
|
|
517
|
+
const declarator = declarations[0];
|
|
518
|
+
const id = declarator.id;
|
|
519
|
+
const init = declarator.init;
|
|
520
|
+
if (id.type !== "Identifier" || init === null) return null;
|
|
521
|
+
const call = unwrap(init);
|
|
522
|
+
if (call.type !== "CallExpression" || call.arguments.length !== 0) return null;
|
|
523
|
+
const callee = unwrap(call.callee);
|
|
524
|
+
return callee.type === "Identifier" && callee.name === useTLocal ? id.name : null;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Injects the hook into every top-level component (PascalCase, or an
|
|
528
|
+
* anonymous default export) or custom hook (`use*`) that reads the locale
|
|
529
|
+
* while it renders and renders JSX or calls hooks: `const __t = useVi18nT();`
|
|
530
|
+
* becomes its first statement (after directives; concise arrow bodies become
|
|
531
|
+
* blocks), its `t` calls become `__t(…)`, and its `rich`/`fmt` calls
|
|
532
|
+
* `rich.call(__t, …)`. That covers calls in nested functions that run during
|
|
533
|
+
* render, unless the nested function shadows the name; functions passed to
|
|
534
|
+
* hooks and inline `on*` handlers keep the global `t`. Class components,
|
|
535
|
+
* async and generator functions are left alone, as are functions opted out
|
|
536
|
+
* with a `/* vi18n-no-hook *\/` comment before their body or a
|
|
537
|
+
* `"use no memo"` directive (function or module), and PascalCase functions
|
|
538
|
+
* that call no hook but are called as plain functions in this module. Adds
|
|
539
|
+
* `import { __vi18nUseT as useVi18nT } from "virtual:vi18n/glue/<framework>"`
|
|
540
|
+
* once. Idempotent: hooked calls are no longer direct calls of the imports.
|
|
541
|
+
* Returns true when anything was edited.
|
|
542
|
+
*/
|
|
543
|
+
function injectHooks(parsed, sink, options) {
|
|
544
|
+
if (options.tNames.size === 0 && (options.trackNames?.size ?? 0) === 0 && (options.namespaces?.size ?? 0) === 0) return false;
|
|
545
|
+
const program = parsed.program;
|
|
546
|
+
if (hasNoMemo(program.body)) return false;
|
|
547
|
+
const found = candidates(program);
|
|
548
|
+
if (found.length === 0) return false;
|
|
549
|
+
const code = parsed.code;
|
|
550
|
+
const glue = GLUE + options.framework;
|
|
551
|
+
const context = moduleContext(listImports(parsed), options, glue);
|
|
552
|
+
const anyOptOut = code.includes(NO_HOOK_COMMENT);
|
|
553
|
+
let plain = null;
|
|
554
|
+
let useTLocal = context.useTLocal;
|
|
555
|
+
let hookLocal = null;
|
|
556
|
+
let edited = false;
|
|
557
|
+
for (const { name, anonymous, fn, regionStart } of found) {
|
|
558
|
+
const body = fn.body;
|
|
559
|
+
const block = body.type === "BlockStatement";
|
|
560
|
+
const statements = block ? body.body : [];
|
|
561
|
+
if (block && hasNoMemo(statements)) continue;
|
|
562
|
+
const root = {
|
|
563
|
+
calls: [],
|
|
564
|
+
shadowed: null,
|
|
565
|
+
children: []
|
|
566
|
+
};
|
|
567
|
+
const analysis = {
|
|
568
|
+
root,
|
|
569
|
+
jsx: false,
|
|
570
|
+
hooks: false
|
|
571
|
+
};
|
|
572
|
+
for (const param of fn.params) declare(param, root, context.watched);
|
|
573
|
+
visit(body, root, analysis, context);
|
|
574
|
+
const calls = liveCalls(root, null, []);
|
|
575
|
+
if (calls.length === 0 || !(analysis.jsx || analysis.hooks)) continue;
|
|
576
|
+
if (anonymous !== true && isComponentName(name) && !analysis.hooks) {
|
|
577
|
+
plain ??= plainlyCalled(program);
|
|
578
|
+
if (plain.has(name)) continue;
|
|
579
|
+
}
|
|
580
|
+
const prologue = block ? directives(statements) : [];
|
|
581
|
+
const firstStatement = statements[prologue.length];
|
|
582
|
+
const bodyHead = block ? firstStatement?.start ?? body.end - 1 : body.start;
|
|
583
|
+
if (anyOptOut && optedOut(code, regionStart, bodyHead)) continue;
|
|
584
|
+
let local = block ? existingHookLocal(statements, useTLocal) : null;
|
|
585
|
+
if (local === null) {
|
|
586
|
+
const head = block ? -1 : conciseBodyHead(code, fn);
|
|
587
|
+
if (!block && head === -1) continue;
|
|
588
|
+
useTLocal ??= freeName(code, USE_T_LOCAL);
|
|
589
|
+
hookLocal ??= freeName(code, HOOK_LOCAL);
|
|
590
|
+
local = hookLocal;
|
|
591
|
+
const declaration = `const ${local} = ${useTLocal}();`;
|
|
592
|
+
if (!block) {
|
|
593
|
+
sink.appendLeft(head, `{ ${declaration} return `);
|
|
594
|
+
sink.appendLeft(fn.end, "; }");
|
|
595
|
+
} else {
|
|
596
|
+
const last = prologue[prologue.length - 1];
|
|
597
|
+
if (last === void 0) sink.appendLeft(body.start + 1, ` ${declaration}`);
|
|
598
|
+
else {
|
|
599
|
+
const semi = code.charCodeAt(last.end - 1) === 59 ? "" : ";";
|
|
600
|
+
sink.appendLeft(last.end, `${semi} ${declaration}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
for (const { kind, call, callee } of calls) {
|
|
605
|
+
if (kind === "t") {
|
|
606
|
+
sink.overwrite(callee.start, callee.end, local);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
sink.appendLeft(callee.end, ".call");
|
|
610
|
+
const first = call.arguments[0];
|
|
611
|
+
if (first === void 0) sink.appendLeft(call.end - 1, local);
|
|
612
|
+
else sink.appendLeft(first.start, `${local}, `);
|
|
613
|
+
}
|
|
614
|
+
edited = true;
|
|
615
|
+
}
|
|
616
|
+
if (edited && context.useTLocal === null) {
|
|
617
|
+
const statement = `import { ${USE_T} as ${useTLocal} } from ${JSON.stringify(glue)};`;
|
|
618
|
+
const prologue = directives(program.body);
|
|
619
|
+
const last = prologue[prologue.length - 1];
|
|
620
|
+
if (last !== void 0) {
|
|
621
|
+
const semi = code.charCodeAt(last.end - 1) === 59 ? "" : ";";
|
|
622
|
+
sink.appendLeft(last.end, `${semi}\n${statement}`);
|
|
623
|
+
} else if (program.hashbang != null) sink.appendLeft(program.hashbang.end, `\n${statement}`);
|
|
624
|
+
else sink.prepend(`${statement}\n`);
|
|
625
|
+
}
|
|
626
|
+
return edited;
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/core/extract/captured.ts
|
|
630
|
+
/** Callee kinds whose result is translated text (not `msg`, which declares a message). */
|
|
631
|
+
const TRANSLATING = /* @__PURE__ */ new Set([
|
|
632
|
+
"t",
|
|
633
|
+
"rich",
|
|
634
|
+
"richHtml",
|
|
635
|
+
"richParts",
|
|
636
|
+
"alias",
|
|
637
|
+
"global",
|
|
638
|
+
"translator"
|
|
639
|
+
]);
|
|
640
|
+
const FIX = {
|
|
641
|
+
svelte: "call t() in the markup or derive it with $derived(t(…))",
|
|
642
|
+
vue: "call t() in the template or use computed(() => t(…))",
|
|
643
|
+
solid: "call t() in JSX, or make it a function (() => t(…)) or createMemo(() => t(…))",
|
|
644
|
+
react: "call t() during render instead of keeping its result",
|
|
645
|
+
preact: "call t() during render instead of keeping its result",
|
|
646
|
+
astro: ""
|
|
647
|
+
};
|
|
648
|
+
/**
|
|
649
|
+
* React/Preact hooks whose arguments are kept from the first render: the
|
|
650
|
+
* argument positions, and whether a function there is called once (a lazy
|
|
651
|
+
* initialiser) or stored as it is (`useRef(fn)`, `useSignal(fn)`).
|
|
652
|
+
*/
|
|
653
|
+
const ONCE_HOOKS = /* @__PURE__ */ new Map([
|
|
654
|
+
["useState", [[0, "call"]]],
|
|
655
|
+
["useRef", [[0, "store"]]],
|
|
656
|
+
["useReducer", [[1, "store"], [2, "call"]]],
|
|
657
|
+
["useSignal", [[0, "store"]]]
|
|
658
|
+
]);
|
|
659
|
+
/** Vue's function-form components: the first argument is `setup`. */
|
|
660
|
+
const VUE_DEFINE = /* @__PURE__ */ new Set(["defineComponent", "defineVaporComponent"]);
|
|
661
|
+
/** Vue options whose function runs once per instance, next to these keys that mark a component. */
|
|
662
|
+
const VUE_ONCE = /* @__PURE__ */ new Set(["setup", "data"]);
|
|
663
|
+
const VUE_COMPONENT_KEYS = /* @__PURE__ */ new Set([
|
|
664
|
+
"setup",
|
|
665
|
+
"render",
|
|
666
|
+
"template",
|
|
667
|
+
"props",
|
|
668
|
+
"computed",
|
|
669
|
+
"methods",
|
|
670
|
+
"components",
|
|
671
|
+
"watch",
|
|
672
|
+
"emits",
|
|
673
|
+
"name",
|
|
674
|
+
"__name"
|
|
675
|
+
]);
|
|
676
|
+
/** Keys that hold only TypeScript types; nothing under them runs. */
|
|
677
|
+
const TYPE_KEYS = /* @__PURE__ */ new Set([
|
|
678
|
+
"typeAnnotation",
|
|
679
|
+
"typeParameters",
|
|
680
|
+
"typeArguments",
|
|
681
|
+
"superTypeArguments",
|
|
682
|
+
"returnType",
|
|
683
|
+
"implements"
|
|
684
|
+
]);
|
|
685
|
+
const RETURNED = "the returned value";
|
|
686
|
+
function isFunction(node) {
|
|
687
|
+
return node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration";
|
|
688
|
+
}
|
|
689
|
+
/** Pushes the children of `node` that are nodes (types excluded). */
|
|
690
|
+
function children(node, into) {
|
|
691
|
+
for (const key in node) {
|
|
692
|
+
if (key === "type" || key === "start" || key === "end" || TYPE_KEYS.has(key)) continue;
|
|
693
|
+
const value = node[key];
|
|
694
|
+
if (value === null || typeof value !== "object") continue;
|
|
695
|
+
if (Array.isArray(value)) {
|
|
696
|
+
for (const item of value) if (item !== null && typeof item.type === "string") into.push(item);
|
|
697
|
+
} else if (typeof value.type === "string") into.push(value);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
var Finder = class {
|
|
701
|
+
/** Reported calls by start offset, so diagnostics come out in source order whatever the walk order. */
|
|
702
|
+
found = [];
|
|
703
|
+
/** Call start → index into `scan.calls`, for translating calls only. */
|
|
704
|
+
targets = /* @__PURE__ */ new Map();
|
|
705
|
+
reported = /* @__PURE__ */ new Set();
|
|
706
|
+
code;
|
|
707
|
+
scan;
|
|
708
|
+
fix;
|
|
709
|
+
/** What a `return` inside a once-running function stores, as named in messages. */
|
|
710
|
+
returned = RETURNED;
|
|
711
|
+
/** Roots (`__t`, a translator object) of calls the current walk treats as live. */
|
|
712
|
+
live = null;
|
|
713
|
+
markers;
|
|
714
|
+
ranges;
|
|
715
|
+
constructor(parsed, scan, framework, ranges) {
|
|
716
|
+
this.code = parsed.code;
|
|
717
|
+
this.scan = scan;
|
|
718
|
+
this.fix = FIX[framework];
|
|
719
|
+
this.markers = markersOf(parsed);
|
|
720
|
+
this.ranges = ranges;
|
|
721
|
+
for (let i = 0; i < scan.calls.length; i++) {
|
|
722
|
+
const call = scan.calls[i];
|
|
723
|
+
if (TRANSLATING.has(call.kind) && call.callStart !== void 0) this.targets.set(call.callStart, i);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
get empty() {
|
|
727
|
+
return this.targets.size === 0;
|
|
728
|
+
}
|
|
729
|
+
/** The `scan.calls` index of a translating call node, or -1. */
|
|
730
|
+
target(node) {
|
|
731
|
+
if (node.type !== "CallExpression") return -1;
|
|
732
|
+
const index = this.targets.get(node.start);
|
|
733
|
+
if (index === void 0) return -1;
|
|
734
|
+
const call = this.scan.calls[index];
|
|
735
|
+
return call.callEnd === void 0 || call.callEnd === node.end ? index : -1;
|
|
736
|
+
}
|
|
737
|
+
report(node, subject) {
|
|
738
|
+
const index = this.target(node);
|
|
739
|
+
if (index === -1 || this.reported.has(index)) return;
|
|
740
|
+
if (this.live !== null && this.live.has(rootName(node))) return;
|
|
741
|
+
if (this.markers.ignored.length > 0 && isIgnored(this.markers, node.start)) return;
|
|
742
|
+
this.reported.add(index);
|
|
743
|
+
const occurrence = this.scan.occurrences[this.scan.calls[index].occurrence];
|
|
744
|
+
const diagnostic = {
|
|
745
|
+
code: "extract-captured-value",
|
|
746
|
+
severity: "warning",
|
|
747
|
+
message: `${subject} captures a translation once and keeps the old locale's text after setLocale(); ${this.fix}`
|
|
748
|
+
};
|
|
749
|
+
if (occurrence !== void 0) {
|
|
750
|
+
diagnostic.file = occurrence.file;
|
|
751
|
+
if (occurrence.line > 0) {
|
|
752
|
+
diagnostic.line = occurrence.line;
|
|
753
|
+
diagnostic.column = occurrence.column;
|
|
754
|
+
}
|
|
755
|
+
diagnostic.id = occurrence.id;
|
|
756
|
+
}
|
|
757
|
+
if (this.ranges) diagnostic.range = [node.start, node.end];
|
|
758
|
+
this.found.push([node.start, diagnostic]);
|
|
759
|
+
}
|
|
760
|
+
get diagnostics() {
|
|
761
|
+
return this.found.sort((a, b) => a[0] - b[0]).map((entry) => entry[1]);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* A hook argument kept from the first render. A function is called once
|
|
765
|
+
* (`returned` names what it returns) or stored and called later; any other
|
|
766
|
+
* value is stored as computed, and functions inside it run later.
|
|
767
|
+
*/
|
|
768
|
+
kept(argument, function_, subject) {
|
|
769
|
+
const value = unwrap(argument);
|
|
770
|
+
if (!isFunction(value)) this.once(value, subject);
|
|
771
|
+
else if (function_ === "call") this.onceFunction(value, subject);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* A `useMemo` callback: runs on the first render and again only when its
|
|
775
|
+
* dependencies change, so its stored result is captured unless it reads
|
|
776
|
+
* through a translator in `live`.
|
|
777
|
+
*/
|
|
778
|
+
memo(callback, subject, live) {
|
|
779
|
+
const value = unwrap(callback);
|
|
780
|
+
if (!isFunction(value)) return;
|
|
781
|
+
this.live = live;
|
|
782
|
+
try {
|
|
783
|
+
this.onceFunction(value, subject);
|
|
784
|
+
} finally {
|
|
785
|
+
this.live = null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/** How a stored value is named in the message. */
|
|
789
|
+
subject(target) {
|
|
790
|
+
const node = unwrap(target);
|
|
791
|
+
if (node.type === "Identifier") return `\`${node.name}\``;
|
|
792
|
+
if (node.type === "MemberExpression" && node.end - node.start <= 40) return `\`${this.code.slice(node.start, node.end).replace(/\s+/g, "")}\``;
|
|
793
|
+
return "this declaration";
|
|
794
|
+
}
|
|
795
|
+
/** A function that runs once: its body, with a concise body as its returned value. */
|
|
796
|
+
onceFunction(fn, returned = RETURNED) {
|
|
797
|
+
const body = fn.body;
|
|
798
|
+
if (body === null) return;
|
|
799
|
+
const outer = this.returned;
|
|
800
|
+
this.returned = returned;
|
|
801
|
+
try {
|
|
802
|
+
if (body.type === "BlockStatement") this.once(body, null);
|
|
803
|
+
else this.once(body, returned);
|
|
804
|
+
} finally {
|
|
805
|
+
this.returned = outer;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Walks code that runs when the enclosing once-running function runs.
|
|
810
|
+
* `subject` is non-null inside a value that is stored.
|
|
811
|
+
*/
|
|
812
|
+
once(node, subject) {
|
|
813
|
+
switch (node.type) {
|
|
814
|
+
case "FunctionDeclaration":
|
|
815
|
+
case "FunctionExpression":
|
|
816
|
+
case "ArrowFunctionExpression":
|
|
817
|
+
case "ClassDeclaration":
|
|
818
|
+
case "ClassExpression":
|
|
819
|
+
case "JSXElement":
|
|
820
|
+
case "JSXFragment": return;
|
|
821
|
+
case "CallExpression": {
|
|
822
|
+
const callee = unwrap(node.callee);
|
|
823
|
+
if (isDerived(callee)) return;
|
|
824
|
+
if (subject !== null) this.report(node, subject);
|
|
825
|
+
if (callee.type === "FunctionExpression" || callee.type === "ArrowFunctionExpression") this.onceFunction(callee);
|
|
826
|
+
else this.once(callee, subject);
|
|
827
|
+
for (const argument of node.arguments) this.once(argument, subject);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
case "VariableDeclarator": {
|
|
831
|
+
const init = node.init;
|
|
832
|
+
const stored = this.subject(node.id);
|
|
833
|
+
this.once(node.id, stored);
|
|
834
|
+
if (init !== null) this.once(init, stored);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
case "AssignmentExpression":
|
|
838
|
+
this.once(node.left, subject);
|
|
839
|
+
this.once(node.right, this.subject(node.left));
|
|
840
|
+
return;
|
|
841
|
+
case "ReturnStatement":
|
|
842
|
+
if (node.argument !== null) this.once(node.argument, this.returned);
|
|
843
|
+
return;
|
|
844
|
+
default: {
|
|
845
|
+
const list = [];
|
|
846
|
+
children(node, list);
|
|
847
|
+
for (const child of list) this.once(child, subject);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
/** Svelte's `$derived(…)` and `$derived.by(…)`, as written in a component's source. */
|
|
853
|
+
function isDerived(callee) {
|
|
854
|
+
if (callee.type === "Identifier") return callee.name === "$derived";
|
|
855
|
+
return callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "$derived";
|
|
856
|
+
}
|
|
857
|
+
/** Svelte 5 client components: top-level `function C($$anchor, …)`. */
|
|
858
|
+
function svelteComponents(program) {
|
|
859
|
+
const out = [];
|
|
860
|
+
for (const statement of program.body) {
|
|
861
|
+
let node = statement;
|
|
862
|
+
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") node = node.declaration ?? node;
|
|
863
|
+
if (node.type !== "FunctionDeclaration") continue;
|
|
864
|
+
const first = node.params[0];
|
|
865
|
+
if (first?.type === "Identifier" && first.name === "$$anchor") out.push(node);
|
|
866
|
+
}
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Vue `setup()`/`data()` functions of objects that look like component
|
|
871
|
+
* options, and the setup function of the function form
|
|
872
|
+
* `defineComponent((props) => { … })` (`define` holds the imported locals of
|
|
873
|
+
* `defineComponent`/`defineVaporComponent`; `Vue.defineComponent` counts too).
|
|
874
|
+
*/
|
|
875
|
+
function vueOnceFunctions(program, define) {
|
|
876
|
+
const out = [];
|
|
877
|
+
const stack = [program];
|
|
878
|
+
for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
|
|
879
|
+
if (node.type === "CallExpression") {
|
|
880
|
+
const callee = unwrap(node.callee);
|
|
881
|
+
const defines = callee.type === "Identifier" ? define.has(callee.name) : callee.type === "MemberExpression" && VUE_DEFINE.has(keyName(callee.property, callee.computed === true) ?? "");
|
|
882
|
+
const first = node.arguments[0];
|
|
883
|
+
const setup = first === void 0 ? null : unwrap(first);
|
|
884
|
+
if (defines && setup !== null && isFunction(setup)) out.push(setup);
|
|
885
|
+
} else if (node.type === "ObjectExpression") {
|
|
886
|
+
let component = false;
|
|
887
|
+
const found = [];
|
|
888
|
+
for (const property of node.properties) {
|
|
889
|
+
if (property.type !== "Property") continue;
|
|
890
|
+
const name = keyName(property.key, property.computed === true);
|
|
891
|
+
if (name === null) continue;
|
|
892
|
+
if (VUE_COMPONENT_KEYS.has(name)) component = true;
|
|
893
|
+
const value = unwrap(property.value);
|
|
894
|
+
if (VUE_ONCE.has(name) && isFunction(value)) {
|
|
895
|
+
if (name === "setup") component = true;
|
|
896
|
+
found.push(value);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (component) out.push(...found);
|
|
900
|
+
}
|
|
901
|
+
children(node, stack);
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
/** The name a callee calls (`useMemo`, `React.useMemo`), or null. */
|
|
906
|
+
function calleeName(call) {
|
|
907
|
+
const callee = unwrap(call.callee);
|
|
908
|
+
if (callee.type === "Identifier") return callee.name;
|
|
909
|
+
if (callee.type === "MemberExpression") return keyName(callee.property, callee.computed === true);
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Locals that hold the locale: `const locale = getLocale()` with `getLocale`
|
|
914
|
+
* imported. A `useMemo` keyed on one recomputes on a switch.
|
|
915
|
+
*/
|
|
916
|
+
function localeLocals(program, getLocale) {
|
|
917
|
+
const out = /* @__PURE__ */ new Set();
|
|
918
|
+
if (getLocale.size === 0) return out;
|
|
919
|
+
const stack = [program];
|
|
920
|
+
for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
|
|
921
|
+
children(node, stack);
|
|
922
|
+
if (node.type !== "VariableDeclarator") continue;
|
|
923
|
+
const id = node.id;
|
|
924
|
+
const init = node.init === null ? null : unwrap(node.init);
|
|
925
|
+
if (id.type === "Identifier" && init?.type === "CallExpression" && isLocaleCall(init, getLocale)) out.add(id.name);
|
|
926
|
+
}
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
function isLocaleCall(call, getLocale) {
|
|
930
|
+
const callee = unwrap(call.callee);
|
|
931
|
+
return callee.type === "Identifier" && getLocale.has(callee.name);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* React/Preact hook initialisers that run once. A `useMemo` callback is
|
|
935
|
+
* captured unless its dependencies name the call's translator (the hook
|
|
936
|
+
* local `__t`, or a translator object), which changes per locale, or the
|
|
937
|
+
* locale itself (`getLocale()`, or a local holding it); an imported `t`
|
|
938
|
+
* never changes identity, so listing it does not help.
|
|
939
|
+
*/
|
|
940
|
+
function reactHooks(program, finder, imports) {
|
|
941
|
+
const imported = /* @__PURE__ */ new Set();
|
|
942
|
+
const getLocale = /* @__PURE__ */ new Set();
|
|
943
|
+
for (const record of imports) for (const binding of record.bindings) {
|
|
944
|
+
imported.add(binding.local);
|
|
945
|
+
if (binding.imported === "getLocale") getLocale.add(binding.local);
|
|
946
|
+
}
|
|
947
|
+
let locales = null;
|
|
948
|
+
const stack = [program];
|
|
949
|
+
for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
|
|
950
|
+
children(node, stack);
|
|
951
|
+
if (node.type !== "CallExpression") continue;
|
|
952
|
+
const name = calleeName(node);
|
|
953
|
+
if (name === null) continue;
|
|
954
|
+
const args = node.arguments;
|
|
955
|
+
if (name === "useMemo") {
|
|
956
|
+
const deps = args[1] === void 0 ? null : unwrap(args[1]);
|
|
957
|
+
if (deps === null || deps.type !== "ArrayExpression") continue;
|
|
958
|
+
locales ??= localeLocals(program, getLocale);
|
|
959
|
+
const live = /* @__PURE__ */ new Set();
|
|
960
|
+
let keyedOnLocale = false;
|
|
961
|
+
for (const element of deps.elements) {
|
|
962
|
+
const value = element === null ? null : unwrap(element);
|
|
963
|
+
if (value?.type === "Identifier") {
|
|
964
|
+
const dep = value.name;
|
|
965
|
+
if (locales.has(dep)) keyedOnLocale = true;
|
|
966
|
+
else if (!imported.has(dep)) live.add(dep);
|
|
967
|
+
} else if (value?.type === "CallExpression" && isLocaleCall(value, getLocale)) keyedOnLocale = true;
|
|
968
|
+
}
|
|
969
|
+
if (!keyedOnLocale && args[0] !== void 0) finder.memo(args[0], "a useMemo() without the translator or the locale in its dependencies", live);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const positions = ONCE_HOOKS.get(name);
|
|
973
|
+
if (positions === void 0) continue;
|
|
974
|
+
for (const [position, function_] of positions) {
|
|
975
|
+
const argument = args[position];
|
|
976
|
+
if (argument !== void 0 && argument.type !== "SpreadElement") finder.kept(argument, function_, `the initial value of ${name}()`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/** The identifier a callee starts from (`__t` in `__t(…)`, `tr` in `tr.t(…)`), or "". */
|
|
981
|
+
function rootName(call) {
|
|
982
|
+
let node = unwrap(call.callee);
|
|
983
|
+
for (;;) {
|
|
984
|
+
if (node.type === "Identifier") return node.name;
|
|
985
|
+
if (node.type === "MemberExpression") node = unwrap(node.object);
|
|
986
|
+
else if (node.type === "SequenceExpression") {
|
|
987
|
+
const expressions = node.expressions;
|
|
988
|
+
node = unwrap(expressions[expressions.length - 1]);
|
|
989
|
+
} else return "";
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Warnings (`extract-captured-value`) for translations that `framework`'s
|
|
994
|
+
* components compute once and keep, so an in-place locale switch leaves them
|
|
995
|
+
* in the old locale. Positions are the occurrences' (the message literal);
|
|
996
|
+
* `id` names the message. No framework, or Astro, finds nothing. Calls under
|
|
997
|
+
* `vi18n-ignore` are not reported. The `/lint` rule passes `topLevel` for a
|
|
998
|
+
* Vue or Svelte script as written, and `ranges` to report at the call.
|
|
999
|
+
*/
|
|
1000
|
+
function findCapturedValues(parsed, scan, framework, options = {}) {
|
|
1001
|
+
if (framework === null || framework === "astro") return [];
|
|
1002
|
+
const finder = new Finder(parsed, scan, framework, options.ranges === true);
|
|
1003
|
+
if (finder.empty) return [];
|
|
1004
|
+
const program = parsed.program;
|
|
1005
|
+
if (options.topLevel === true) finder.onceFunction({
|
|
1006
|
+
type: "FunctionExpression",
|
|
1007
|
+
start: program.start,
|
|
1008
|
+
end: program.end,
|
|
1009
|
+
body: {
|
|
1010
|
+
type: "BlockStatement",
|
|
1011
|
+
start: program.start,
|
|
1012
|
+
end: program.end,
|
|
1013
|
+
body: program.body
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
switch (framework) {
|
|
1017
|
+
case "svelte":
|
|
1018
|
+
for (const fn of svelteComponents(program)) finder.onceFunction(fn);
|
|
1019
|
+
break;
|
|
1020
|
+
case "vue": {
|
|
1021
|
+
const define = /* @__PURE__ */ new Set();
|
|
1022
|
+
for (const record of listImports(parsed)) for (const binding of record.bindings) if (VUE_DEFINE.has(binding.imported)) define.add(binding.local);
|
|
1023
|
+
for (const fn of vueOnceFunctions(program, define)) finder.onceFunction(fn);
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
case "solid":
|
|
1027
|
+
for (const { name, fn } of topLevelFunctions(program)) if (isComponentName(name)) finder.onceFunction(fn);
|
|
1028
|
+
break;
|
|
1029
|
+
default: reactHooks(program, finder, listImports(parsed));
|
|
1030
|
+
}
|
|
1031
|
+
return finder.diagnostics;
|
|
1032
|
+
}
|
|
1033
|
+
//#endregion
|
|
1034
|
+
export { injectHooks as n, findCapturedValues as t };
|