@dualkit/convert 0.1.0-beta.0 → 0.2.0-beta.0
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/README.md +2 -1
- package/dist/index.cjs +133 -8
- package/dist/index.js +133 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,11 +94,12 @@ Convert flags these in the report instead of guessing:
|
|
|
94
94
|
It refuses, with a one-line reason, when:
|
|
95
95
|
|
|
96
96
|
- The app is not Next.js App Router or Vite React. The Pages Router, Create React App, Remix, Vue, Svelte and Angular are refused.
|
|
97
|
-
- The app already has
|
|
97
|
+
- The app already has files with the names convert would write, such as `app-middleware`.
|
|
98
98
|
- The root layout is a client component, has no `<html>` element, or takes its props in a form convert cannot add params to.
|
|
99
99
|
|
|
100
100
|
## Known gaps
|
|
101
101
|
|
|
102
|
+
- If the app has its own middleware or proxy file, convert moves it to `app-middleware` and runs it first, on the path without its locale. We tested this with a hand-written login check on Next.js 15. Clerk and other auth libraries are untested, so check your login after converting.
|
|
102
103
|
- Convert does not place the language switch, because that would change the English look. It writes `components/dualkit-language-switch.tsx` and tells you to add it.
|
|
103
104
|
- Next.js sends a visitor whose browser asks for Arabic from an English URL to the Arabic page. That is next-intl's locale detection, and convert leaves it on.
|
|
104
105
|
|
package/dist/index.cjs
CHANGED
|
@@ -478,6 +478,15 @@ async function applyPlan(disk, plan) {
|
|
|
478
478
|
return { touched, edits: edits2, writes, perFile };
|
|
479
479
|
}
|
|
480
480
|
var json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
481
|
+
var isMessages = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
482
|
+
function keepHandAdded(existing, generated) {
|
|
483
|
+
const kept = Object.entries(existing).map(([key, value]) => {
|
|
484
|
+
const fresh = generated[key];
|
|
485
|
+
if (fresh === void 0) return [key, value];
|
|
486
|
+
return [key, isMessages(value) && isMessages(fresh) ? keepHandAdded(value, fresh) : fresh];
|
|
487
|
+
});
|
|
488
|
+
return { ...generated, ...Object.fromEntries(kept) };
|
|
489
|
+
}
|
|
481
490
|
var messagesFiles = (ledger) => ({
|
|
482
491
|
[messagesPath("en")]: json(messagesFor(ledger, "en")),
|
|
483
492
|
[messagesPath("ar")]: json(messagesFor(ledger, "ar"))
|
|
@@ -1686,6 +1695,88 @@ export const config = {
|
|
|
1686
1695
|
matcher: '/((?!(?:api|_next|_vercel)(?:/|$)|.*\\\\..*).*)',
|
|
1687
1696
|
}
|
|
1688
1697
|
`;
|
|
1698
|
+
var composedMiddlewareFile = (routing, matcher, own) => `import { NextRequest, NextResponse, type NextFetchEvent } from 'next/server'
|
|
1699
|
+
import createMiddleware from 'next-intl/middleware'
|
|
1700
|
+
import { routing } from '${routing}'
|
|
1701
|
+
import { matchesApp, type Matcher } from '${matcher}'
|
|
1702
|
+
import * as own from '${own}'
|
|
1703
|
+
|
|
1704
|
+
const intl = createMiddleware(routing)
|
|
1705
|
+
type OwnHandler = (request: NextRequest, event: NextFetchEvent) => Response | null | undefined | Promise<Response | null | undefined>
|
|
1706
|
+
// A spread, so the bundler does not warn about the export names this app does not use.
|
|
1707
|
+
const exported: Record<string, unknown> = { ...own }
|
|
1708
|
+
const candidate = exported.default ?? exported.middleware ?? exported.proxy
|
|
1709
|
+
if (typeof candidate !== 'function') throw new Error('Dualkit could not find the middleware function exported by ${own}.')
|
|
1710
|
+
const ownHandler = candidate as OwnHandler
|
|
1711
|
+
const ownMatcher = (exported as { config?: { matcher?: Matcher } }).config?.matcher
|
|
1712
|
+
const OWN_ONLY = /^\\/(?:api|trpc)(?:\\/|$)/
|
|
1713
|
+
|
|
1714
|
+
export default async function middleware(request: NextRequest, event: NextFetchEvent) {
|
|
1715
|
+
const url = request.nextUrl.clone()
|
|
1716
|
+
const [, first = '', ...rest] = url.pathname.split('/')
|
|
1717
|
+
if ((routing.locales as readonly string[]).includes(first)) url.pathname = \`/\${rest.join('/')}\`
|
|
1718
|
+
const result = matchesApp(url.pathname, ownMatcher) ? await ownHandler(new NextRequest(url, request), event) : null
|
|
1719
|
+
// Anything but a plain "continue" is the app's answer: a redirect, a rewrite, a 401 or a page. Keep it as it is.
|
|
1720
|
+
if (result && result.headers.get('x-middleware-next') !== '1') return result
|
|
1721
|
+
if (OWN_ONLY.test(url.pathname)) return result ?? NextResponse.next()
|
|
1722
|
+
const response = intl(request)
|
|
1723
|
+
if (result) {
|
|
1724
|
+
for (const cookie of result.headers.getSetCookie()) response.headers.append('set-cookie', cookie)
|
|
1725
|
+
// Headers the app set for the browser, such as a content security policy.
|
|
1726
|
+
result.headers.forEach((value, key) => { if (key !== 'set-cookie' && !key.startsWith('x-middleware-')) response.headers.set(key, value) })
|
|
1727
|
+
const overridden = result.headers.get('x-middleware-override-headers')
|
|
1728
|
+
if (overridden) {
|
|
1729
|
+
const names = new Set([...(response.headers.get('x-middleware-override-headers') ?? '').split(','), ...overridden.split(',')].filter(Boolean))
|
|
1730
|
+
response.headers.set('x-middleware-override-headers', [...names].join(','))
|
|
1731
|
+
result.headers.forEach((value, key) => { if (key.startsWith('x-middleware-request-')) response.headers.set(key, value) })
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return response
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
export const config = {
|
|
1738
|
+
matcher: '/((?!(?:_next|_vercel)(?:/|$)|.*\\\\..*).*)',
|
|
1739
|
+
}
|
|
1740
|
+
`;
|
|
1741
|
+
var APP_MATCHER = String.raw`/** Whether a path is covered by an app's own middleware matcher. No matcher means every path. */
|
|
1742
|
+
export type Matcher = string | { source: string } | (string | { source: string })[]
|
|
1743
|
+
|
|
1744
|
+
/** An entry with has or missing conditions is treated as matching, so the app's middleware runs rather than being skipped. */
|
|
1745
|
+
export function matchesApp(pathname: string, matcher: Matcher | undefined): boolean {
|
|
1746
|
+
if (matcher === undefined) return true
|
|
1747
|
+
return (Array.isArray(matcher) ? matcher : [matcher]).some((entry) => toRegExp(typeof entry === 'string' ? entry : entry.source).test(pathname))
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function toRegExp(pattern: string): RegExp {
|
|
1751
|
+
let out = ''
|
|
1752
|
+
let i = 0
|
|
1753
|
+
while (i < pattern.length) {
|
|
1754
|
+
const char = pattern[i]!
|
|
1755
|
+
const param = char === '/' ? /^\/:[A-Za-z0-9_]+([*+?]?)/.exec(pattern.slice(i)) : null
|
|
1756
|
+
if (param && pattern[i + param[0].length] === '(') {
|
|
1757
|
+
out += '/'
|
|
1758
|
+
i += param[0].length
|
|
1759
|
+
} else if (param) {
|
|
1760
|
+
out += { '*': '(?:/.*)?', '+': '/.+', '?': '(?:/[^/]+)?', '': '/[^/]+' }[param[1]!]
|
|
1761
|
+
i += param[0].length
|
|
1762
|
+
} else if (char === '(') {
|
|
1763
|
+
let depth = 0
|
|
1764
|
+
let j = i
|
|
1765
|
+
for (; j < pattern.length; j++) {
|
|
1766
|
+
if (pattern[j] === '\\') j++
|
|
1767
|
+
else if (pattern[j] === '(') depth++
|
|
1768
|
+
else if (pattern[j] === ')' && --depth === 0) break
|
|
1769
|
+
}
|
|
1770
|
+
out += pattern.slice(i, j + 1)
|
|
1771
|
+
i = j + 1
|
|
1772
|
+
} else {
|
|
1773
|
+
out += char.replace(/[.*+?^$|{}[\]\\]/g, '\\$&')
|
|
1774
|
+
i++
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return new RegExp('^' + out + (out.endsWith('/') ? '' : '/?') + '$')
|
|
1778
|
+
}
|
|
1779
|
+
`;
|
|
1689
1780
|
var switchFile = (navigation) => `'use client'
|
|
1690
1781
|
|
|
1691
1782
|
import { useLocale } from 'next-intl'
|
|
@@ -1738,16 +1829,24 @@ function alreadyWired(project, mode, appDir, files, paths) {
|
|
|
1738
1829
|
const wired = (files(project.entry) ?? "").includes("NextIntlClientProvider") && files(`${sourceBase(project)}i18n/request.ts`) !== null;
|
|
1739
1830
|
return wired ? project.entry : null;
|
|
1740
1831
|
}
|
|
1741
|
-
function
|
|
1832
|
+
function findMiddleware(base, files) {
|
|
1833
|
+
const found = [];
|
|
1742
1834
|
for (const name of ["middleware", "proxy"]) {
|
|
1743
1835
|
for (const ext of ["ts", "js", "mjs", "tsx", "jsx"]) {
|
|
1744
1836
|
const path = `${base}${name}.${ext}`;
|
|
1745
|
-
const
|
|
1746
|
-
if (
|
|
1747
|
-
throw new UnsupportedError(`This app already has ${path}. Convert cannot merge the next-intl middleware into it, so it stopped before changing anything.`);
|
|
1837
|
+
const text = files(path);
|
|
1838
|
+
if (text !== null) found.push({ path, text });
|
|
1748
1839
|
}
|
|
1749
1840
|
}
|
|
1841
|
+
if (found.length > 1) throw new UnsupportedError(`This app has both ${found[0].path} and ${found[1].path}. Convert cannot tell which one to compose with next-intl, so it stopped before changing anything.`);
|
|
1842
|
+
const only = found[0] ?? null;
|
|
1843
|
+
if (only && !EXPORTS_HANDLER.test(only.text)) {
|
|
1844
|
+
throw new UnsupportedError(`Convert could not find the middleware function exported from ${only.path}, so it stopped before changing anything.`);
|
|
1845
|
+
}
|
|
1846
|
+
return only;
|
|
1750
1847
|
}
|
|
1848
|
+
var EXPORTS_HANDLER = /export\s+default\b|export\s+(?:async\s+)?function\s+(?:middleware|proxy)\b|export\s+(?:const|let|var)\s+(?:middleware|proxy)\b|export\s*\{[^}]*\b(?:middleware|proxy|default)\b[^}]*\}/;
|
|
1849
|
+
var movedMiddlewarePath = (base, existing) => `${base}app-middleware.${existing.split(".").pop()}`;
|
|
1751
1850
|
var merge = (into, from) => {
|
|
1752
1851
|
const out = { ...into };
|
|
1753
1852
|
for (const [path, edits2] of Object.entries(from)) out[path] = [...out[path] ?? [], ...edits2];
|
|
@@ -1757,8 +1856,13 @@ function bilingualParts(project, appDir, files, paths) {
|
|
|
1757
1856
|
const base = sourceBase(project);
|
|
1758
1857
|
const { moves, keptPages } = routeMoves(appDir, paths);
|
|
1759
1858
|
const middlewarePath = `${base}${nextMajor(files) >= 16 ? "proxy" : "middleware"}.ts`;
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1859
|
+
const own = findMiddleware(base, files);
|
|
1860
|
+
const routingSpec = specifierTo(middlewarePath, `${base}i18n/routing.ts`);
|
|
1861
|
+
const ownMoved = own ? movedMiddlewarePath(base, own.path) : null;
|
|
1862
|
+
if (ownMoved && files(ownMoved) !== null) {
|
|
1863
|
+
throw new UnsupportedError(`This app already has ${ownMoved}. Convert needs that name for the app's own middleware, so move or remove that file and run convert again.`);
|
|
1864
|
+
}
|
|
1865
|
+
const middleware = ownMoved ? composedMiddlewareFile(routingSpec, specifierTo(middlewarePath, `${base}i18n/app-matcher.ts`), specifierTo(middlewarePath, ownMoved)) : middlewareFile(routingSpec);
|
|
1762
1866
|
const catchAll = topCatchAll(appDir, moves, paths);
|
|
1763
1867
|
const flags = keptPages.map((page) => flagAt(
|
|
1764
1868
|
page,
|
|
@@ -1766,6 +1870,14 @@ function bilingualParts(project, appDir, files, paths) {
|
|
|
1766
1870
|
`${page} stays outside [locale], because the middleware skips its path, so it has no Arabic version.`,
|
|
1767
1871
|
"Move the page to a path the middleware matches, or translate it by hand."
|
|
1768
1872
|
));
|
|
1873
|
+
if (own && ownMoved) {
|
|
1874
|
+
flags.push(flagAt(
|
|
1875
|
+
ownMoved,
|
|
1876
|
+
"info",
|
|
1877
|
+
`${own.path} moved to ${ownMoved}, and the new ${middlewarePath} runs it first on the path without its locale prefix, then next-intl.`,
|
|
1878
|
+
"Check the app middleware still behaves as before and that its auth checks still hold under /ar. Any answer other than a plain continue (a redirect, rewrite or 401) is returned as it is. Paths under /api and /trpc skip next-intl. Config options other than matcher, such as runtime, are not carried over, and has or missing conditions on a matcher entry are ignored, so the middleware runs on those paths."
|
|
1879
|
+
));
|
|
1880
|
+
}
|
|
1769
1881
|
if (catchAll) {
|
|
1770
1882
|
flags.push(flagAt(
|
|
1771
1883
|
catchAll,
|
|
@@ -1778,11 +1890,13 @@ function bilingualParts(project, appDir, files, paths) {
|
|
|
1778
1890
|
[`${base}i18n/routing.ts`]: ROUTING,
|
|
1779
1891
|
[`${base}i18n/navigation.ts`]: NAVIGATION,
|
|
1780
1892
|
[middlewarePath]: middleware,
|
|
1893
|
+
...own ? { [`${base}i18n/app-matcher.ts`]: APP_MATCHER } : {},
|
|
1781
1894
|
[switchPath(project)]: switchFile(specifierTo(switchPath(project), `${base}i18n/navigation.ts`)),
|
|
1782
1895
|
...catchAll ? {} : { [`${appDir}/[locale]/[...rest]/page.tsx`]: CATCH_ALL }
|
|
1783
1896
|
};
|
|
1784
1897
|
const rewrites = importRewrites(appDir, moves, paths, files, readAliases(files));
|
|
1785
|
-
|
|
1898
|
+
const ownMove = own && ownMoved ? [[own.path, ownMoved]] : [];
|
|
1899
|
+
return { moves: [...moves, ...ownMove], writes, flags: [...flags, ...rewrites.flags], edits: rewrites.edits };
|
|
1786
1900
|
}
|
|
1787
1901
|
function wireNextIntl(project, mode, files, paths) {
|
|
1788
1902
|
const appDir = project.appDir ?? "app";
|
|
@@ -3112,7 +3226,18 @@ function pendingCheck(ledger, force) {
|
|
|
3112
3226
|
}
|
|
3113
3227
|
function writeMessages(root, ledger) {
|
|
3114
3228
|
const disk = { root, created: [] };
|
|
3115
|
-
for (const [path, text] of Object.entries(messagesFiles(ledger))) writeText(disk, path, text);
|
|
3229
|
+
for (const [path, text] of Object.entries(messagesFiles(ledger))) writeText(disk, path, mergedMessages(root, path, text));
|
|
3230
|
+
}
|
|
3231
|
+
function mergedMessages(root, path, generated) {
|
|
3232
|
+
const full = (0, import_node_path19.join)(root, path);
|
|
3233
|
+
if (!(0, import_node_fs11.existsSync)(full)) return generated;
|
|
3234
|
+
try {
|
|
3235
|
+
const existing = JSON.parse((0, import_node_fs11.readFileSync)(full, "utf8"));
|
|
3236
|
+
if (typeof existing !== "object" || existing === null) return generated;
|
|
3237
|
+
return JSON.stringify(keepHandAdded(existing, JSON.parse(generated)), null, 2) + "\n";
|
|
3238
|
+
} catch {
|
|
3239
|
+
return generated;
|
|
3240
|
+
}
|
|
3116
3241
|
}
|
|
3117
3242
|
async function englishChecks(root, base, routes, shoot) {
|
|
3118
3243
|
const english2 = [];
|
package/dist/index.js
CHANGED
|
@@ -443,6 +443,15 @@ async function applyPlan(disk, plan) {
|
|
|
443
443
|
return { touched, edits: edits2, writes, perFile };
|
|
444
444
|
}
|
|
445
445
|
var json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
446
|
+
var isMessages = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
447
|
+
function keepHandAdded(existing, generated) {
|
|
448
|
+
const kept = Object.entries(existing).map(([key, value]) => {
|
|
449
|
+
const fresh = generated[key];
|
|
450
|
+
if (fresh === void 0) return [key, value];
|
|
451
|
+
return [key, isMessages(value) && isMessages(fresh) ? keepHandAdded(value, fresh) : fresh];
|
|
452
|
+
});
|
|
453
|
+
return { ...generated, ...Object.fromEntries(kept) };
|
|
454
|
+
}
|
|
446
455
|
var messagesFiles = (ledger) => ({
|
|
447
456
|
[messagesPath("en")]: json(messagesFor(ledger, "en")),
|
|
448
457
|
[messagesPath("ar")]: json(messagesFor(ledger, "ar"))
|
|
@@ -1651,6 +1660,88 @@ export const config = {
|
|
|
1651
1660
|
matcher: '/((?!(?:api|_next|_vercel)(?:/|$)|.*\\\\..*).*)',
|
|
1652
1661
|
}
|
|
1653
1662
|
`;
|
|
1663
|
+
var composedMiddlewareFile = (routing, matcher, own) => `import { NextRequest, NextResponse, type NextFetchEvent } from 'next/server'
|
|
1664
|
+
import createMiddleware from 'next-intl/middleware'
|
|
1665
|
+
import { routing } from '${routing}'
|
|
1666
|
+
import { matchesApp, type Matcher } from '${matcher}'
|
|
1667
|
+
import * as own from '${own}'
|
|
1668
|
+
|
|
1669
|
+
const intl = createMiddleware(routing)
|
|
1670
|
+
type OwnHandler = (request: NextRequest, event: NextFetchEvent) => Response | null | undefined | Promise<Response | null | undefined>
|
|
1671
|
+
// A spread, so the bundler does not warn about the export names this app does not use.
|
|
1672
|
+
const exported: Record<string, unknown> = { ...own }
|
|
1673
|
+
const candidate = exported.default ?? exported.middleware ?? exported.proxy
|
|
1674
|
+
if (typeof candidate !== 'function') throw new Error('Dualkit could not find the middleware function exported by ${own}.')
|
|
1675
|
+
const ownHandler = candidate as OwnHandler
|
|
1676
|
+
const ownMatcher = (exported as { config?: { matcher?: Matcher } }).config?.matcher
|
|
1677
|
+
const OWN_ONLY = /^\\/(?:api|trpc)(?:\\/|$)/
|
|
1678
|
+
|
|
1679
|
+
export default async function middleware(request: NextRequest, event: NextFetchEvent) {
|
|
1680
|
+
const url = request.nextUrl.clone()
|
|
1681
|
+
const [, first = '', ...rest] = url.pathname.split('/')
|
|
1682
|
+
if ((routing.locales as readonly string[]).includes(first)) url.pathname = \`/\${rest.join('/')}\`
|
|
1683
|
+
const result = matchesApp(url.pathname, ownMatcher) ? await ownHandler(new NextRequest(url, request), event) : null
|
|
1684
|
+
// Anything but a plain "continue" is the app's answer: a redirect, a rewrite, a 401 or a page. Keep it as it is.
|
|
1685
|
+
if (result && result.headers.get('x-middleware-next') !== '1') return result
|
|
1686
|
+
if (OWN_ONLY.test(url.pathname)) return result ?? NextResponse.next()
|
|
1687
|
+
const response = intl(request)
|
|
1688
|
+
if (result) {
|
|
1689
|
+
for (const cookie of result.headers.getSetCookie()) response.headers.append('set-cookie', cookie)
|
|
1690
|
+
// Headers the app set for the browser, such as a content security policy.
|
|
1691
|
+
result.headers.forEach((value, key) => { if (key !== 'set-cookie' && !key.startsWith('x-middleware-')) response.headers.set(key, value) })
|
|
1692
|
+
const overridden = result.headers.get('x-middleware-override-headers')
|
|
1693
|
+
if (overridden) {
|
|
1694
|
+
const names = new Set([...(response.headers.get('x-middleware-override-headers') ?? '').split(','), ...overridden.split(',')].filter(Boolean))
|
|
1695
|
+
response.headers.set('x-middleware-override-headers', [...names].join(','))
|
|
1696
|
+
result.headers.forEach((value, key) => { if (key.startsWith('x-middleware-request-')) response.headers.set(key, value) })
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return response
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
export const config = {
|
|
1703
|
+
matcher: '/((?!(?:_next|_vercel)(?:/|$)|.*\\\\..*).*)',
|
|
1704
|
+
}
|
|
1705
|
+
`;
|
|
1706
|
+
var APP_MATCHER = String.raw`/** Whether a path is covered by an app's own middleware matcher. No matcher means every path. */
|
|
1707
|
+
export type Matcher = string | { source: string } | (string | { source: string })[]
|
|
1708
|
+
|
|
1709
|
+
/** An entry with has or missing conditions is treated as matching, so the app's middleware runs rather than being skipped. */
|
|
1710
|
+
export function matchesApp(pathname: string, matcher: Matcher | undefined): boolean {
|
|
1711
|
+
if (matcher === undefined) return true
|
|
1712
|
+
return (Array.isArray(matcher) ? matcher : [matcher]).some((entry) => toRegExp(typeof entry === 'string' ? entry : entry.source).test(pathname))
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function toRegExp(pattern: string): RegExp {
|
|
1716
|
+
let out = ''
|
|
1717
|
+
let i = 0
|
|
1718
|
+
while (i < pattern.length) {
|
|
1719
|
+
const char = pattern[i]!
|
|
1720
|
+
const param = char === '/' ? /^\/:[A-Za-z0-9_]+([*+?]?)/.exec(pattern.slice(i)) : null
|
|
1721
|
+
if (param && pattern[i + param[0].length] === '(') {
|
|
1722
|
+
out += '/'
|
|
1723
|
+
i += param[0].length
|
|
1724
|
+
} else if (param) {
|
|
1725
|
+
out += { '*': '(?:/.*)?', '+': '/.+', '?': '(?:/[^/]+)?', '': '/[^/]+' }[param[1]!]
|
|
1726
|
+
i += param[0].length
|
|
1727
|
+
} else if (char === '(') {
|
|
1728
|
+
let depth = 0
|
|
1729
|
+
let j = i
|
|
1730
|
+
for (; j < pattern.length; j++) {
|
|
1731
|
+
if (pattern[j] === '\\') j++
|
|
1732
|
+
else if (pattern[j] === '(') depth++
|
|
1733
|
+
else if (pattern[j] === ')' && --depth === 0) break
|
|
1734
|
+
}
|
|
1735
|
+
out += pattern.slice(i, j + 1)
|
|
1736
|
+
i = j + 1
|
|
1737
|
+
} else {
|
|
1738
|
+
out += char.replace(/[.*+?^$|{}[\]\\]/g, '\\$&')
|
|
1739
|
+
i++
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return new RegExp('^' + out + (out.endsWith('/') ? '' : '/?') + '$')
|
|
1743
|
+
}
|
|
1744
|
+
`;
|
|
1654
1745
|
var switchFile = (navigation) => `'use client'
|
|
1655
1746
|
|
|
1656
1747
|
import { useLocale } from 'next-intl'
|
|
@@ -1703,16 +1794,24 @@ function alreadyWired(project, mode, appDir, files, paths) {
|
|
|
1703
1794
|
const wired = (files(project.entry) ?? "").includes("NextIntlClientProvider") && files(`${sourceBase(project)}i18n/request.ts`) !== null;
|
|
1704
1795
|
return wired ? project.entry : null;
|
|
1705
1796
|
}
|
|
1706
|
-
function
|
|
1797
|
+
function findMiddleware(base, files) {
|
|
1798
|
+
const found = [];
|
|
1707
1799
|
for (const name of ["middleware", "proxy"]) {
|
|
1708
1800
|
for (const ext of ["ts", "js", "mjs", "tsx", "jsx"]) {
|
|
1709
1801
|
const path = `${base}${name}.${ext}`;
|
|
1710
|
-
const
|
|
1711
|
-
if (
|
|
1712
|
-
throw new UnsupportedError(`This app already has ${path}. Convert cannot merge the next-intl middleware into it, so it stopped before changing anything.`);
|
|
1802
|
+
const text = files(path);
|
|
1803
|
+
if (text !== null) found.push({ path, text });
|
|
1713
1804
|
}
|
|
1714
1805
|
}
|
|
1806
|
+
if (found.length > 1) throw new UnsupportedError(`This app has both ${found[0].path} and ${found[1].path}. Convert cannot tell which one to compose with next-intl, so it stopped before changing anything.`);
|
|
1807
|
+
const only = found[0] ?? null;
|
|
1808
|
+
if (only && !EXPORTS_HANDLER.test(only.text)) {
|
|
1809
|
+
throw new UnsupportedError(`Convert could not find the middleware function exported from ${only.path}, so it stopped before changing anything.`);
|
|
1810
|
+
}
|
|
1811
|
+
return only;
|
|
1715
1812
|
}
|
|
1813
|
+
var EXPORTS_HANDLER = /export\s+default\b|export\s+(?:async\s+)?function\s+(?:middleware|proxy)\b|export\s+(?:const|let|var)\s+(?:middleware|proxy)\b|export\s*\{[^}]*\b(?:middleware|proxy|default)\b[^}]*\}/;
|
|
1814
|
+
var movedMiddlewarePath = (base, existing) => `${base}app-middleware.${existing.split(".").pop()}`;
|
|
1716
1815
|
var merge = (into, from) => {
|
|
1717
1816
|
const out = { ...into };
|
|
1718
1817
|
for (const [path, edits2] of Object.entries(from)) out[path] = [...out[path] ?? [], ...edits2];
|
|
@@ -1722,8 +1821,13 @@ function bilingualParts(project, appDir, files, paths) {
|
|
|
1722
1821
|
const base = sourceBase(project);
|
|
1723
1822
|
const { moves, keptPages } = routeMoves(appDir, paths);
|
|
1724
1823
|
const middlewarePath = `${base}${nextMajor(files) >= 16 ? "proxy" : "middleware"}.ts`;
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1824
|
+
const own = findMiddleware(base, files);
|
|
1825
|
+
const routingSpec = specifierTo(middlewarePath, `${base}i18n/routing.ts`);
|
|
1826
|
+
const ownMoved = own ? movedMiddlewarePath(base, own.path) : null;
|
|
1827
|
+
if (ownMoved && files(ownMoved) !== null) {
|
|
1828
|
+
throw new UnsupportedError(`This app already has ${ownMoved}. Convert needs that name for the app's own middleware, so move or remove that file and run convert again.`);
|
|
1829
|
+
}
|
|
1830
|
+
const middleware = ownMoved ? composedMiddlewareFile(routingSpec, specifierTo(middlewarePath, `${base}i18n/app-matcher.ts`), specifierTo(middlewarePath, ownMoved)) : middlewareFile(routingSpec);
|
|
1727
1831
|
const catchAll = topCatchAll(appDir, moves, paths);
|
|
1728
1832
|
const flags = keptPages.map((page) => flagAt(
|
|
1729
1833
|
page,
|
|
@@ -1731,6 +1835,14 @@ function bilingualParts(project, appDir, files, paths) {
|
|
|
1731
1835
|
`${page} stays outside [locale], because the middleware skips its path, so it has no Arabic version.`,
|
|
1732
1836
|
"Move the page to a path the middleware matches, or translate it by hand."
|
|
1733
1837
|
));
|
|
1838
|
+
if (own && ownMoved) {
|
|
1839
|
+
flags.push(flagAt(
|
|
1840
|
+
ownMoved,
|
|
1841
|
+
"info",
|
|
1842
|
+
`${own.path} moved to ${ownMoved}, and the new ${middlewarePath} runs it first on the path without its locale prefix, then next-intl.`,
|
|
1843
|
+
"Check the app middleware still behaves as before and that its auth checks still hold under /ar. Any answer other than a plain continue (a redirect, rewrite or 401) is returned as it is. Paths under /api and /trpc skip next-intl. Config options other than matcher, such as runtime, are not carried over, and has or missing conditions on a matcher entry are ignored, so the middleware runs on those paths."
|
|
1844
|
+
));
|
|
1845
|
+
}
|
|
1734
1846
|
if (catchAll) {
|
|
1735
1847
|
flags.push(flagAt(
|
|
1736
1848
|
catchAll,
|
|
@@ -1743,11 +1855,13 @@ function bilingualParts(project, appDir, files, paths) {
|
|
|
1743
1855
|
[`${base}i18n/routing.ts`]: ROUTING,
|
|
1744
1856
|
[`${base}i18n/navigation.ts`]: NAVIGATION,
|
|
1745
1857
|
[middlewarePath]: middleware,
|
|
1858
|
+
...own ? { [`${base}i18n/app-matcher.ts`]: APP_MATCHER } : {},
|
|
1746
1859
|
[switchPath(project)]: switchFile(specifierTo(switchPath(project), `${base}i18n/navigation.ts`)),
|
|
1747
1860
|
...catchAll ? {} : { [`${appDir}/[locale]/[...rest]/page.tsx`]: CATCH_ALL }
|
|
1748
1861
|
};
|
|
1749
1862
|
const rewrites = importRewrites(appDir, moves, paths, files, readAliases(files));
|
|
1750
|
-
|
|
1863
|
+
const ownMove = own && ownMoved ? [[own.path, ownMoved]] : [];
|
|
1864
|
+
return { moves: [...moves, ...ownMove], writes, flags: [...flags, ...rewrites.flags], edits: rewrites.edits };
|
|
1751
1865
|
}
|
|
1752
1866
|
function wireNextIntl(project, mode, files, paths) {
|
|
1753
1867
|
const appDir = project.appDir ?? "app";
|
|
@@ -3077,7 +3191,18 @@ function pendingCheck(ledger, force) {
|
|
|
3077
3191
|
}
|
|
3078
3192
|
function writeMessages(root, ledger) {
|
|
3079
3193
|
const disk = { root, created: [] };
|
|
3080
|
-
for (const [path, text] of Object.entries(messagesFiles(ledger))) writeText(disk, path, text);
|
|
3194
|
+
for (const [path, text] of Object.entries(messagesFiles(ledger))) writeText(disk, path, mergedMessages(root, path, text));
|
|
3195
|
+
}
|
|
3196
|
+
function mergedMessages(root, path, generated) {
|
|
3197
|
+
const full = join11(root, path);
|
|
3198
|
+
if (!existsSync8(full)) return generated;
|
|
3199
|
+
try {
|
|
3200
|
+
const existing = JSON.parse(readFileSync7(full, "utf8"));
|
|
3201
|
+
if (typeof existing !== "object" || existing === null) return generated;
|
|
3202
|
+
return JSON.stringify(keepHandAdded(existing, JSON.parse(generated)), null, 2) + "\n";
|
|
3203
|
+
} catch {
|
|
3204
|
+
return generated;
|
|
3205
|
+
}
|
|
3081
3206
|
}
|
|
3082
3207
|
async function englishChecks(root, base, routes, shoot) {
|
|
3083
3208
|
const english2 = [];
|