next-tongue 0.9.4 → 0.9.5
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.
|
@@ -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}`;
|
|
@@ -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/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);
|