next-tongue 0.9.13 → 0.9.14
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/cli/index.js +17 -1
- package/dist/cli/init.d.ts +6 -1
- package/dist/cli/init.js +53 -19
- package/dist/cli/merge-proxy.d.ts +27 -0
- package/dist/cli/merge-proxy.js +145 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function runCli(argv) {
|
|
|
32
32
|
prefix: { type: 'string' },
|
|
33
33
|
export: { type: 'boolean', default: false },
|
|
34
34
|
force: { type: 'boolean', default: false },
|
|
35
|
+
proxy: { type: 'string' },
|
|
35
36
|
format: { type: 'string' },
|
|
36
37
|
out: { type: 'string' },
|
|
37
38
|
'missing-only': { type: 'boolean', default: false },
|
|
@@ -116,11 +117,26 @@ export async function runCli(argv) {
|
|
|
116
117
|
process.exitCode = 2;
|
|
117
118
|
break;
|
|
118
119
|
}
|
|
120
|
+
const proxyRaw = typeof values.proxy === 'string' ? values.proxy : undefined;
|
|
121
|
+
const proxyMode = proxyRaw === 'merge' ||
|
|
122
|
+
proxyRaw === 'skip' ||
|
|
123
|
+
proxyRaw === 'overwrite'
|
|
124
|
+
? proxyRaw
|
|
125
|
+
: undefined;
|
|
126
|
+
if (proxyRaw != null &&
|
|
127
|
+
proxyRaw !== 'merge' &&
|
|
128
|
+
proxyRaw !== 'skip' &&
|
|
129
|
+
proxyRaw !== 'overwrite') {
|
|
130
|
+
console.error(`Invalid --proxy ${JSON.stringify(proxyRaw)} (expected merge|skip|overwrite)`);
|
|
131
|
+
process.exitCode = 2;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
119
134
|
await cmdInit({
|
|
120
135
|
...(typeof values.cwd === 'string' ? { cwd: values.cwd } : {}),
|
|
121
136
|
...(typeof values.locales === 'string' ? { locales: values.locales } : {}),
|
|
122
137
|
...(typeof values.default === 'string' ? { default: values.default } : {}),
|
|
123
138
|
...(prefix ? { prefix } : {}),
|
|
139
|
+
...(proxyMode ? { proxyMode } : {}),
|
|
124
140
|
export: values.export === true,
|
|
125
141
|
yes: values.yes === true,
|
|
126
142
|
force: values.force === true,
|
|
@@ -270,7 +286,7 @@ Usage:
|
|
|
270
286
|
tongue build [--cwd <dir>] [--config <path>] [--json] [--quiet]
|
|
271
287
|
tongue check [--cwd <dir>] [--config <path>] [--locale de] [--reporter pretty|json|github] [--max-warnings N]
|
|
272
288
|
tongue extract [--include app,src] [--placeholder ''] [--prune] [--yes] [--dry-run]
|
|
273
|
-
tongue init [--locales en,de] [--default en] [--prefix as-needed] [--export] [--yes] [--force] [--dry-run]
|
|
289
|
+
tongue init [--locales en,de] [--default en] [--prefix as-needed] [--proxy merge|skip|overwrite] [--export] [--yes] [--force] [--dry-run]
|
|
274
290
|
tongue export --format xliff|csv [--locale de] [--out ./translations] [--missing-only]
|
|
275
291
|
tongue import <file.xlf|file.csv> [--dry-run]
|
|
276
292
|
tongue report [--json] [--max-bytes 4kb]
|
package/dist/cli/init.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
export type ProxyMode = 'merge' | 'skip' | 'overwrite';
|
|
1
2
|
export type InitCliOptions = {
|
|
2
3
|
cwd?: string;
|
|
3
4
|
locales?: string;
|
|
4
5
|
default?: string;
|
|
5
6
|
prefix?: 'always' | 'as-needed' | 'never';
|
|
7
|
+
/** How to treat an existing proxy.ts / middleware.ts. Default: skip. */
|
|
8
|
+
proxyMode?: ProxyMode;
|
|
6
9
|
export?: boolean;
|
|
7
10
|
yes?: boolean;
|
|
8
11
|
force?: boolean;
|
|
@@ -12,8 +15,10 @@ export type InitCliOptions = {
|
|
|
12
15
|
export declare function cmdInit(opts: InitCliOptions): Promise<void>;
|
|
13
16
|
/** Prefer src/app when present; otherwise app/. */
|
|
14
17
|
export declare function detectAppRoot(cwd: string): Promise<'app' | 'src/app'>;
|
|
18
|
+
/** Where to write a new proxy.ts when none exists. */
|
|
19
|
+
export declare function detectProxyWritePath(appRoot: 'app' | 'src/app'): string;
|
|
15
20
|
/**
|
|
16
21
|
* Where to write proxy.ts. Returns null when the project already has proxy/middleware
|
|
17
|
-
* (caller should compose
|
|
22
|
+
* (caller should compose or pass `--proxy merge|overwrite`).
|
|
18
23
|
*/
|
|
19
24
|
export declare function detectProxyPath(cwd: string, appRoot: 'app' | 'src/app'): Promise<string | null>;
|
package/dist/cli/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
3
|
import * as readline from 'node:readline';
|
|
4
|
+
import { findExistingProxyPath, mergeProxySource, PROXY_CANDIDATES, } from './merge-proxy.js';
|
|
4
5
|
import { resolveInsideProject } from './safe-path.js';
|
|
5
6
|
export async function cmdInit(opts) {
|
|
6
7
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -36,7 +37,7 @@ export async function cmdInit(opts) {
|
|
|
36
37
|
console.log(`tongue init: using ${appRoot}/ for App Router files`);
|
|
37
38
|
}
|
|
38
39
|
const hasRootLayout = await hasFile(cwd, `${appRoot}/layout.tsx`, `${appRoot}/layout.ts`, `${appRoot}/layout.jsx`, `${appRoot}/layout.js`);
|
|
39
|
-
const
|
|
40
|
+
const existingProxy = await findExistingProxyPath(cwd, (rel) => exists(join(cwd, rel)));
|
|
40
41
|
const plans = [];
|
|
41
42
|
plans.push({
|
|
42
43
|
path: 'tongue.config.ts',
|
|
@@ -64,16 +65,55 @@ export default defineConfig({
|
|
|
64
65
|
content: `${JSON.stringify(sample, null, 2)}\n`,
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
let proxyMode = opts.proxyMode ?? 'skip';
|
|
69
|
+
if (existingProxy && opts.proxyMode == null && interactive) {
|
|
70
|
+
const answer = await ask(`Existing ${existingProxy} — merge|skip|overwrite [skip]: `, 'skip');
|
|
71
|
+
if (answer === 'merge' || answer === 'skip' || answer === 'overwrite') {
|
|
72
|
+
proxyMode = answer;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const scaffold = proxyScaffold(locales, defaultLocale, prefix);
|
|
76
|
+
if (!existingProxy) {
|
|
68
77
|
plans.push({
|
|
69
|
-
path:
|
|
78
|
+
path: detectProxyWritePath(appRoot),
|
|
70
79
|
kind: 'create',
|
|
71
|
-
content:
|
|
80
|
+
content: scaffold,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else if (proxyMode === 'overwrite') {
|
|
84
|
+
plans.push({
|
|
85
|
+
path: existingProxy,
|
|
86
|
+
kind: 'merge',
|
|
87
|
+
content: scaffold,
|
|
72
88
|
});
|
|
73
89
|
}
|
|
90
|
+
else if (proxyMode === 'merge') {
|
|
91
|
+
const abs = join(cwd, existingProxy);
|
|
92
|
+
const src = await readFile(abs, 'utf8');
|
|
93
|
+
const merged = mergeProxySource(src, {
|
|
94
|
+
locales,
|
|
95
|
+
defaultLocale,
|
|
96
|
+
prefix,
|
|
97
|
+
});
|
|
98
|
+
if (merged.status === 'merged') {
|
|
99
|
+
plans.push({
|
|
100
|
+
path: existingProxy,
|
|
101
|
+
kind: 'merge',
|
|
102
|
+
content: merged.code,
|
|
103
|
+
});
|
|
104
|
+
if (!opts.quiet && merged.note) {
|
|
105
|
+
console.log(`tongue init: ${merged.note}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (!opts.quiet) {
|
|
109
|
+
console.log(`tongue init: could not merge ${existingProxy} (${merged.reason}). Compose like:\n\n` +
|
|
110
|
+
scaffold +
|
|
111
|
+
'\nSee docs/routing-and-proxy.md#composing-with-your-own-proxy-logic.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
74
114
|
else if (!opts.quiet) {
|
|
75
|
-
console.log(
|
|
76
|
-
|
|
115
|
+
console.log(`tongue init: existing ${existingProxy} — skipped (pass --proxy merge to wire createProxy, or --proxy overwrite). Compose like:\n\n` +
|
|
116
|
+
scaffold +
|
|
77
117
|
'\nSee docs/routing-and-proxy.md#composing-with-your-own-proxy-logic / hybrid routing.');
|
|
78
118
|
}
|
|
79
119
|
const layoutPath = `${appRoot}/[locale]/layout.tsx`;
|
|
@@ -311,26 +351,20 @@ export async function detectAppRoot(cwd) {
|
|
|
311
351
|
return 'src/app';
|
|
312
352
|
return 'app';
|
|
313
353
|
}
|
|
354
|
+
/** Where to write a new proxy.ts when none exists. */
|
|
355
|
+
export function detectProxyWritePath(appRoot) {
|
|
356
|
+
return appRoot.startsWith('src/') ? 'src/proxy.ts' : 'proxy.ts';
|
|
357
|
+
}
|
|
314
358
|
/**
|
|
315
359
|
* Where to write proxy.ts. Returns null when the project already has proxy/middleware
|
|
316
|
-
* (caller should compose
|
|
360
|
+
* (caller should compose or pass `--proxy merge|overwrite`).
|
|
317
361
|
*/
|
|
318
362
|
export async function detectProxyPath(cwd, appRoot) {
|
|
319
|
-
const
|
|
320
|
-
'proxy.ts',
|
|
321
|
-
'proxy.js',
|
|
322
|
-
'src/proxy.ts',
|
|
323
|
-
'src/proxy.js',
|
|
324
|
-
'middleware.ts',
|
|
325
|
-
'middleware.js',
|
|
326
|
-
'src/middleware.ts',
|
|
327
|
-
'src/middleware.js',
|
|
328
|
-
];
|
|
329
|
-
for (const p of existing) {
|
|
363
|
+
for (const p of PROXY_CANDIDATES) {
|
|
330
364
|
if (await exists(join(cwd, p)))
|
|
331
365
|
return null;
|
|
332
366
|
}
|
|
333
|
-
return appRoot
|
|
367
|
+
return detectProxyWritePath(appRoot);
|
|
334
368
|
}
|
|
335
369
|
function wrapNextConfig(src, staticExport) {
|
|
336
370
|
let out = src;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type MergeProxyOptions = {
|
|
2
|
+
locales: readonly string[];
|
|
3
|
+
defaultLocale: string;
|
|
4
|
+
prefix: string;
|
|
5
|
+
};
|
|
6
|
+
export type MergeProxyResult = {
|
|
7
|
+
status: 'unchanged';
|
|
8
|
+
reason: string;
|
|
9
|
+
code: string;
|
|
10
|
+
} | {
|
|
11
|
+
status: 'merged';
|
|
12
|
+
code: string;
|
|
13
|
+
note?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Merge `createProxy({ locales, defaultLocale, prefix })` into an existing
|
|
17
|
+
* proxy.ts / middleware.ts without wiping user logic.
|
|
18
|
+
*
|
|
19
|
+
* - Adds the import + `const tongue = createProxy(...)`.
|
|
20
|
+
* - Replaces bare `return NextResponse.next()` with `return tongue(<req>)`
|
|
21
|
+
* using the handler's first parameter name.
|
|
22
|
+
* - If already using `createProxy`, leaves the file alone.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mergeProxySource(source: string, opts: MergeProxyOptions): MergeProxyResult;
|
|
25
|
+
export declare const PROXY_CANDIDATES: readonly ["proxy.ts", "proxy.js", "src/proxy.ts", "src/proxy.js", "middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
26
|
+
/** Relative path of an existing proxy/middleware file, or null. */
|
|
27
|
+
export declare function findExistingProxyPath(cwd: string, exists: (rel: string) => Promise<boolean>): Promise<string | null>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const CREATE_PROXY_IMPORT = /import\s*\{([^}]*)\}\s*from\s*['"]next-tongue\/proxy['"]/;
|
|
2
|
+
/**
|
|
3
|
+
* Merge `createProxy({ locales, defaultLocale, prefix })` into an existing
|
|
4
|
+
* proxy.ts / middleware.ts without wiping user logic.
|
|
5
|
+
*
|
|
6
|
+
* - Adds the import + `const tongue = createProxy(...)`.
|
|
7
|
+
* - Replaces bare `return NextResponse.next()` with `return tongue(<req>)`
|
|
8
|
+
* using the handler's first parameter name.
|
|
9
|
+
* - If already using `createProxy`, leaves the file alone.
|
|
10
|
+
*/
|
|
11
|
+
export function mergeProxySource(source, opts) {
|
|
12
|
+
if (/\bcreateProxy\s*\(/.test(source)) {
|
|
13
|
+
return {
|
|
14
|
+
status: 'unchanged',
|
|
15
|
+
reason: 'createProxy already present',
|
|
16
|
+
code: source,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
let code = source.replace(/^\uFEFF/, '');
|
|
20
|
+
const reqIdent = detectRequestIdent(code) ?? 'req';
|
|
21
|
+
const tongueIdent = /\btongue\b/.test(code) ? '__tongueProxy' : 'tongue';
|
|
22
|
+
code = ensureCreateProxyImport(code);
|
|
23
|
+
code = insertTongueConst(code, tongueIdent, opts);
|
|
24
|
+
// Only the *last* NextResponse.next() — early returns for /api etc. stay passthrough.
|
|
25
|
+
const replaced = replaceLastNextReturn(code, tongueIdent, reqIdent);
|
|
26
|
+
if (replaced) {
|
|
27
|
+
return {
|
|
28
|
+
status: 'merged',
|
|
29
|
+
code: ensureTrailingNewline(replaced),
|
|
30
|
+
note: `Replaced final NextResponse.next() with ${tongueIdent}(${reqIdent}). Early next() returns (allow-lists) are kept.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// No next() — wrap exported handler so user logic runs first, then tongue.
|
|
34
|
+
const wrapped = wrapExportedHandler(code, tongueIdent, reqIdent);
|
|
35
|
+
if (wrapped) {
|
|
36
|
+
return {
|
|
37
|
+
status: 'merged',
|
|
38
|
+
code: ensureTrailingNewline(wrapped),
|
|
39
|
+
note: `Wrapped existing handler: user Response wins unless it is a passthrough next; otherwise ${tongueIdent} runs.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
status: 'unchanged',
|
|
44
|
+
reason: 'could not find NextResponse.next() or an exportable proxy/middleware handler to wrap',
|
|
45
|
+
code: source,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function ensureTrailingNewline(s) {
|
|
49
|
+
return s.endsWith('\n') ? s : `${s}\n`;
|
|
50
|
+
}
|
|
51
|
+
function ensureCreateProxyImport(code) {
|
|
52
|
+
if (CREATE_PROXY_IMPORT.test(code)) {
|
|
53
|
+
return code.replace(CREATE_PROXY_IMPORT, (full, specs) => {
|
|
54
|
+
if (/\bcreateProxy\b/.test(specs))
|
|
55
|
+
return full;
|
|
56
|
+
const parts = specs
|
|
57
|
+
.split(',')
|
|
58
|
+
.map((s) => s.trim())
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
parts.push('createProxy');
|
|
61
|
+
return full.replace(specs, ` ${parts.join(', ')} `);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const importBlock = `import { createProxy } from 'next-tongue/proxy';\n`;
|
|
65
|
+
// After last import, or at top
|
|
66
|
+
const lastImport = [...code.matchAll(/^import\s.+;?\s*$/gm)].pop();
|
|
67
|
+
if (lastImport && lastImport.index != null) {
|
|
68
|
+
const end = lastImport.index + lastImport[0].length;
|
|
69
|
+
return code.slice(0, end) + '\n' + importBlock + code.slice(end);
|
|
70
|
+
}
|
|
71
|
+
return importBlock + code;
|
|
72
|
+
}
|
|
73
|
+
function insertTongueConst(code, tongueIdent, opts) {
|
|
74
|
+
if (new RegExp(`\\b(?:const|let|var)\\s+${tongueIdent}\\s*=`).test(code)) {
|
|
75
|
+
return code;
|
|
76
|
+
}
|
|
77
|
+
const decl = `const ${tongueIdent} = createProxy({\n` +
|
|
78
|
+
` locales: ${JSON.stringify([...opts.locales])},\n` +
|
|
79
|
+
` defaultLocale: ${JSON.stringify(opts.defaultLocale)},\n` +
|
|
80
|
+
` prefix: ${JSON.stringify(opts.prefix)},\n` +
|
|
81
|
+
`});\n`;
|
|
82
|
+
// Prefer after imports / 'use …' directives
|
|
83
|
+
const lastImport = [...code.matchAll(/^import\s.+;?\s*$/gm)].pop();
|
|
84
|
+
if (lastImport && lastImport.index != null) {
|
|
85
|
+
const end = lastImport.index + lastImport[0].length;
|
|
86
|
+
return code.slice(0, end) + '\n\n' + decl + code.slice(end);
|
|
87
|
+
}
|
|
88
|
+
return decl + '\n' + code;
|
|
89
|
+
}
|
|
90
|
+
function replaceLastNextReturn(code, tongueIdent, reqIdent) {
|
|
91
|
+
const re = /\breturn\s+NextResponse\.next\s*\(\s*\)\s*;?/g;
|
|
92
|
+
let last = null;
|
|
93
|
+
let m;
|
|
94
|
+
while ((m = re.exec(code)) !== null)
|
|
95
|
+
last = m;
|
|
96
|
+
if (!last)
|
|
97
|
+
return null;
|
|
98
|
+
return (code.slice(0, last.index) +
|
|
99
|
+
`return ${tongueIdent}(${reqIdent});` +
|
|
100
|
+
code.slice(last.index + last[0].length));
|
|
101
|
+
}
|
|
102
|
+
function detectRequestIdent(code) {
|
|
103
|
+
const m = /export\s+(?:async\s+)?function\s+(?:proxy|middleware)\s*\(\s*([A-Za-z_$][\w$]*)/.exec(code) ??
|
|
104
|
+
/export\s+const\s+(?:proxy|middleware)\s*=\s*(?:async\s*)?\(\s*([A-Za-z_$][\w$]*)/.exec(code);
|
|
105
|
+
return m?.[1] ?? null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Rename `export function proxy/middleware` → internal, add wrapper that
|
|
109
|
+
* prefers a non-passthrough user Response then falls back to tongue.
|
|
110
|
+
*/
|
|
111
|
+
function wrapExportedHandler(code, tongueIdent, reqIdent) {
|
|
112
|
+
const fnRe = /export\s+(async\s+)?function\s+(proxy|middleware)\s*\(/;
|
|
113
|
+
const m = fnRe.exec(code);
|
|
114
|
+
if (!m || m.index == null)
|
|
115
|
+
return null;
|
|
116
|
+
const asyncKw = m[1] ?? '';
|
|
117
|
+
const name = m[2];
|
|
118
|
+
const inner = `__tongue_user_${name}`;
|
|
119
|
+
let out = code.slice(0, m.index) + `${asyncKw}function ${inner}(` + code.slice(m.index + m[0].length);
|
|
120
|
+
out +=
|
|
121
|
+
`\n\nexport ${asyncKw}function ${name}(${reqIdent}: Parameters<typeof ${inner}>[0]) {\n` +
|
|
122
|
+
` const __user = ${asyncKw ? 'await ' : ''}${inner}(${reqIdent});\n` +
|
|
123
|
+
` if (__user && __user.headers?.get?.('x-middleware-next') !== '1') return __user;\n` +
|
|
124
|
+
` return ${tongueIdent}(${reqIdent});\n` +
|
|
125
|
+
`}\n`;
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
export const PROXY_CANDIDATES = [
|
|
129
|
+
'proxy.ts',
|
|
130
|
+
'proxy.js',
|
|
131
|
+
'src/proxy.ts',
|
|
132
|
+
'src/proxy.js',
|
|
133
|
+
'middleware.ts',
|
|
134
|
+
'middleware.js',
|
|
135
|
+
'src/middleware.ts',
|
|
136
|
+
'src/middleware.js',
|
|
137
|
+
];
|
|
138
|
+
/** Relative path of an existing proxy/middleware file, or null. */
|
|
139
|
+
export async function findExistingProxyPath(cwd, exists) {
|
|
140
|
+
for (const p of PROXY_CANDIDATES) {
|
|
141
|
+
if (await exists(p))
|
|
142
|
+
return p;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|