next-tongue 0.9.6 → 0.9.7
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/dist/client/index.js +1 -1
- package/dist/compiler/dynamic-from.d.ts +31 -0
- package/dist/compiler/dynamic-from.js +240 -0
- package/dist/compiler/scan-keys.js +6 -10
- package/dist/next/loader.d.ts +2 -1
- package/dist/next/loader.js +17 -18
- package/package.json +1 -1
package/dist/client/index.js
CHANGED
|
@@ -88,7 +88,7 @@ export function useT(namespace) {
|
|
|
88
88
|
*/
|
|
89
89
|
function createLoaderStubT(locale, ns) {
|
|
90
90
|
const fail = (kind) => {
|
|
91
|
-
throw new TongueError('TONGUE_E_NO_PROVIDER', `useT('${ns}') ${kind} was not rewritten to a per-key import. Ensure withTongue() is applied (loader on SSR + client). Prefer getT on the server for static copy; for dynamic keys use t.dynamic(key, { from: [...] })
|
|
91
|
+
throw new TongueError('TONGUE_E_NO_PROVIDER', `useT('${ns}') ${kind} was not rewritten to a per-key import. Ensure withTongue() is applied (loader on SSR + client). Prefer getT on the server for static copy; for dynamic keys use t.dynamic(key, { from: [...] }) with an inline array or a same-file const (imported from arrays are not rewritten).`);
|
|
92
92
|
};
|
|
93
93
|
const t = Object.assign((..._args) => fail('t()'), {
|
|
94
94
|
rich: () => fail('t.rich()'),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `t.dynamic(key, { from })` key lists for the call-site loader and
|
|
3
|
+
* `tongue check` scan. Supports:
|
|
4
|
+
* - inline arrays: `{ from: ['a', 'b'] }`
|
|
5
|
+
* - same-file const arrays: `const X = ['a', 'b']; { from: X }`
|
|
6
|
+
* - Object.values of same-file const objects: `{ from: Object.values(BTN_KEYS) }`
|
|
7
|
+
*
|
|
8
|
+
* Imported / computed `from` expressions are not resolved (left as stubs).
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractStringLiterals(body: string): string[];
|
|
11
|
+
/** Take a balanced `[...]` / `{...}` / `(...)` starting at `source[start]`. */
|
|
12
|
+
export declare function takeBalanced(source: string, start: number, open: string, close: string): string | null;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve keys from a `from` expression text (already sliced out of the call).
|
|
15
|
+
* Returns null when the expression cannot be resolved statically in-file.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveFromKeys(fileSource: string, fromExpr: string): string[] | null;
|
|
18
|
+
export type DynamicCallSite = {
|
|
19
|
+
/** Start of `t.dynamic` */
|
|
20
|
+
start: number;
|
|
21
|
+
/** End after closing `)` */
|
|
22
|
+
end: number;
|
|
23
|
+
keyExpr: string;
|
|
24
|
+
fromExpr: string;
|
|
25
|
+
valuesExpr: string | null;
|
|
26
|
+
keys: string[] | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Find `varName.dynamic(...)` call sites and resolve their `from` keys.
|
|
30
|
+
*/
|
|
31
|
+
export declare function findDynamicCalls(source: string, varName: string): DynamicCallSite[];
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `t.dynamic(key, { from })` key lists for the call-site loader and
|
|
3
|
+
* `tongue check` scan. Supports:
|
|
4
|
+
* - inline arrays: `{ from: ['a', 'b'] }`
|
|
5
|
+
* - same-file const arrays: `const X = ['a', 'b']; { from: X }`
|
|
6
|
+
* - Object.values of same-file const objects: `{ from: Object.values(BTN_KEYS) }`
|
|
7
|
+
*
|
|
8
|
+
* Imported / computed `from` expressions are not resolved (left as stubs).
|
|
9
|
+
*/
|
|
10
|
+
export function extractStringLiterals(body) {
|
|
11
|
+
return [...body.matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
12
|
+
}
|
|
13
|
+
/** Take a balanced `[...]` / `{...}` / `(...)` starting at `source[start]`. */
|
|
14
|
+
export function takeBalanced(source, start, open, close) {
|
|
15
|
+
if (source[start] !== open)
|
|
16
|
+
return null;
|
|
17
|
+
let depth = 0;
|
|
18
|
+
let inStr = null;
|
|
19
|
+
let escape = false;
|
|
20
|
+
for (let i = start; i < source.length; i++) {
|
|
21
|
+
const c = source[i];
|
|
22
|
+
if (inStr) {
|
|
23
|
+
if (escape) {
|
|
24
|
+
escape = false;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (c === '\\' && inStr !== '`') {
|
|
28
|
+
escape = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (c === inStr)
|
|
32
|
+
inStr = null;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
36
|
+
inStr = c;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (c === open)
|
|
40
|
+
depth++;
|
|
41
|
+
else if (c === close) {
|
|
42
|
+
depth--;
|
|
43
|
+
if (depth === 0)
|
|
44
|
+
return source.slice(start, i + 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function escapeRegExp(s) {
|
|
50
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
51
|
+
}
|
|
52
|
+
/** Index of `ident`'s `const|let|var` initializer expression, or -1. */
|
|
53
|
+
function findDeclInit(source, ident) {
|
|
54
|
+
const re = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(ident)}\\s*=\\s*`, 'g');
|
|
55
|
+
const m = re.exec(source);
|
|
56
|
+
return m ? m.index + m[0].length : -1;
|
|
57
|
+
}
|
|
58
|
+
function skipWs(source, i) {
|
|
59
|
+
while (/\s/.test(source[i] ?? ''))
|
|
60
|
+
i++;
|
|
61
|
+
return i;
|
|
62
|
+
}
|
|
63
|
+
function resolveConstArrayKeys(source, ident) {
|
|
64
|
+
const init = findDeclInit(source, ident);
|
|
65
|
+
if (init < 0)
|
|
66
|
+
return null;
|
|
67
|
+
const arr = takeBalanced(source, skipWs(source, init), '[', ']');
|
|
68
|
+
if (!arr)
|
|
69
|
+
return null;
|
|
70
|
+
return extractStringLiterals(arr.slice(1, -1));
|
|
71
|
+
}
|
|
72
|
+
function resolveObjectValueKeys(source, ident) {
|
|
73
|
+
const init = findDeclInit(source, ident);
|
|
74
|
+
if (init < 0)
|
|
75
|
+
return null;
|
|
76
|
+
// Type assertions (`as const`) sit after the literal; object is first.
|
|
77
|
+
const obj = takeBalanced(source, skipWs(source, init), '{', '}');
|
|
78
|
+
if (!obj)
|
|
79
|
+
return null;
|
|
80
|
+
return [...obj.matchAll(/:\s*['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve keys from a `from` expression text (already sliced out of the call).
|
|
84
|
+
* Returns null when the expression cannot be resolved statically in-file.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveFromKeys(fileSource, fromExpr) {
|
|
87
|
+
const trimmed = fromExpr.trim();
|
|
88
|
+
if (!trimmed)
|
|
89
|
+
return null;
|
|
90
|
+
if (trimmed.startsWith('[')) {
|
|
91
|
+
const arr = takeBalanced(trimmed, 0, '[', ']');
|
|
92
|
+
if (!arr)
|
|
93
|
+
return null;
|
|
94
|
+
return extractStringLiterals(arr.slice(1, -1));
|
|
95
|
+
}
|
|
96
|
+
const ov = /^Object\.values\(\s*([A-Za-z_$][\w$]*)\s*\)$/.exec(trimmed);
|
|
97
|
+
if (ov) {
|
|
98
|
+
const keys = resolveObjectValueKeys(fileSource, ov[1]);
|
|
99
|
+
return keys && keys.length > 0 ? keys : null;
|
|
100
|
+
}
|
|
101
|
+
if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) {
|
|
102
|
+
const keys = resolveConstArrayKeys(fileSource, trimmed);
|
|
103
|
+
return keys && keys.length > 0 ? keys : null;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Find `varName.dynamic(...)` call sites and resolve their `from` keys.
|
|
109
|
+
*/
|
|
110
|
+
export function findDynamicCalls(source, varName) {
|
|
111
|
+
const re = new RegExp(`\\b${escapeRegExp(varName)}\\.dynamic\\s*\\(`, 'g');
|
|
112
|
+
const out = [];
|
|
113
|
+
let m;
|
|
114
|
+
while ((m = re.exec(source)) !== null) {
|
|
115
|
+
const openParen = m.index + m[0].length - 1;
|
|
116
|
+
const parsed = parseDynamicArgs(source, openParen);
|
|
117
|
+
if (!parsed)
|
|
118
|
+
continue;
|
|
119
|
+
const keys = resolveFromKeys(source, parsed.fromExpr);
|
|
120
|
+
out.push({
|
|
121
|
+
start: m.index,
|
|
122
|
+
end: parsed.closeParen + 1,
|
|
123
|
+
keyExpr: parsed.keyExpr,
|
|
124
|
+
fromExpr: parsed.fromExpr,
|
|
125
|
+
valuesExpr: parsed.valuesExpr,
|
|
126
|
+
keys,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function parseDynamicArgs(source, openParen) {
|
|
132
|
+
let i = openParen + 1;
|
|
133
|
+
while (/\s/.test(source[i] ?? ''))
|
|
134
|
+
i++;
|
|
135
|
+
const keyStart = i;
|
|
136
|
+
i = scanArgEnd(source, i);
|
|
137
|
+
if (i < 0 || source[i] !== ',')
|
|
138
|
+
return null;
|
|
139
|
+
const keyExpr = source.slice(keyStart, i).trim();
|
|
140
|
+
i++;
|
|
141
|
+
while (/\s/.test(source[i] ?? ''))
|
|
142
|
+
i++;
|
|
143
|
+
if (source[i] !== '{')
|
|
144
|
+
return null;
|
|
145
|
+
const opts = takeBalanced(source, i, '{', '}');
|
|
146
|
+
if (!opts)
|
|
147
|
+
return null;
|
|
148
|
+
const optsEnd = i + opts.length;
|
|
149
|
+
const fromExpr = extractFromProp(opts);
|
|
150
|
+
if (fromExpr == null)
|
|
151
|
+
return null;
|
|
152
|
+
i = optsEnd;
|
|
153
|
+
while (/\s/.test(source[i] ?? ''))
|
|
154
|
+
i++;
|
|
155
|
+
let valuesExpr = null;
|
|
156
|
+
if (source[i] === ',') {
|
|
157
|
+
i++;
|
|
158
|
+
while (/\s/.test(source[i] ?? ''))
|
|
159
|
+
i++;
|
|
160
|
+
const valuesStart = i;
|
|
161
|
+
i = scanArgEnd(source, i);
|
|
162
|
+
if (i < 0)
|
|
163
|
+
return null;
|
|
164
|
+
valuesExpr = source.slice(valuesStart, i).trim() || null;
|
|
165
|
+
}
|
|
166
|
+
while (/\s/.test(source[i] ?? ''))
|
|
167
|
+
i++;
|
|
168
|
+
if (source[i] !== ')')
|
|
169
|
+
return null;
|
|
170
|
+
return { keyExpr, fromExpr, valuesExpr, closeParen: i };
|
|
171
|
+
}
|
|
172
|
+
/** Scan to next top-level `,` or `)` (not inside strings/brackets). */
|
|
173
|
+
function scanArgEnd(source, start) {
|
|
174
|
+
let depthParen = 0;
|
|
175
|
+
let depthBracket = 0;
|
|
176
|
+
let depthBrace = 0;
|
|
177
|
+
let inStr = null;
|
|
178
|
+
let escape = false;
|
|
179
|
+
for (let i = start; i < source.length; i++) {
|
|
180
|
+
const c = source[i];
|
|
181
|
+
if (inStr) {
|
|
182
|
+
if (escape) {
|
|
183
|
+
escape = false;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (c === '\\' && inStr !== '`') {
|
|
187
|
+
escape = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (c === inStr)
|
|
191
|
+
inStr = null;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
195
|
+
inStr = c;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (c === '(')
|
|
199
|
+
depthParen++;
|
|
200
|
+
else if (c === ')') {
|
|
201
|
+
if (depthParen === 0 && depthBracket === 0 && depthBrace === 0)
|
|
202
|
+
return i;
|
|
203
|
+
depthParen--;
|
|
204
|
+
}
|
|
205
|
+
else if (c === '[')
|
|
206
|
+
depthBracket++;
|
|
207
|
+
else if (c === ']')
|
|
208
|
+
depthBracket--;
|
|
209
|
+
else if (c === '{')
|
|
210
|
+
depthBrace++;
|
|
211
|
+
else if (c === '}')
|
|
212
|
+
depthBrace--;
|
|
213
|
+
else if (c === ',' &&
|
|
214
|
+
depthParen === 0 &&
|
|
215
|
+
depthBracket === 0 &&
|
|
216
|
+
depthBrace === 0) {
|
|
217
|
+
return i;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return -1;
|
|
221
|
+
}
|
|
222
|
+
function extractFromProp(optsLiteral) {
|
|
223
|
+
// optsLiteral includes surrounding braces
|
|
224
|
+
const inner = optsLiteral.slice(1, -1);
|
|
225
|
+
const fromIdx = /\bfrom\s*:/.exec(inner);
|
|
226
|
+
if (!fromIdx)
|
|
227
|
+
return null;
|
|
228
|
+
let i = fromIdx.index + fromIdx[0].length;
|
|
229
|
+
while (/\s/.test(inner[i] ?? ''))
|
|
230
|
+
i++;
|
|
231
|
+
if (inner[i] === '[') {
|
|
232
|
+
const arr = takeBalanced(inner, i, '[', ']');
|
|
233
|
+
return arr;
|
|
234
|
+
}
|
|
235
|
+
const ov = /^Object\.values\(\s*[A-Za-z_$][\w$]*\s*\)/.exec(inner.slice(i));
|
|
236
|
+
if (ov)
|
|
237
|
+
return ov[0];
|
|
238
|
+
const id = /^[A-Za-z_$][\w$]*/.exec(inner.slice(i));
|
|
239
|
+
return id ? id[0] : null;
|
|
240
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { join, relative } from 'node:path';
|
|
3
|
+
import { findDynamicCalls } from './dynamic-from.js';
|
|
3
4
|
const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mdx']);
|
|
4
5
|
const SKIP_DIRS = new Set([
|
|
5
6
|
'node_modules',
|
|
@@ -152,16 +153,11 @@ export function extractFromSource(source, file) {
|
|
|
152
153
|
while ((cm = callRe.exec(code)) !== null) {
|
|
153
154
|
push(ns, cm[2], cm.index);
|
|
154
155
|
}
|
|
155
|
-
// t.dynamic(any, { from
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const fromMatch = /from\s*:\s*\[([^\]]*)\]/.exec(tail);
|
|
161
|
-
if (fromMatch) {
|
|
162
|
-
const keys = fromMatch[1].match(/['"]([^'"]+)['"]/g) ?? [];
|
|
163
|
-
for (const raw of keys) {
|
|
164
|
-
push(ns, raw.slice(1, -1), dm.index);
|
|
156
|
+
// t.dynamic(any, { from }) — only resolvable `from` keys become refs
|
|
157
|
+
for (const call of findDynamicCalls(code, varName)) {
|
|
158
|
+
if (call.keys) {
|
|
159
|
+
for (const key of call.keys) {
|
|
160
|
+
push(ns, key, call.start);
|
|
165
161
|
}
|
|
166
162
|
}
|
|
167
163
|
dynamicNamespaces.add(ns || '_root');
|
package/dist/next/loader.d.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* const t = useT('home'); t('inbox', { count })
|
|
7
7
|
* into a static import of the per-key module when the key is a string literal.
|
|
8
8
|
*
|
|
9
|
-
* Also rewrites `t.dynamic(key, { from
|
|
9
|
+
* Also rewrites `t.dynamic(key, { from })` to a map of those imports when `from`
|
|
10
|
+
* is an inline array, a same-file const array, or `Object.values(sameFileConst)`.
|
|
10
11
|
*
|
|
11
12
|
* Escape analysis: if `t` is passed around, falls back to namespace import + warning.
|
|
12
13
|
*/
|
package/dist/next/loader.js
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* const t = useT('home'); t('inbox', { count })
|
|
7
7
|
* into a static import of the per-key module when the key is a string literal.
|
|
8
8
|
*
|
|
9
|
-
* Also rewrites `t.dynamic(key, { from
|
|
9
|
+
* Also rewrites `t.dynamic(key, { from })` to a map of those imports when `from`
|
|
10
|
+
* is an inline array, a same-file const array, or `Object.values(sameFileConst)`.
|
|
10
11
|
*
|
|
11
12
|
* Escape analysis: if `t` is passed around, falls back to namespace import + warning.
|
|
12
13
|
*/
|
|
14
|
+
import { findDynamicCalls } from '../compiler/dynamic-from.js';
|
|
13
15
|
const USE_T_DECL = /(?:const|let|var)\s+(\w+)\s*=\s*useT\s*\(\s*(['"])([^'"]*)\2\s*\)/g;
|
|
14
16
|
export function transformCallSites(source, resourcePath, _options = {}) {
|
|
15
17
|
const warnings = [];
|
|
@@ -44,16 +46,11 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
44
46
|
imports.set(`${ns}:${key}`, { ns, key, ident });
|
|
45
47
|
return full;
|
|
46
48
|
});
|
|
47
|
-
// t.dynamic(expr, { from
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
while ((dm = dynRe.exec(withoutDirs)) !== null) {
|
|
51
|
-
const tail = withoutDirs.slice(dm.index, dm.index + 500);
|
|
52
|
-
const fromMatch = /from\s*:\s*\[([^\]]*)\]/.exec(tail);
|
|
53
|
-
if (!fromMatch)
|
|
49
|
+
// t.dynamic(expr, { from }, values?) — collect resolvable keys
|
|
50
|
+
for (const call of findDynamicCalls(withoutDirs, varName)) {
|
|
51
|
+
if (!call.keys)
|
|
54
52
|
continue;
|
|
55
|
-
const
|
|
56
|
-
for (const key of keys) {
|
|
53
|
+
for (const key of call.keys) {
|
|
57
54
|
const ident = safeImportIdent(ns, key);
|
|
58
55
|
imports.set(`${ns}:${key}`, { ns, key, ident });
|
|
59
56
|
}
|
|
@@ -72,13 +69,14 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
72
69
|
const localeVar = '__tongue_locale';
|
|
73
70
|
// Inject locale once after the first useT declaration
|
|
74
71
|
code = code.replace(/((?:const|let|var)\s+\w+\s*=\s*useT\s*\([^)]*\))/, `$1\n const ${localeVar} = useLocale();`);
|
|
75
|
-
// Replace t.dynamic(expr, { from
|
|
72
|
+
// Replace t.dynamic(expr, { from }, values?) — rewrite from the end so indices stay valid
|
|
76
73
|
for (const [varName, ns] of translators) {
|
|
77
74
|
if (escapes(withoutDirs, varName))
|
|
78
75
|
continue;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const
|
|
76
|
+
const calls = findDynamicCalls(code, varName).filter((c) => c.keys && c.keys.length > 0);
|
|
77
|
+
for (let ci = calls.length - 1; ci >= 0; ci--) {
|
|
78
|
+
const call = calls[ci];
|
|
79
|
+
const keys = call.keys;
|
|
82
80
|
const mapEntries = keys
|
|
83
81
|
.map((key) => {
|
|
84
82
|
const ident = safeImportIdent(ns, key);
|
|
@@ -86,11 +84,12 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
86
84
|
return `${JSON.stringify(key)}: ${ident}`;
|
|
87
85
|
})
|
|
88
86
|
.join(', ');
|
|
89
|
-
const valuesArg =
|
|
90
|
-
? `, ${
|
|
87
|
+
const valuesArg = call.valuesExpr != null && call.valuesExpr.trim() !== ''
|
|
88
|
+
? `, ${call.valuesExpr.trim()}`
|
|
91
89
|
: '';
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
const replacement = `((__tongue_dyn) => { const __tongue_fn = __tongue_dyn[${call.keyExpr.trim()}]; if (!__tongue_fn) throw new Error(${JSON.stringify(`[next-tongue] t.dynamic key not in from: [${keys.join(', ')}]`)}); return __tongue_fn(${localeVar}${valuesArg}); })({ ${mapEntries} })`;
|
|
91
|
+
code = code.slice(0, call.start) + replacement + code.slice(call.end);
|
|
92
|
+
}
|
|
94
93
|
}
|
|
95
94
|
// Replace t('key', values) / t.rich('key', values) with per-key calls
|
|
96
95
|
for (const [varName, ns] of translators) {
|