next-tongue 0.9.4 → 0.9.6
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/dev-registry.js +15 -2
- package/dist/client/index.js +9 -9
- package/dist/compiler/scan-keys.d.ts +4 -2
- package/dist/compiler/scan-keys.js +74 -25
- package/dist/next/loader.d.ts +10 -0
- package/dist/next/loader.js +73 -8
- package/dist/next/with-tongue.js +14 -13
- package/package.json +1 -1
|
@@ -13,15 +13,28 @@ function store() {
|
|
|
13
13
|
rendered: [],
|
|
14
14
|
listeners: new Set(),
|
|
15
15
|
snapshot: null,
|
|
16
|
+
notifyScheduled: false,
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
return g.__nextTongueDev;
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Invalidate snapshot and notify subscribers **after** the current render.
|
|
23
|
+
* Synchronous notify from inside `t()` (render) caused
|
|
24
|
+
* "Cannot update DevOverlay while rendering Header".
|
|
25
|
+
*/
|
|
20
26
|
function notify() {
|
|
21
27
|
const s = store();
|
|
22
28
|
s.snapshot = null;
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
if (s.notifyScheduled)
|
|
30
|
+
return;
|
|
31
|
+
s.notifyScheduled = true;
|
|
32
|
+
queueMicrotask(() => {
|
|
33
|
+
const st = store();
|
|
34
|
+
st.notifyScheduled = false;
|
|
35
|
+
for (const l of st.listeners)
|
|
36
|
+
l();
|
|
37
|
+
});
|
|
25
38
|
}
|
|
26
39
|
export function tongueDevLoaded(namespace, key) {
|
|
27
40
|
const fullKey = namespace === '_root' || namespace === '' ? key : `${namespace}.${key}`;
|
package/dist/client/index.js
CHANGED
|
@@ -82,19 +82,19 @@ export function useT(namespace) {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
|
-
* Stub used when the call-site loader owns message delivery.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
85
|
+
* Stub used when the call-site loader owns message delivery. Literal `t('key')`
|
|
86
|
+
* and `t.dynamic(key, { from })` should be rewritten by the loader; if you still
|
|
87
|
+
* hit this, the rewrite did not run (or a key was not in `from`).
|
|
88
88
|
*/
|
|
89
89
|
function createLoaderStubT(locale, ns) {
|
|
90
|
-
const fail = () => {
|
|
91
|
-
throw new TongueError('TONGUE_E_NO_PROVIDER', `useT('${ns}')
|
|
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: [...] }) so the loader can import those keys.`);
|
|
92
92
|
};
|
|
93
|
-
const t = Object.assign((..._args) => fail(), {
|
|
94
|
-
rich: fail,
|
|
93
|
+
const t = Object.assign((..._args) => fail('t()'), {
|
|
94
|
+
rich: () => fail('t.rich()'),
|
|
95
95
|
has: () => false,
|
|
96
|
-
dynamic: fail,
|
|
97
|
-
ns: fail,
|
|
96
|
+
dynamic: () => fail('t.dynamic()'),
|
|
97
|
+
ns: () => fail('t.ns()'),
|
|
98
98
|
});
|
|
99
99
|
void locale;
|
|
100
100
|
return t;
|
|
@@ -8,8 +8,8 @@ export type ReferencedKey = {
|
|
|
8
8
|
export type ScanResult = {
|
|
9
9
|
refs: ReferencedKey[];
|
|
10
10
|
/**
|
|
11
|
-
* Namespaces where `t(variable)` / `t.ns(...)` was seen —
|
|
12
|
-
* for these namespaces are suppressed
|
|
11
|
+
* Namespaces where `t(variable)` / `t.ns(...)` / `t.dynamic` was seen —
|
|
12
|
+
* unused-key warnings for these namespaces are suppressed.
|
|
13
13
|
*/
|
|
14
14
|
dynamicNamespaces: Set<string>;
|
|
15
15
|
};
|
|
@@ -19,4 +19,6 @@ export type ScanResult = {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function scanReferencedKeys(cwd: string, roots?: string[]): Promise<ReferencedKey[]>;
|
|
21
21
|
export declare function scanKeys(cwd: string, roots?: string[]): Promise<ScanResult>;
|
|
22
|
+
/** Strip // and /* *\/ comments; preserve newlines for line numbers. */
|
|
23
|
+
export declare function stripComments(source: string): string;
|
|
22
24
|
export declare function extractFromSource(source: string, file: string): ScanResult;
|
|
@@ -62,14 +62,77 @@ function extname(name) {
|
|
|
62
62
|
const i = name.lastIndexOf('.');
|
|
63
63
|
return i === -1 ? '' : name.slice(i);
|
|
64
64
|
}
|
|
65
|
+
/** Strip // and /* *\/ comments; preserve newlines for line numbers. */
|
|
66
|
+
export function stripComments(source) {
|
|
67
|
+
let out = '';
|
|
68
|
+
let i = 0;
|
|
69
|
+
while (i < source.length) {
|
|
70
|
+
const ch = source[i];
|
|
71
|
+
const next = source[i + 1];
|
|
72
|
+
// string / template — copy verbatim so // inside strings stay
|
|
73
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
74
|
+
const quote = ch;
|
|
75
|
+
out += ch;
|
|
76
|
+
i++;
|
|
77
|
+
while (i < source.length) {
|
|
78
|
+
const c = source[i];
|
|
79
|
+
out += c;
|
|
80
|
+
if (c === '\\' && i + 1 < source.length) {
|
|
81
|
+
out += source[i + 1];
|
|
82
|
+
i += 2;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (c === quote) {
|
|
86
|
+
i++;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
// template ${ } — shallow skip (good enough for comment strip)
|
|
90
|
+
if (quote === '`' && c === '$' && source[i + 1] === '{') {
|
|
91
|
+
out += '{';
|
|
92
|
+
i += 2;
|
|
93
|
+
let depth = 1;
|
|
94
|
+
while (i < source.length && depth > 0) {
|
|
95
|
+
if (source[i] === '{')
|
|
96
|
+
depth++;
|
|
97
|
+
else if (source[i] === '}')
|
|
98
|
+
depth--;
|
|
99
|
+
out += source[i];
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
i++;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (ch === '/' && next === '/') {
|
|
109
|
+
while (i < source.length && source[i] !== '\n')
|
|
110
|
+
i++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (ch === '/' && next === '*') {
|
|
114
|
+
i += 2;
|
|
115
|
+
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) {
|
|
116
|
+
out += source[i] === '\n' ? '\n' : ' ';
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
i += 2;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
out += ch;
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
65
127
|
export function extractFromSource(source, file) {
|
|
128
|
+
const code = stripComments(source);
|
|
66
129
|
const out = [];
|
|
67
130
|
const dynamicNamespaces = new Set();
|
|
68
131
|
const translators = new Map(); // var → ns
|
|
69
132
|
// useT('ns') | getT('ns') | getT(locale, 'ns')
|
|
70
133
|
const declRe = /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?(?:useT|getT)\s*\(\s*(?:[^'"\n,]+,\s*)?(['"])([^'"]*)\2\s*\)/g;
|
|
71
134
|
let m;
|
|
72
|
-
while ((m = declRe.exec(
|
|
135
|
+
while ((m = declRe.exec(code)) !== null) {
|
|
73
136
|
translators.set(m[1], m[3] || '_root');
|
|
74
137
|
}
|
|
75
138
|
const push = (ns, key, index) => {
|
|
@@ -79,27 +142,26 @@ export function extractFromSource(source, file) {
|
|
|
79
142
|
namespace: ns || '_root',
|
|
80
143
|
key,
|
|
81
144
|
file,
|
|
82
|
-
line: lineAt(
|
|
145
|
+
line: lineAt(code, index),
|
|
83
146
|
});
|
|
84
147
|
};
|
|
85
148
|
for (const [varName, ns] of translators) {
|
|
86
149
|
// Literal t('key') / t.rich('key')
|
|
87
150
|
const callRe = new RegExp(`\\b${escapeRegExp(varName)}(?:\\.rich)?\\s*\\(\\s*(['"])([^'"]+)\\1`, 'g');
|
|
88
151
|
let cm;
|
|
89
|
-
while ((cm = callRe.exec(
|
|
152
|
+
while ((cm = callRe.exec(code)) !== null) {
|
|
90
153
|
push(ns, cm[2], cm.index);
|
|
91
154
|
}
|
|
92
|
-
// t.dynamic(any, { from: ['a', 'b'] })
|
|
155
|
+
// t.dynamic(any, { from: ['a', 'b'] }) — only `from` keys become refs
|
|
93
156
|
const dynRe = new RegExp(`\\b${escapeRegExp(varName)}\\.dynamic\\s*\\(`, 'g');
|
|
94
157
|
let dm;
|
|
95
|
-
while ((dm = dynRe.exec(
|
|
96
|
-
const tail =
|
|
158
|
+
while ((dm = dynRe.exec(code)) !== null) {
|
|
159
|
+
const tail = code.slice(dm.index, dm.index + 400);
|
|
97
160
|
const fromMatch = /from\s*:\s*\[([^\]]*)\]/.exec(tail);
|
|
98
161
|
if (fromMatch) {
|
|
99
162
|
const keys = fromMatch[1].match(/['"]([^'"]+)['"]/g) ?? [];
|
|
100
163
|
for (const raw of keys) {
|
|
101
|
-
|
|
102
|
-
push(ns, key, dm.index);
|
|
164
|
+
push(ns, raw.slice(1, -1), dm.index);
|
|
103
165
|
}
|
|
104
166
|
}
|
|
105
167
|
dynamicNamespaces.add(ns || '_root');
|
|
@@ -107,28 +169,15 @@ export function extractFromSource(source, file) {
|
|
|
107
169
|
// t.ns('other') — whole namespace marked dynamic
|
|
108
170
|
const nsRe = new RegExp(`\\b${escapeRegExp(varName)}\\.ns\\s*\\(\\s*(['"])([^'"]+)\\1`, 'g');
|
|
109
171
|
let nm;
|
|
110
|
-
while ((nm = nsRe.exec(
|
|
172
|
+
while ((nm = nsRe.exec(code)) !== null) {
|
|
111
173
|
dynamicNamespaces.add(nm[2] || '_root');
|
|
112
174
|
}
|
|
113
|
-
// Non-literal t(var)
|
|
175
|
+
// Non-literal t(var) — suppress unused for the namespace only.
|
|
176
|
+
// Do NOT harvest unrelated string arrays (cache tags, sort keys, …).
|
|
114
177
|
const nonLitRe = new RegExp(`\\b${escapeRegExp(varName)}(?:\\.rich)?\\s*\\(\\s*(?!['"])([A-Za-z_$][\\w$]*)`, 'g');
|
|
115
|
-
|
|
116
|
-
let sawNonLit = false;
|
|
117
|
-
while ((nl = nonLitRe.exec(source)) !== null) {
|
|
118
|
-
sawNonLit = true;
|
|
178
|
+
if (nonLitRe.test(code)) {
|
|
119
179
|
dynamicNamespaces.add(ns || '_root');
|
|
120
180
|
}
|
|
121
|
-
if (sawNonLit) {
|
|
122
|
-
// Keys listed in string arrays in this file (e.g. KEYS.map((k) => t(k)))
|
|
123
|
-
const arrayLitRe = /\[\s*((?:['"][^'"]+['"]\s*,?\s*)+)\]/g;
|
|
124
|
-
let am;
|
|
125
|
-
while ((am = arrayLitRe.exec(source)) !== null) {
|
|
126
|
-
const keys = am[1].match(/['"]([^'"]+)['"]/g) ?? [];
|
|
127
|
-
for (const raw of keys) {
|
|
128
|
-
push(ns, raw.slice(1, -1), am.index);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
181
|
}
|
|
133
182
|
return { refs: out, dynamicNamespaces };
|
|
134
183
|
}
|
package/dist/next/loader.d.ts
CHANGED
|
@@ -6,6 +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: [...] })` to a map of those imports.
|
|
10
|
+
*
|
|
9
11
|
* Escape analysis: if `t` is passed around, falls back to namespace import + warning.
|
|
10
12
|
*/
|
|
11
13
|
export type LoaderOptions = {
|
|
@@ -17,6 +19,14 @@ export type LoaderResult = {
|
|
|
17
19
|
warnings: string[];
|
|
18
20
|
};
|
|
19
21
|
export declare function transformCallSites(source: string, resourcePath: string, _options?: LoaderOptions): LoaderResult;
|
|
22
|
+
/**
|
|
23
|
+
* Peel leading `"use client"` / `"use server"` (and preceding comments/whitespace)
|
|
24
|
+
* so we can re-emit them before any injected imports.
|
|
25
|
+
*/
|
|
26
|
+
export declare function splitLeadingDirectives(source: string): {
|
|
27
|
+
directives: string;
|
|
28
|
+
body: string;
|
|
29
|
+
};
|
|
20
30
|
/** Webpack/Turbopack loader entry. */
|
|
21
31
|
export default function tongueLoader(this: {
|
|
22
32
|
resourcePath: string;
|
package/dist/next/loader.js
CHANGED
|
@@ -6,6 +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: [...] })` to a map of those imports.
|
|
10
|
+
*
|
|
9
11
|
* Escape analysis: if `t` is passed around, falls back to namespace import + warning.
|
|
10
12
|
*/
|
|
11
13
|
const USE_T_DECL = /(?:const|let|var)\s+(\w+)\s*=\s*useT\s*\(\s*(['"])([^'"]*)\2\s*\)/g;
|
|
@@ -14,10 +16,11 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
14
16
|
if (!source.includes('useT(')) {
|
|
15
17
|
return { code: source, warnings };
|
|
16
18
|
}
|
|
19
|
+
const { directives, body: withoutDirs } = splitLeadingDirectives(source);
|
|
17
20
|
const translators = new Map(); // varName → namespace
|
|
18
21
|
let m;
|
|
19
22
|
const declRe = new RegExp(USE_T_DECL.source, 'g');
|
|
20
|
-
while ((m = declRe.exec(
|
|
23
|
+
while ((m = declRe.exec(withoutDirs)) !== null) {
|
|
21
24
|
translators.set(m[1], m[3] || '_root');
|
|
22
25
|
}
|
|
23
26
|
if (translators.size === 0) {
|
|
@@ -25,21 +28,36 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
25
28
|
}
|
|
26
29
|
// Escape analysis: if t is used beyond property calls, warn and skip rewrite
|
|
27
30
|
for (const [varName, ns] of translators) {
|
|
28
|
-
if (escapes(
|
|
31
|
+
if (escapes(withoutDirs, varName)) {
|
|
29
32
|
warnings.push(`TONGUE_W_ESCAPE: ${resourcePath}: '${varName}' from useT('${ns}') escapes this file; importing whole namespace`);
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
const imports = new Map();
|
|
33
|
-
let code =
|
|
36
|
+
let code = withoutDirs;
|
|
34
37
|
for (const [varName, ns] of translators) {
|
|
35
|
-
if (escapes(
|
|
38
|
+
if (escapes(withoutDirs, varName))
|
|
36
39
|
continue;
|
|
40
|
+
// Literal t('key') / t.rich('key')
|
|
37
41
|
const callRe = new RegExp(`\\b${escapeRegExp(varName)}(?:\\.rich)?\\s*\\(\\s*(['"])([^'"]+)\\1`, 'g');
|
|
38
42
|
code = code.replace(callRe, (full, _q, key) => {
|
|
39
43
|
const ident = safeImportIdent(ns, key);
|
|
40
44
|
imports.set(`${ns}:${key}`, { ns, key, ident });
|
|
41
45
|
return full;
|
|
42
46
|
});
|
|
47
|
+
// t.dynamic(expr, { from: ['a', 'b'] }, values?)
|
|
48
|
+
const dynRe = new RegExp(`\\b${escapeRegExp(varName)}\\.dynamic\\s*\\(`, 'g');
|
|
49
|
+
let dm;
|
|
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)
|
|
54
|
+
continue;
|
|
55
|
+
const keys = [...fromMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]);
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const ident = safeImportIdent(ns, key);
|
|
58
|
+
imports.set(`${ns}:${key}`, { ns, key, ident });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
if (imports.size === 0) {
|
|
45
63
|
return { code: source, warnings };
|
|
@@ -54,9 +72,29 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
54
72
|
const localeVar = '__tongue_locale';
|
|
55
73
|
// Inject locale once after the first useT declaration
|
|
56
74
|
code = code.replace(/((?:const|let|var)\s+\w+\s*=\s*useT\s*\([^)]*\))/, `$1\n const ${localeVar} = useLocale();`);
|
|
57
|
-
//
|
|
75
|
+
// Replace t.dynamic(expr, { from: [...] }, values?)
|
|
76
|
+
for (const [varName, ns] of translators) {
|
|
77
|
+
if (escapes(withoutDirs, varName))
|
|
78
|
+
continue;
|
|
79
|
+
const dynCallRe = new RegExp(`\\b${escapeRegExp(varName)}\\.dynamic\\s*\\(\\s*([^,]+?)\\s*,\\s*\\{[^}]*?from\\s*:\\s*\\[([^\\]]*)][^}]*\\}\\s*(?:,\\s*([^)]*?))?\\)`, 'g');
|
|
80
|
+
code = code.replace(dynCallRe, (_full, keyExpr, fromBody, values) => {
|
|
81
|
+
const keys = [...fromBody.matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]);
|
|
82
|
+
const mapEntries = keys
|
|
83
|
+
.map((key) => {
|
|
84
|
+
const ident = safeImportIdent(ns, key);
|
|
85
|
+
imports.set(`${ns}:${key}`, { ns, key, ident });
|
|
86
|
+
return `${JSON.stringify(key)}: ${ident}`;
|
|
87
|
+
})
|
|
88
|
+
.join(', ');
|
|
89
|
+
const valuesArg = values != null && String(values).trim() !== ''
|
|
90
|
+
? `, ${String(values).trim()}`
|
|
91
|
+
: '';
|
|
92
|
+
return `((__tongue_dyn) => { const __tongue_fn = __tongue_dyn[${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} })`;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Replace t('key', values) / t.rich('key', values) with per-key calls
|
|
58
96
|
for (const [varName, ns] of translators) {
|
|
59
|
-
if (escapes(
|
|
97
|
+
if (escapes(withoutDirs, varName))
|
|
60
98
|
continue;
|
|
61
99
|
for (const { key, ident } of [...imports.values()].filter((i) => i.ns === ns)) {
|
|
62
100
|
const richRe = new RegExp(`\\b${escapeRegExp(varName)}\\.rich\\s*\\(\\s*(['"])${escapeRegExp(key)}\\1\\s*(,\\s*([^)]*))?\\)`, 'g');
|
|
@@ -85,13 +123,40 @@ export function transformCallSites(source, resourcePath, _options = {}) {
|
|
|
85
123
|
preamble += `import { useLocale } from 'next-tongue/client';\n`;
|
|
86
124
|
}
|
|
87
125
|
for (const [varName, ns] of translators) {
|
|
88
|
-
if (!escapes(
|
|
126
|
+
if (!escapes(withoutDirs, varName))
|
|
89
127
|
continue;
|
|
90
128
|
preamble +=
|
|
91
129
|
`process.env.NODE_ENV !== 'production' && globalThis.__tongueDevEscape?.(` +
|
|
92
130
|
`${JSON.stringify(ns || '_root')}, ${JSON.stringify(resourcePath)});\n`;
|
|
93
131
|
}
|
|
94
|
-
|
|
132
|
+
// "use client" / "use server" MUST be the first statement(s) — never after imports.
|
|
133
|
+
return { code: directives + preamble + code, warnings };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Peel leading `"use client"` / `"use server"` (and preceding comments/whitespace)
|
|
137
|
+
* so we can re-emit them before any injected imports.
|
|
138
|
+
*/
|
|
139
|
+
export function splitLeadingDirectives(source) {
|
|
140
|
+
let rest = source.replace(/^\uFEFF/, '');
|
|
141
|
+
const found = [];
|
|
142
|
+
while (rest.length > 0) {
|
|
143
|
+
// leading whitespace / comments
|
|
144
|
+
const lead = rest.match(/^((?:\s|\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)*)/);
|
|
145
|
+
const leadLen = lead?.[0]?.length ?? 0;
|
|
146
|
+
const afterLead = rest.slice(leadLen);
|
|
147
|
+
const dir = afterLead.match(/^['"]use (client|server)['"];?[ \t]*(?:\r?\n)?/);
|
|
148
|
+
if (!dir)
|
|
149
|
+
break;
|
|
150
|
+
found.push(`"use ${dir[1]}";`);
|
|
151
|
+
rest = afterLead.slice(dir[0].length);
|
|
152
|
+
}
|
|
153
|
+
if (found.length === 0) {
|
|
154
|
+
return { directives: '', body: source.replace(/^\uFEFF/, '') };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
directives: found.join('\n') + '\n\n',
|
|
158
|
+
body: rest,
|
|
159
|
+
};
|
|
95
160
|
}
|
|
96
161
|
function escapes(source, varName) {
|
|
97
162
|
const v = escapeRegExp(varName);
|
package/dist/next/with-tongue.js
CHANGED
|
@@ -32,9 +32,11 @@ export function withTongue(nextConfig = {}, options = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
// Next 16: content must be RegExp; omit `as` when returning the same module type
|
|
34
34
|
// (using `as: '*.tsx'` previously doubled extensions → `file.tsx.tsx`).
|
|
35
|
+
// Do NOT gate on `browser` — client components still SSR, and useT must be
|
|
36
|
+
// rewritten on the server graph too (otherwise SSR throws without loadNamespace).
|
|
35
37
|
const tongueLoaderRule = {
|
|
36
38
|
condition: {
|
|
37
|
-
all: [{ not: 'foreign' },
|
|
39
|
+
all: [{ not: 'foreign' }, { content: /useT\s*\(/ }],
|
|
38
40
|
},
|
|
39
41
|
loaders: [
|
|
40
42
|
{
|
|
@@ -65,18 +67,17 @@ export function withTongue(nextConfig = {}, options = {}) {
|
|
|
65
67
|
...(config.resolve.alias ?? {}),
|
|
66
68
|
'#tongue': outDir,
|
|
67
69
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
70
|
+
// Client components SSR under webpack too — apply on both graphs.
|
|
71
|
+
config.module ??= {};
|
|
72
|
+
config.module.rules = [
|
|
73
|
+
...(config.module.rules ?? []),
|
|
74
|
+
{
|
|
75
|
+
test: /\.[jt]sx?$/,
|
|
76
|
+
exclude: /node_modules/,
|
|
77
|
+
enforce: 'pre',
|
|
78
|
+
use: [{ loader: loaderPath, options: { outDir: '.tongue' } }],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
80
81
|
return userWebpack ? userWebpack(config, ctx) : config;
|
|
81
82
|
};
|
|
82
83
|
const enableRootParams = peekRootParamsFlag(cwd, options.config);
|