Skip to content

Commit 024c219

Browse files
feat: Add Vercel hosting support alongside Netlify (#5057)
* feat: Add Vercel hosting support alongside Netlify Add vercel.json with build config, headers, and redirects converted from Netlify format. Update docusaurus.config.ts to detect both Netlify and Vercel environments. Add Vercel-specific build/deploy scripts without modifying existing commands. * fix: Adjust vercel.json paths for Vercel root directory config The Vercel project has rootDirectory set to "website", so build/dev commands run from within the website directory already. Remove the "cd website" prefix and adjust outputDirectory to "build". * feat: Generate vercel.json redirects from Netlify _redirects Add sync-vercel-redirects.ts script that reads website/static/_redirects (Netlify format), converts to Vercel format, and updates vercel.json. This keeps _redirects as the single source of truth. Add CI check that fails if vercel.json redirects drift out of sync. Run 'yarn sync-redirects:vercel' after editing _redirects. * fix: Add permissions block to workflow, use replaceAll for wildcards
1 parent ad88dab commit 024c219

7 files changed

Lines changed: 711 additions & 5 deletions

File tree

.github/workflows/pre-merge.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ on:
55
branches:
66
- main
77

8+
permissions:
9+
contents: read
10+
811
jobs:
912
lint:
1013
runs-on: ubuntu-latest
@@ -33,6 +36,29 @@ jobs:
3336
- name: Run plugins lint
3437
run: yarn lint:plugins
3538

39+
check-vercel-redirects:
40+
runs-on: ubuntu-latest
41+
steps:
42+
- name: Checkout repository
43+
uses: actions/checkout@v6
44+
45+
- name: Set up Node.js
46+
uses: actions/setup-node@v6
47+
with:
48+
node-version: "22"
49+
cache: yarn
50+
51+
- name: Install dependencies
52+
run: yarn install --immutable
53+
54+
- name: Check Vercel redirects are in sync
55+
run: |
56+
yarn sync-redirects:vercel
57+
if ! git diff --exit-code vercel.json; then
58+
echo "::error::vercel.json redirects are out of sync with _redirects. Run 'yarn sync-redirects:vercel' and commit."
59+
exit 1
60+
fi
61+
3662
lint-website:
3763
runs-on: ubuntu-latest
3864
steps:

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ website/build/
3333
!.yarn/releases
3434
!.yarn/sdks
3535
!.yarn/versions
36+
.vercel
37+
.env*.local

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
"lint:plugins": "yarn workspace @react-native-website/remark-codeblock-language-as-title lint && yarn workspace @react-native-website/remark-lint-no-broken-external-links lint && yarn workspace @react-native-website/remark-snackplayer lint && yarn workspace @react-native-website/remark-lint-no-broken-external-links test && yarn workspace @react-native-website/remark-snackplayer test",
2323
"lint:website": "eslint ./website ./docs",
2424
"update-lock": "npx yarn-deduplicate",
25-
"check-dependencies": "manypkg check"
25+
"check-dependencies": "manypkg check",
26+
"sync-redirects:vercel": "node scripts/src/sync-vercel-redirects.ts",
27+
"build:vercel": "yarn --cwd website build:vercel",
28+
"build:vercel:fast": "yarn --cwd website build:vercel:fast",
29+
"dev:vercel": "vercel dev"
2630
},
2731
"devDependencies": {
2832
"@eslint/css": "^1.0.0",
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import fs from 'node:fs';
9+
import path from 'node:path';
10+
11+
const REPO_ROOT = path.resolve(import.meta.dirname, '../..');
12+
const REDIRECTS_PATH = path.join(REPO_ROOT, 'website/static/_redirects');
13+
const VERSIONS_PATH = path.join(REPO_ROOT, 'website/versions.json');
14+
const VERCEL_JSON_PATH = path.join(REPO_ROOT, 'vercel.json');
15+
16+
interface VercelRedirect {
17+
source: string;
18+
destination: string;
19+
permanent: boolean;
20+
}
21+
22+
function isPartialSegmentWildcard(source: string): boolean {
23+
return source.split('/').some(seg => seg.includes('*') && seg !== '*');
24+
}
25+
26+
function expandPartialWildcard(
27+
source: string,
28+
destination: string
29+
): VercelRedirect[] {
30+
const segments = source.split('/');
31+
const wildcardSeg = segments.find(seg => seg.includes('*') && seg !== '*')!;
32+
const prefix = wildcardSeg.replaceAll('*', '');
33+
34+
// Infer docs directory from destination path
35+
// e.g. /docs/next/legacy/native-modules-:splat → docs/legacy/
36+
const destDir = destination
37+
.replace(/^\/docs\/next\//, '')
38+
.replace(/\/[^/]*$/, '');
39+
const searchDir = path.join(REPO_ROOT, 'docs', destDir);
40+
41+
let files: string[];
42+
try {
43+
files = fs
44+
.readdirSync(searchDir)
45+
.filter(f => f.startsWith(prefix) && /\.mdx?$/.test(f))
46+
.map(f => f.replace(/\.mdx?$/, ''));
47+
} catch {
48+
console.warn(
49+
`Warning: Could not read ${searchDir} for expanding ${source}`
50+
);
51+
return [];
52+
}
53+
54+
return files.map(filename => {
55+
const suffix = filename.slice(prefix.length);
56+
return {
57+
source: source.replace(`${prefix}*`, filename),
58+
destination: destination.replace(':splat', suffix),
59+
permanent: true,
60+
};
61+
});
62+
}
63+
64+
function syncRedirects(): void {
65+
const latestVersion: string = JSON.parse(
66+
fs.readFileSync(VERSIONS_PATH, 'utf8')
67+
)[0];
68+
69+
const lines = fs.readFileSync(REDIRECTS_PATH, 'utf8').split('\n');
70+
const redirects: VercelRedirect[] = [];
71+
72+
for (const line of lines) {
73+
const trimmed = line.trim();
74+
if (!trimmed || trimmed.startsWith('#')) continue;
75+
76+
const parts = trimmed.split(/\s+/);
77+
if (parts.length < 2) continue;
78+
79+
let [source, destination] = parts;
80+
81+
// Strip full URL sources to path only
82+
if (source.startsWith('http://') || source.startsWith('https://')) {
83+
try {
84+
source = new URL(source).pathname;
85+
} catch {
86+
continue;
87+
}
88+
}
89+
90+
// Convert same-domain full URL destinations to relative paths
91+
if (destination.startsWith('https://reactnative.dev/')) {
92+
destination = destination.replace('https://reactnative.dev', '');
93+
}
94+
95+
// Replace $LATEST_VERSION$ placeholder with actual version
96+
source = source.replaceAll('$LATEST_VERSION$', latestVersion);
97+
destination = destination.replaceAll('$LATEST_VERSION$', latestVersion);
98+
99+
// Handle partial-segment wildcards (e.g., native-modules-*)
100+
// Vercel doesn't support wildcards within a path segment, so enumerate
101+
if (isPartialSegmentWildcard(source)) {
102+
redirects.push(...expandPartialWildcard(source, destination));
103+
continue;
104+
}
105+
106+
// Convert Netlify wildcard syntax to Vercel format
107+
// Netlify: /* and :splat → Vercel: /:path*
108+
source = source.replace(/\*$/, ':path*');
109+
destination = destination.replace(':splat', ':path*');
110+
111+
redirects.push({source, destination, permanent: true});
112+
}
113+
114+
const vercelJson = JSON.parse(fs.readFileSync(VERCEL_JSON_PATH, 'utf8'));
115+
vercelJson.redirects = redirects;
116+
fs.writeFileSync(
117+
VERCEL_JSON_PATH,
118+
JSON.stringify(vercelJson, null, 2) + '\n'
119+
);
120+
121+
console.log(`Synced ${redirects.length} redirects to vercel.json`);
122+
}
123+
124+
syncRedirects();

0 commit comments

Comments
 (0)