next-tongue 0.9.14 → 0.9.15
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/merge-proxy.js +44 -4
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -0
- package/dist/compiler/codegen/emit-types.js +3 -2
- package/dist/navigation/pathname.d.ts +11 -0
- package/dist/navigation/pathname.js +46 -13
- package/dist/proxy/index.d.ts +15 -0
- package/dist/proxy/index.js +108 -61
- package/package.json +1 -1
package/dist/cli/merge-proxy.js
CHANGED
|
@@ -27,7 +27,9 @@ export function mergeProxySource(source, opts) {
|
|
|
27
27
|
return {
|
|
28
28
|
status: 'merged',
|
|
29
29
|
code: ensureTrailingNewline(replaced),
|
|
30
|
-
note: `Replaced final NextResponse.next() with ${tongueIdent}(${reqIdent}).
|
|
30
|
+
note: `Replaced final NextResponse.next() with ${tongueIdent}(${reqIdent}). ` +
|
|
31
|
+
`Early next() returns (allow-lists) are kept. ` +
|
|
32
|
+
`If you stamp CSP/nonce on next(), use createProxy({ wrapResponse }) or mergeProxyResponse — see routing docs.`,
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
35
|
// No next() — wrap exported handler so user logic runs first, then tongue.
|
|
@@ -36,7 +38,9 @@ export function mergeProxySource(source, opts) {
|
|
|
36
38
|
return {
|
|
37
39
|
status: 'merged',
|
|
38
40
|
code: ensureTrailingNewline(wrapped),
|
|
39
|
-
note: `Wrapped existing handler: user Response wins
|
|
41
|
+
note: `Wrapped existing handler: non-passthrough user Response wins (auth/maintenance); ` +
|
|
42
|
+
`passthrough next() → ${tongueIdent} with mergeProxyResponse (keeps CSP/nonce). ` +
|
|
43
|
+
`Rewrites to paths under [locale] must include the locale segment.`,
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
46
|
return {
|
|
@@ -117,14 +121,50 @@ function wrapExportedHandler(code, tongueIdent, reqIdent) {
|
|
|
117
121
|
const name = m[2];
|
|
118
122
|
const inner = `__tongue_user_${name}`;
|
|
119
123
|
let out = code.slice(0, m.index) + `${asyncKw}function ${inner}(` + code.slice(m.index + m[0].length);
|
|
124
|
+
// Prefer non-passthrough user Response (auth redirect, maintenance rewrite).
|
|
125
|
+
// Passthrough next() → tongue, then mergeProxyResponse so CSP/nonce on the
|
|
126
|
+
// user next() are not dropped.
|
|
127
|
+
out = ensureMergeProxyResponseImport(out);
|
|
128
|
+
// Outer wrapper is always async — createProxy returns a Promise.
|
|
120
129
|
out +=
|
|
121
|
-
`\n\nexport
|
|
130
|
+
`\n\nexport async function ${name}(${reqIdent}: Parameters<typeof ${inner}>[0]) {\n` +
|
|
122
131
|
` const __user = ${asyncKw ? 'await ' : ''}${inner}(${reqIdent});\n` +
|
|
123
132
|
` if (__user && __user.headers?.get?.('x-middleware-next') !== '1') return __user;\n` +
|
|
124
|
-
`
|
|
133
|
+
` const __tongue = await ${tongueIdent}(${reqIdent});\n` +
|
|
134
|
+
` return __user ? mergeProxyResponse(__user, __tongue) : __tongue;\n` +
|
|
125
135
|
`}\n`;
|
|
126
136
|
return out;
|
|
127
137
|
}
|
|
138
|
+
function ensureMergeProxyResponseImport(code) {
|
|
139
|
+
if (/\bmergeProxyResponse\b/.test(code) && CREATE_PROXY_IMPORT.test(code)) {
|
|
140
|
+
return code.replace(CREATE_PROXY_IMPORT, (full, specs) => {
|
|
141
|
+
if (/\bmergeProxyResponse\b/.test(specs))
|
|
142
|
+
return full;
|
|
143
|
+
const parts = specs
|
|
144
|
+
.split(',')
|
|
145
|
+
.map((s) => s.trim())
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
parts.push('mergeProxyResponse');
|
|
148
|
+
return full.replace(specs, ` ${parts.join(', ')} `);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// ensureCreateProxyImport already ran — add mergeProxyResponse to that import
|
|
152
|
+
if (CREATE_PROXY_IMPORT.test(code)) {
|
|
153
|
+
return code.replace(CREATE_PROXY_IMPORT, (full, specs) => {
|
|
154
|
+
if (/\bmergeProxyResponse\b/.test(specs))
|
|
155
|
+
return full;
|
|
156
|
+
const parts = specs
|
|
157
|
+
.split(',')
|
|
158
|
+
.map((s) => s.trim())
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
if (!parts.includes('createProxy'))
|
|
161
|
+
parts.unshift('createProxy');
|
|
162
|
+
parts.push('mergeProxyResponse');
|
|
163
|
+
return full.replace(specs, ` ${parts.join(', ')} `);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return code;
|
|
167
|
+
}
|
|
128
168
|
export const PROXY_CANDIDATES = [
|
|
129
169
|
'proxy.ts',
|
|
130
170
|
'proxy.js',
|
package/dist/client/index.d.ts
CHANGED
|
@@ -39,5 +39,7 @@ export declare const defaultLocale: {
|
|
|
39
39
|
export type { TFunction, Format };
|
|
40
40
|
export type { SanctionedUnit, SimpleUnit, DisplayNameType, } from '../runtime/format.js';
|
|
41
41
|
export { isSanctionedUnit, SIMPLE_UNITS } from '../runtime/format.js';
|
|
42
|
+
/** Isomorphic — safe in client HtmlLang helpers. */
|
|
43
|
+
export { getDir } from '../runtime/locale.js';
|
|
42
44
|
export { translateIssues, defaultFormatPath, } from '../runtime/translate-issues.js';
|
|
43
45
|
export type { TranslateIssue, TranslateIssuesOptions, IssuePathSegment, } from '../runtime/translate-issues.js';
|
package/dist/client/index.js
CHANGED
|
@@ -116,4 +116,6 @@ export const defaultLocale = {
|
|
|
116
116
|
},
|
|
117
117
|
};
|
|
118
118
|
export { isSanctionedUnit, SIMPLE_UNITS } from '../runtime/format.js';
|
|
119
|
+
/** Isomorphic — safe in client HtmlLang helpers. */
|
|
120
|
+
export { getDir } from '../runtime/locale.js';
|
|
119
121
|
export { translateIssues, defaultFormatPath, } from '../runtime/translate-issues.js';
|
|
@@ -14,6 +14,7 @@ export function emitTypesFile(opts) {
|
|
|
14
14
|
: 'string';
|
|
15
15
|
return `/* eslint-disable */
|
|
16
16
|
// Generated by next-tongue. Do not edit.
|
|
17
|
+
import type { ReactNode } from 'react';
|
|
17
18
|
declare module 'next-tongue' {
|
|
18
19
|
interface TongueRegister {
|
|
19
20
|
locales: ${localeUnion};
|
|
@@ -46,8 +47,8 @@ function argsToType(args) {
|
|
|
46
47
|
}
|
|
47
48
|
case 'rich':
|
|
48
49
|
return kind.selfClosing
|
|
49
|
-
? `${JSON.stringify(name)}: () =>
|
|
50
|
-
: `${JSON.stringify(name)}: (chunks:
|
|
50
|
+
? `${JSON.stringify(name)}: () => ReactNode`
|
|
51
|
+
: `${JSON.stringify(name)}: (chunks: ReactNode) => ReactNode`;
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
54
|
return `{ ${fields.join('; ')} }`;
|
|
@@ -2,6 +2,8 @@ export type PathnameInput = string | {
|
|
|
2
2
|
pathname: string;
|
|
3
3
|
params?: Record<string, string | number>;
|
|
4
4
|
query?: Record<string, string | number | undefined>;
|
|
5
|
+
/** Fragment without requiring `#` (e.g. `'preferences'` or `'#preferences'`). */
|
|
6
|
+
hash?: string;
|
|
5
7
|
};
|
|
6
8
|
export type GetPathnameArgs = {
|
|
7
9
|
href: PathnameInput;
|
|
@@ -15,6 +17,9 @@ export type GetPathnameArgs = {
|
|
|
15
17
|
};
|
|
16
18
|
/**
|
|
17
19
|
* Build a localized public pathname for links, redirects, and sitemaps.
|
|
20
|
+
* Query strings and hashes are preserved (stripped before pathnames/prefix,
|
|
21
|
+
* reattached after). Prefer the object form for typed `pathnames` unions:
|
|
22
|
+
* `{ pathname: '/dashboard', query: { demo: 1 }, hash: 'prefs' }`.
|
|
18
23
|
*/
|
|
19
24
|
export declare function getPathname(args: GetPathnameArgs): string;
|
|
20
25
|
/** Strip a leading locale segment from a pathname. */
|
|
@@ -24,3 +29,9 @@ export declare function stripLocalePrefix(pathname: string, locales: readonly st
|
|
|
24
29
|
* used in `routing.pathnames` (e.g. `/ueber-uns` → `/about` for `de`).
|
|
25
30
|
*/
|
|
26
31
|
export declare function toInternalPathname(pathname: string, locale: string, pathnames?: Record<string, Partial<Record<string, string>>>): string;
|
|
32
|
+
/** Split `/path?q=1#hash` into parts (pathnames lookup needs the path alone). */
|
|
33
|
+
export declare function splitPathSearchHash(full: string): {
|
|
34
|
+
pathname: string;
|
|
35
|
+
search: string;
|
|
36
|
+
hash: string;
|
|
37
|
+
};
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build a localized public pathname for links, redirects, and sitemaps.
|
|
3
|
+
* Query strings and hashes are preserved (stripped before pathnames/prefix,
|
|
4
|
+
* reattached after). Prefer the object form for typed `pathnames` unions:
|
|
5
|
+
* `{ pathname: '/dashboard', query: { demo: 1 }, hash: 'prefs' }`.
|
|
3
6
|
*/
|
|
4
7
|
export function getPathname(args) {
|
|
5
8
|
const { href, locale, prefix = 'as-needed', defaultLocale = 'en', pathnames, } = args;
|
|
6
|
-
|
|
9
|
+
const { pathname: rawPath, search, hash } = resolveHref(href);
|
|
10
|
+
let path = rawPath;
|
|
7
11
|
if (pathnames) {
|
|
8
12
|
const localized = pathnames[path]?.[locale];
|
|
9
13
|
if (localized)
|
|
10
14
|
path = localized;
|
|
11
15
|
}
|
|
12
|
-
if (prefix === 'never')
|
|
13
|
-
return path;
|
|
16
|
+
if (prefix === 'never') {
|
|
17
|
+
return path + search + hash;
|
|
18
|
+
}
|
|
14
19
|
if (prefix === 'as-needed' && locale === defaultLocale) {
|
|
15
|
-
return path;
|
|
20
|
+
return path + search + hash;
|
|
16
21
|
}
|
|
17
22
|
// always, or as-needed for non-default
|
|
18
|
-
if (path === '/')
|
|
19
|
-
return `/${locale}
|
|
20
|
-
|
|
23
|
+
if (path === '/') {
|
|
24
|
+
return `/${locale}` + search + hash;
|
|
25
|
+
}
|
|
26
|
+
return (`/${locale}${path.startsWith('/') ? path : `/${path}`}` + search + hash);
|
|
21
27
|
}
|
|
22
28
|
/** Strip a leading locale segment from a pathname. */
|
|
23
29
|
export function stripLocalePrefix(pathname, locales) {
|
|
@@ -43,14 +49,21 @@ export function toInternalPathname(pathname, locale, pathnames) {
|
|
|
43
49
|
return pathname;
|
|
44
50
|
}
|
|
45
51
|
function resolveHref(href) {
|
|
46
|
-
if (typeof href === 'string')
|
|
47
|
-
return href.startsWith('/') ? href : `/${href}
|
|
48
|
-
|
|
52
|
+
if (typeof href === 'string') {
|
|
53
|
+
return splitPathSearchHash(href.startsWith('/') ? href : `/${href}`);
|
|
54
|
+
}
|
|
55
|
+
let path = href.pathname.startsWith('/')
|
|
56
|
+
? href.pathname
|
|
57
|
+
: `/${href.pathname}`;
|
|
58
|
+
// Object pathname should be path-only; tolerate accidental ?/#.
|
|
59
|
+
const split = splitPathSearchHash(path);
|
|
60
|
+
path = split.pathname;
|
|
49
61
|
if (href.params) {
|
|
50
62
|
for (const [key, value] of Object.entries(href.params)) {
|
|
51
63
|
path = path.replace(`[${key}]`, encodeURIComponent(String(value)));
|
|
52
64
|
}
|
|
53
65
|
}
|
|
66
|
+
let search = split.search;
|
|
54
67
|
if (href.query) {
|
|
55
68
|
const qs = new URLSearchParams();
|
|
56
69
|
for (const [key, value] of Object.entries(href.query)) {
|
|
@@ -58,8 +71,28 @@ function resolveHref(href) {
|
|
|
58
71
|
qs.set(key, String(value));
|
|
59
72
|
}
|
|
60
73
|
const s = qs.toString();
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
search = s ? `?${s}` : '';
|
|
75
|
+
}
|
|
76
|
+
let hash = split.hash;
|
|
77
|
+
if (href.hash != null && href.hash !== '') {
|
|
78
|
+
hash = href.hash.startsWith('#') ? href.hash : `#${href.hash}`;
|
|
79
|
+
}
|
|
80
|
+
return { pathname: path, search, hash };
|
|
81
|
+
}
|
|
82
|
+
/** Split `/path?q=1#hash` into parts (pathnames lookup needs the path alone). */
|
|
83
|
+
export function splitPathSearchHash(full) {
|
|
84
|
+
let path = full;
|
|
85
|
+
let hash = '';
|
|
86
|
+
let search = '';
|
|
87
|
+
const hashIdx = path.indexOf('#');
|
|
88
|
+
if (hashIdx >= 0) {
|
|
89
|
+
hash = path.slice(hashIdx);
|
|
90
|
+
path = path.slice(0, hashIdx);
|
|
91
|
+
}
|
|
92
|
+
const qIdx = path.indexOf('?');
|
|
93
|
+
if (qIdx >= 0) {
|
|
94
|
+
search = path.slice(qIdx);
|
|
95
|
+
path = path.slice(0, qIdx);
|
|
63
96
|
}
|
|
64
|
-
return path;
|
|
97
|
+
return { pathname: path || '/', search, hash };
|
|
65
98
|
}
|
package/dist/proxy/index.d.ts
CHANGED
|
@@ -47,6 +47,12 @@ export type CreateProxyOptions = {
|
|
|
47
47
|
/** Override detection (domain → cookie → Accept-Language → default). */
|
|
48
48
|
resolveLocale?: (req: TongueProxyRequest) => string | undefined | Promise<string | undefined>;
|
|
49
49
|
onLocale?: (locale: string, req: TongueProxyRequest) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Post-process every tongue response (redirect / rewrite / next).
|
|
52
|
+
* Use to re-stamp CSP / nonce headers after locale routing — tongue does
|
|
53
|
+
* not preserve headers from a prior `NextResponse.next()`.
|
|
54
|
+
*/
|
|
55
|
+
wrapResponse?: (res: Response, req: TongueProxyRequest) => Response | Promise<Response>;
|
|
50
56
|
};
|
|
51
57
|
/** Default locale cookie name (`SameSite=Lax`). Use with `requestLocale` in API routes. */
|
|
52
58
|
export declare const LOCALE_COOKIE_NAME = "TONGUE_LOCALE";
|
|
@@ -57,6 +63,15 @@ export declare const LOCALE_COOKIE_NAME = "TONGUE_LOCALE";
|
|
|
57
63
|
* @see docs/routing-and-proxy.md
|
|
58
64
|
*/
|
|
59
65
|
export declare function createProxy(options: CreateProxyOptions): (req: TongueProxyRequest) => Response | Promise<Response>;
|
|
66
|
+
/**
|
|
67
|
+
* Copy response headers from a prior `NextResponse.next()` (CSP, nonce, …)
|
|
68
|
+
* onto a tongue rewrite/redirect/next response. Skips middleware control
|
|
69
|
+
* headers (`x-middleware-*`, `Location` when tongue already set one).
|
|
70
|
+
*
|
|
71
|
+
* Use when you stamp headers on `next()` then call tongue — tongue alone
|
|
72
|
+
* cannot see those headers.
|
|
73
|
+
*/
|
|
74
|
+
export declare function mergeProxyResponse(prior: Response, tongue: Response): Response;
|
|
60
75
|
/** Map internal path → localized public path for a locale. */
|
|
61
76
|
export declare function publicPathForLocale(internal: string, locale: string, pathnames?: Record<string, Partial<Record<string, string>>>): string;
|
|
62
77
|
/** Resolve `Host` / `x-forwarded-host` against `routing.domains`. */
|
package/dist/proxy/index.js
CHANGED
|
@@ -37,78 +37,125 @@ export function createProxy(options) {
|
|
|
37
37
|
if (!localeSet.has(defaultLocale)) {
|
|
38
38
|
throw new Error(`[next-tongue] createProxy: defaultLocale "${defaultLocale}" is not in locales`);
|
|
39
39
|
}
|
|
40
|
+
const wrap = options.wrapResponse;
|
|
40
41
|
return async (req) => {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
42
|
+
const res = await handleProxyRequest(req, {
|
|
43
|
+
locales,
|
|
44
|
+
defaultLocale,
|
|
45
|
+
prefix,
|
|
46
|
+
localeDetection,
|
|
47
|
+
pathnames,
|
|
48
|
+
domains,
|
|
49
|
+
cookieCfg,
|
|
50
|
+
excludeDetection,
|
|
51
|
+
localeSet,
|
|
52
|
+
detectionMatcher,
|
|
53
|
+
options,
|
|
54
|
+
});
|
|
55
|
+
return wrap ? await wrap(res, req) : res;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function handleProxyRequest(req, ctx) {
|
|
59
|
+
const { locales, defaultLocale, prefix, localeDetection, pathnames, domains, cookieCfg, excludeDetection, localeSet, detectionMatcher, options, } = ctx;
|
|
60
|
+
const { pathname } = req.nextUrl;
|
|
61
|
+
const https = isHttpsRequest(req);
|
|
62
|
+
if (isUnsafePath(pathname)) {
|
|
63
|
+
return passthrough(cookieCfg, null, https);
|
|
64
|
+
}
|
|
65
|
+
const domain = matchDomain(req, domains);
|
|
66
|
+
const activeLocales = domain?.locales ? [...domain.locales] : locales;
|
|
67
|
+
const activeDefault = domain?.defaultLocale ?? defaultLocale;
|
|
68
|
+
const activeSet = new Set(activeLocales);
|
|
69
|
+
const activeMatcher = activeLocales.length === locales.length
|
|
70
|
+
? detectionMatcher
|
|
71
|
+
: createLocaleMatcher(activeLocales.filter((l) => !excludeDetection.has(l)));
|
|
72
|
+
const matched = matchLocalePrefix(pathname, localeSet);
|
|
73
|
+
if (matched && (!domain || activeSet.has(matched.locale))) {
|
|
74
|
+
const { locale, rest } = matched;
|
|
75
|
+
const publicRest = rest === '' ? '/' : rest;
|
|
76
|
+
const internal = toInternalPathname(publicRest, locale, pathnames);
|
|
77
|
+
if (prefix === 'as-needed' && locale === activeDefault) {
|
|
78
|
+
// Canonicalize default-locale URLs without the prefix.
|
|
79
|
+
const dest = publicPathForLocale(internal, locale, pathnames);
|
|
80
|
+
const redirectUrl = req.nextUrl.clone();
|
|
81
|
+
redirectUrl.pathname = dest;
|
|
66
82
|
options.onLocale?.(locale, req);
|
|
67
|
-
|
|
68
|
-
const url = req.nextUrl.clone();
|
|
69
|
-
url.pathname = joinLocalePath(locale, internal);
|
|
70
|
-
return rewriteResponse(url.toString(), cookieCfg, locale, https);
|
|
71
|
-
}
|
|
72
|
-
return passthrough(cookieCfg, locale, https);
|
|
83
|
+
return redirectResponse(redirectUrl.toString(), 308, cookieCfg, locale, https);
|
|
73
84
|
}
|
|
74
|
-
const resolved = options.resolveLocale
|
|
75
|
-
? await options.resolveLocale(req)
|
|
76
|
-
: undefined;
|
|
77
|
-
let locale = sanitizeLocale(resolved, activeLocales) ??
|
|
78
|
-
(localeDetection
|
|
79
|
-
? detectLocale(req, activeLocales, activeDefault, cookieCfg, excludeDetection, activeMatcher, domain)
|
|
80
|
-
: activeDefault);
|
|
81
|
-
locale = sanitizeLocale(locale, activeLocales) ?? activeDefault;
|
|
82
85
|
options.onLocale?.(locale, req);
|
|
83
|
-
|
|
84
|
-
// Prefer localized public path when the user hit the internal key.
|
|
85
|
-
const publicPath = publicPathForLocale(internal, locale, pathnames);
|
|
86
|
-
if (prefix === 'never') {
|
|
86
|
+
if (internal !== publicRest) {
|
|
87
87
|
const url = req.nextUrl.clone();
|
|
88
88
|
url.pathname = joinLocalePath(locale, internal);
|
|
89
89
|
return rewriteResponse(url.toString(), cookieCfg, locale, https);
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
return passthrough(cookieCfg, locale, https);
|
|
92
|
+
}
|
|
93
|
+
const resolved = options.resolveLocale
|
|
94
|
+
? await options.resolveLocale(req)
|
|
95
|
+
: undefined;
|
|
96
|
+
let locale = sanitizeLocale(resolved, activeLocales) ??
|
|
97
|
+
(localeDetection
|
|
98
|
+
? detectLocale(req, activeLocales, activeDefault, cookieCfg, excludeDetection, activeMatcher, domain)
|
|
99
|
+
: activeDefault);
|
|
100
|
+
locale = sanitizeLocale(locale, activeLocales) ?? activeDefault;
|
|
101
|
+
options.onLocale?.(locale, req);
|
|
102
|
+
const internal = toInternalPathname(pathname, locale, pathnames);
|
|
103
|
+
// Prefer localized public path when the user hit the internal key.
|
|
104
|
+
const publicPath = publicPathForLocale(internal, locale, pathnames);
|
|
105
|
+
if (prefix === 'never') {
|
|
106
|
+
const url = req.nextUrl.clone();
|
|
107
|
+
url.pathname = joinLocalePath(locale, internal);
|
|
108
|
+
return rewriteResponse(url.toString(), cookieCfg, locale, https);
|
|
109
|
+
}
|
|
110
|
+
if (prefix === 'as-needed' && locale === activeDefault) {
|
|
111
|
+
// Serve default locale without prefix; rewrite to internal app path.
|
|
112
|
+
if (pathname !== publicPath && pathnames) {
|
|
113
|
+
// User visited internal `/about` but public is also `/about` — fine.
|
|
114
|
+
// If they somehow differ for default locale, redirect to public.
|
|
115
|
+
if (publicPath !== internal && pathname === internal) {
|
|
116
|
+
const redirectUrl = req.nextUrl.clone();
|
|
117
|
+
redirectUrl.pathname = publicPath;
|
|
118
|
+
return redirectResponse(redirectUrl.toString(), 308, cookieCfg, locale, https);
|
|
101
119
|
}
|
|
102
|
-
const url = req.nextUrl.clone();
|
|
103
|
-
url.pathname = joinLocalePath(activeDefault, internal);
|
|
104
|
-
return rewriteResponse(url.toString(), cookieCfg, locale, https);
|
|
105
120
|
}
|
|
106
|
-
// Non-default (or always): redirect to prefixed localized public URL.
|
|
107
|
-
const destPublic = publicPathForLocale(internal, locale, pathnames);
|
|
108
121
|
const url = req.nextUrl.clone();
|
|
109
|
-
url.pathname = joinLocalePath(
|
|
110
|
-
return
|
|
111
|
-
}
|
|
122
|
+
url.pathname = joinLocalePath(activeDefault, internal);
|
|
123
|
+
return rewriteResponse(url.toString(), cookieCfg, locale, https);
|
|
124
|
+
}
|
|
125
|
+
// Non-default (or always): redirect to prefixed localized public URL.
|
|
126
|
+
const destPublic = publicPathForLocale(internal, locale, pathnames);
|
|
127
|
+
const url = req.nextUrl.clone();
|
|
128
|
+
url.pathname = joinLocalePath(locale, destPublic);
|
|
129
|
+
return redirectResponse(url.toString(), 307, cookieCfg, locale, https);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Copy response headers from a prior `NextResponse.next()` (CSP, nonce, …)
|
|
133
|
+
* onto a tongue rewrite/redirect/next response. Skips middleware control
|
|
134
|
+
* headers (`x-middleware-*`, `Location` when tongue already set one).
|
|
135
|
+
*
|
|
136
|
+
* Use when you stamp headers on `next()` then call tongue — tongue alone
|
|
137
|
+
* cannot see those headers.
|
|
138
|
+
*/
|
|
139
|
+
export function mergeProxyResponse(prior, tongue) {
|
|
140
|
+
const headers = new Headers(tongue.headers);
|
|
141
|
+
prior.headers.forEach((value, key) => {
|
|
142
|
+
const k = key.toLowerCase();
|
|
143
|
+
if (k.startsWith('x-middleware-'))
|
|
144
|
+
return;
|
|
145
|
+
if (k === 'location' && headers.has('location'))
|
|
146
|
+
return;
|
|
147
|
+
if (k === 'set-cookie') {
|
|
148
|
+
headers.append(key, value);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!headers.has(key))
|
|
152
|
+
headers.set(key, value);
|
|
153
|
+
});
|
|
154
|
+
return new Response(tongue.body, {
|
|
155
|
+
status: tongue.status,
|
|
156
|
+
statusText: tongue.statusText,
|
|
157
|
+
headers,
|
|
158
|
+
});
|
|
112
159
|
}
|
|
113
160
|
/** Map internal path → localized public path for a locale. */
|
|
114
161
|
export function publicPathForLocale(internal, locale, pathnames) {
|