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 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]
@@ -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 manually).
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 proxyPath = await detectProxyPath(cwd, appRoot);
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
- if (proxyPath) {
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: proxyPath,
78
+ path: detectProxyWritePath(appRoot),
70
79
  kind: 'create',
71
- content: proxyScaffold(locales, defaultLocale, prefix),
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('tongue init: existing proxy/middleware detected — do not overwrite. Compose like:\n\n' +
76
- proxyScaffold(locales, defaultLocale, prefix) +
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 manually).
360
+ * (caller should compose or pass `--proxy merge|overwrite`).
317
361
  */
318
362
  export async function detectProxyPath(cwd, appRoot) {
319
- const existing = [
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.startsWith('src/') ? 'src/proxy.ts' : 'proxy.ts';
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-tongue",
3
- "version": "0.9.13",
3
+ "version": "0.9.14",
4
4
  "description": "Compiler-first, type-safe i18n for the Next.js 16 App Router",
5
5
  "license": "MIT",
6
6
  "type": "module",