From 1947e1a1062f436e91f2062a9eb7d263e7df64e1 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Wed, 22 Apr 2026 16:16:14 -0700 Subject: [PATCH 1/4] 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. --- .gitignore | 2 + package.json | 5 +- vercel.json | 543 +++++++++++++++++++++++++++++++++++ website/docusaurus.config.ts | 8 +- website/package.json | 7 +- 5 files changed, 560 insertions(+), 5 deletions(-) create mode 100644 vercel.json diff --git a/.gitignore b/.gitignore index 88ee8b01aaa..44301458afd 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ website/build/ !.yarn/releases !.yarn/sdks !.yarn/versions +.vercel +.env*.local diff --git a/package.json b/package.json index 6bb9bc6933e..0736e32b099 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,10 @@ "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", "lint:website": "eslint ./website ./docs", "update-lock": "npx yarn-deduplicate", - "check-dependencies": "manypkg check" + "check-dependencies": "manypkg check", + "build:vercel": "yarn --cwd website build:vercel", + "build:vercel:fast": "yarn --cwd website build:vercel:fast", + "dev:vercel": "vercel dev" }, "devDependencies": { "@eslint/css": "^1.0.0", diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000000..f6585720fe8 --- /dev/null +++ b/vercel.json @@ -0,0 +1,543 @@ +{ + "buildCommand": "cd website && yarn build:vercel", + "devCommand": "cd website && yarn start", + "outputDirectory": "website/build", + "installCommand": "yarn install", + "framework": null, + "git": { + "deepClone": true + }, + "headers": [ + { + "source": "/movies.json", + "headers": [ + { + "key": "Access-Control-Allow-Origin", + "value": "*" + } + ] + } + ], + "redirects": [ + { + "source": "/blog/feed", + "destination": "/blog/rss.xml", + "permanent": true + }, + { + "source": "/blog/feed.xml", + "destination": "/blog/rss.xml", + "permanent": true + }, + { + "source": "/docs/android-setup", + "destination": "/docs/getting-started", + "permanent": true + }, + { + "source": "/docs/building-for-apple-tv", + "destination": "/docs/building-for-tv", + "permanent": true + }, + { + "source": "/docs/building-from-source", + "destination": "/contributing/how-to-build-from-source", + "permanent": true + }, + { + "source": "/docs/contributing", + "destination": "/contributing/overview", + "permanent": true + }, + { + "source": "/docs/sourcemaps", + "destination": "/docs/debugging-release-builds", + "permanent": true + }, + { + "source": "/docs/symbolication", + "destination": "/docs/debugging-release-builds", + "permanent": true + }, + { + "source": "/docs/publishing-forks", + "destination": "/contributing/how-to-build-from-source#publish-your-own-version-of-react-native", + "permanent": true + }, + { + "source": "/docs/react-devtools", + "destination": "/docs/react-native-devtools", + "permanent": true + }, + { + "source": "/docs/testing", + "destination": "/contributing/how-to-run-and-write-tests", + "permanent": true + }, + { + "source": "/docs/understanding-cli", + "destination": "https://github.com/react-native-community/cli#react-native-cli", + "permanent": true + }, + { + "source": "/contributing/how-to-contribute", + "destination": "/contributing/overview", + "permanent": true + }, + { + "source": "/contributing/how-to-file-an-issue", + "destination": "/contributing/how-to-report-a-bug", + "permanent": true + }, + { + "source": "/contributing/versioning-policy", + "destination": "/docs/next/releases/versioning-policy", + "permanent": true + }, + { + "source": "/releases/release-candidate-minor", + "destination": "/docs/next/releases/versioning-policy", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/why", + "destination": "/architecture/landing-page", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/use-app-template", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/pillars", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/pillars-turbomodules", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/pillars-fabric-components", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/pillars-codegen", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/cxx-cxxturbomodules", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/the-new-architecture/cxx-custom-types", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/backward-compatibility", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/backward-compatibility-turbomodules", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/backward-compatibility-fabric-components", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/the-new-architecture/landing-page", + "destination": "/architecture/landing-page", + "permanent": true + }, + { + "source": "/docs/new-architecture-intro", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/new-architecture-library-intro", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/new-architecture-library-android", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/new-architecture-library-ios", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/new-architecture-app-intro", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/react-18-and-react-native", + "destination": "/docs/0.69/react-18-and-react-native", + "permanent": true + }, + { + "source": "/docs/new-architecture-troubleshooting", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/new-architecture-appendix", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/landing-page", + "destination": "/architecture/landing-page", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/why", + "destination": "/architecture/landing-page", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/use-app-template", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/pillars", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/pillars-turbomodules", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/pillars-fabric-components", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/pillars-codegen", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/cxx-cxxturbomodules", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/cxx-custom-types", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/backward-compatibility", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/backward-compatibility-turbomodules", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/the-new-architecture/backward-compatibility-fabric-components", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/new-architecture-intro", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/new-architecture-library-intro", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/new-architecture-library-android", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/new-architecture-library-ios", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/new-architecture-app-intro", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/new-architecture-troubleshooting", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/:version/new-architecture-appendix", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/next/direct-manipulation", + "destination": "/docs/next/legacy/direct-manipulation", + "permanent": true + }, + { + "source": "/docs/next/local-library-setup", + "destination": "/docs/next/legacy/local-library-setup", + "permanent": true + }, + { + "source": "/docs/next/native-modules-android", + "destination": "/docs/next/legacy/native-modules-android", + "permanent": true + }, + { + "source": "/docs/next/native-modules-intro", + "destination": "/docs/next/legacy/native-modules-intro", + "permanent": true + }, + { + "source": "/docs/next/native-modules-ios", + "destination": "/docs/next/legacy/native-modules-ios", + "permanent": true + }, + { + "source": "/docs/next/native-modules-setup", + "destination": "/docs/next/legacy/native-modules-setup", + "permanent": true + }, + { + "source": "/docs/next/native-components-android", + "destination": "/docs/next/legacy/native-components-android", + "permanent": true + }, + { + "source": "/docs/next/native-components-ios", + "destination": "/docs/next/legacy/native-components-ios", + "permanent": true + }, + { + "source": "/blog/2021/03/11/version-0.64", + "destination": "/blog/2021/03/12/version-0.64", + "permanent": true + }, + { + "source": "/docs/new-architecture-app-modules-android", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/new-architecture-app-renderer-android", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/new-architecture-app-modules-ios", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/new-architecture-app-renderer-ios", + "destination": "https://github.com/reactwg/react-native-new-architecture#guides", + "permanent": true + }, + { + "source": "/docs/0.74/ram-bundles-inline-requires", + "destination": "/docs/optimizing-javascript-loading", + "permanent": true + }, + { + "source": "/docs/next/ram-bundles-inline-requires", + "destination": "/docs/next/optimizing-javascript-loading", + "permanent": true + }, + { + "source": "/docs/architecture-overview", + "destination": "/architecture/overview", + "permanent": true + }, + { + "source": "/docs/fabric-renderer", + "destination": "/architecture/fabric-renderer", + "permanent": true + }, + { + "source": "/docs/render-pipeline", + "destination": "/architecture/render-pipeline", + "permanent": true + }, + { + "source": "/docs/xplat-implementation", + "destination": "/architecture/xplat-implementation", + "permanent": true + }, + { + "source": "/docs/view-flattening", + "destination": "/architecture/view-flattening", + "permanent": true + }, + { + "source": "/docs/threading-model", + "destination": "/architecture/threading-model", + "permanent": true + }, + { + "source": "/docs/architecture-glossary", + "destination": "/architecture/glossary", + "permanent": true + }, + { + "source": "/docs/next/architecture-overview", + "destination": "/architecture/overview", + "permanent": true + }, + { + "source": "/docs/next/fabric-renderer", + "destination": "/architecture/fabric-renderer", + "permanent": true + }, + { + "source": "/docs/next/render-pipeline", + "destination": "/architecture/render-pipeline", + "permanent": true + }, + { + "source": "/docs/next/xplat-implementation", + "destination": "/architecture/xplat-implementation", + "permanent": true + }, + { + "source": "/docs/next/view-flattening", + "destination": "/architecture/view-flattening", + "permanent": true + }, + { + "source": "/docs/next/threading-model", + "destination": "/architecture/threading-model", + "permanent": true + }, + { + "source": "/docs/next/architecture-glossary", + "destination": "/architecture/glossary", + "permanent": true + }, + { + "source": "/docs/0.66/architecture-overview", + "destination": "/architecture/overview", + "permanent": true + }, + { + "source": "/docs/0.66/fabric-renderer", + "destination": "/architecture/fabric-renderer", + "permanent": true + }, + { + "source": "/docs/0.66/render-pipeline", + "destination": "/architecture/render-pipeline", + "permanent": true + }, + { + "source": "/docs/0.66/xplat-implementation", + "destination": "/architecture/xplat-implementation", + "permanent": true + }, + { + "source": "/docs/0.66/view-flattening", + "destination": "/architecture/view-flattening", + "permanent": true + }, + { + "source": "/docs/0.66/threading-model", + "destination": "/architecture/threading-model", + "permanent": true + }, + { + "source": "/docs/0.66/architecture-glossary", + "destination": "/architecture/glossary", + "permanent": true + }, + { + "source": "/contributing/release-branch-cut-and-rc0", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/release-candidate-patch", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/release-dependencies", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/release-faq", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/release-roles-responsibilites", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/release-stable-minor", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/release-stable-patch", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/release-testing", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/release-troubleshooting", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/release-updating-packages", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/contributing/updating-upgrade-helper", + "destination": "https://github.com/reactwg/react-native-releases#release-documentation", + "permanent": true + }, + { + "source": "/help", + "destination": "/community/overview", + "permanent": true + }, + { + "source": "/docs/0.85/:path*", + "destination": "/docs/:path*", + "permanent": true + } + ] +} diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 8e160093ccf..f2984614a2e 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -17,9 +17,9 @@ import prismTheme from './core/PrismTheme'; import remarkSnackPlayer from '@react-native-website/remark-snackplayer'; import remarkCodeblockLanguageTitle from '@react-native-website/remark-codeblock-language-as-title'; -// See https://docs.netlify.com/configure-builds/environment-variables/ const isProductionDeployment = - !!process.env.NETLIFY && process.env.CONTEXT === 'production'; + (!!process.env.NETLIFY && process.env.CONTEXT === 'production') || + (!!process.env.VERCEL && process.env.VERCEL_ENV === 'production'); const lastVersion = versions[0]; const copyright = `Copyright © ${new Date().getFullYear()} Meta Platforms, Inc.`; @@ -64,7 +64,9 @@ const commonDocsOptions: PluginContentDocs.Options = { remarkPlugins: [remarkSnackPlayer, remarkCodeblockLanguageTitle], }; -const isDeployPreview = process.env.PREVIEW_DEPLOY === 'true'; +const isDeployPreview = + process.env.PREVIEW_DEPLOY === 'true' || + (!!process.env.VERCEL && process.env.VERCEL_ENV === 'preview'); const config: Config = { future: { diff --git a/website/package.json b/website/package.json index a5cc861d649..e3ac9318e61 100644 --- a/website/package.json +++ b/website/package.json @@ -30,7 +30,12 @@ "lint:markdown:links": "remark ../docs --quiet -r .remarkrc.withBrokenLinks.mjs", "ci:lint": "yarn lint && yarn lint:examples && yarn lint:markdown:images && prettier --check src/**/*.scss", "pwa:generate": "npx pwa-asset-generator ./static/img/header_logo.svg ./static/img/pwa --padding '40px' --background 'rgb(32, 35, 42)' --icon-only --opaque true", - "update-redirects": "node ../scripts/src/update-redirects.ts" + "update-redirects": "node ../scripts/src/update-redirects.ts", + "build:vercel": "docusaurus build", + "build:vercel:fast": "VERCEL=1 VERCEL_ENV=preview docusaurus build", + "deploy:vercel": "vercel", + "deploy:vercel:prod": "vercel --prod", + "dev:vercel": "vercel dev" }, "browserslist": { "production": [ From 39b9ea55c1502dede6b460d42aeee8cbaa23dcc5 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Wed, 22 Apr 2026 16:21:04 -0700 Subject: [PATCH 2/4] 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". --- vercel.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vercel.json b/vercel.json index f6585720fe8..bad5b5a61a1 100644 --- a/vercel.json +++ b/vercel.json @@ -1,7 +1,7 @@ { - "buildCommand": "cd website && yarn build:vercel", - "devCommand": "cd website && yarn start", - "outputDirectory": "website/build", + "buildCommand": "yarn build:vercel", + "devCommand": "yarn start", + "outputDirectory": "build", "installCommand": "yarn install", "framework": null, "git": { From 4aa88d4aeba8a248bd4431153b776401a63e3584 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Thu, 23 Apr 2026 13:33:49 -0700 Subject: [PATCH 3/4] 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. --- .github/workflows/pre-merge.yml | 23 +++++ package.json | 1 + scripts/src/sync-vercel-redirects.ts | 124 +++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 scripts/src/sync-vercel-redirects.ts diff --git a/.github/workflows/pre-merge.yml b/.github/workflows/pre-merge.yml index c24cc4c1429..25777d7e46e 100644 --- a/.github/workflows/pre-merge.yml +++ b/.github/workflows/pre-merge.yml @@ -33,6 +33,29 @@ jobs: - name: Run plugins lint run: yarn lint:plugins + check-vercel-redirects: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: "22" + cache: yarn + + - name: Install dependencies + run: yarn install --immutable + + - name: Check Vercel redirects are in sync + run: | + yarn sync-redirects:vercel + if ! git diff --exit-code vercel.json; then + echo "::error::vercel.json redirects are out of sync with _redirects. Run 'yarn sync-redirects:vercel' and commit." + exit 1 + fi + lint-website: runs-on: ubuntu-latest steps: diff --git a/package.json b/package.json index 0736e32b099..545e3f19dfb 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "lint:website": "eslint ./website ./docs", "update-lock": "npx yarn-deduplicate", "check-dependencies": "manypkg check", + "sync-redirects:vercel": "node scripts/src/sync-vercel-redirects.ts", "build:vercel": "yarn --cwd website build:vercel", "build:vercel:fast": "yarn --cwd website build:vercel:fast", "dev:vercel": "vercel dev" diff --git a/scripts/src/sync-vercel-redirects.ts b/scripts/src/sync-vercel-redirects.ts new file mode 100644 index 00000000000..42cea3f099e --- /dev/null +++ b/scripts/src/sync-vercel-redirects.ts @@ -0,0 +1,124 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +const REPO_ROOT = path.resolve(import.meta.dirname, '../..'); +const REDIRECTS_PATH = path.join(REPO_ROOT, 'website/static/_redirects'); +const VERSIONS_PATH = path.join(REPO_ROOT, 'website/versions.json'); +const VERCEL_JSON_PATH = path.join(REPO_ROOT, 'vercel.json'); + +interface VercelRedirect { + source: string; + destination: string; + permanent: boolean; +} + +function isPartialSegmentWildcard(source: string): boolean { + return source.split('/').some(seg => seg.includes('*') && seg !== '*'); +} + +function expandPartialWildcard( + source: string, + destination: string +): VercelRedirect[] { + const segments = source.split('/'); + const wildcardSeg = segments.find(seg => seg.includes('*') && seg !== '*')!; + const prefix = wildcardSeg.replace('*', ''); + + // Infer docs directory from destination path + // e.g. /docs/next/legacy/native-modules-:splat → docs/legacy/ + const destDir = destination + .replace(/^\/docs\/next\//, '') + .replace(/\/[^/]*$/, ''); + const searchDir = path.join(REPO_ROOT, 'docs', destDir); + + let files: string[]; + try { + files = fs + .readdirSync(searchDir) + .filter(f => f.startsWith(prefix) && /\.mdx?$/.test(f)) + .map(f => f.replace(/\.mdx?$/, '')); + } catch { + console.warn( + `Warning: Could not read ${searchDir} for expanding ${source}` + ); + return []; + } + + return files.map(filename => { + const suffix = filename.slice(prefix.length); + return { + source: source.replace(`${prefix}*`, filename), + destination: destination.replace(':splat', suffix), + permanent: true, + }; + }); +} + +function syncRedirects(): void { + const latestVersion: string = JSON.parse( + fs.readFileSync(VERSIONS_PATH, 'utf8') + )[0]; + + const lines = fs.readFileSync(REDIRECTS_PATH, 'utf8').split('\n'); + const redirects: VercelRedirect[] = []; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + + const parts = trimmed.split(/\s+/); + if (parts.length < 2) continue; + + let [source, destination] = parts; + + // Strip full URL sources to path only + if (source.startsWith('http://') || source.startsWith('https://')) { + try { + source = new URL(source).pathname; + } catch { + continue; + } + } + + // Convert same-domain full URL destinations to relative paths + if (destination.startsWith('https://reactnative.dev/')) { + destination = destination.replace('https://reactnative.dev', ''); + } + + // Replace $LATEST_VERSION$ placeholder with actual version + source = source.replaceAll('$LATEST_VERSION$', latestVersion); + destination = destination.replaceAll('$LATEST_VERSION$', latestVersion); + + // Handle partial-segment wildcards (e.g., native-modules-*) + // Vercel doesn't support wildcards within a path segment, so enumerate + if (isPartialSegmentWildcard(source)) { + redirects.push(...expandPartialWildcard(source, destination)); + continue; + } + + // Convert Netlify wildcard syntax to Vercel format + // Netlify: /* and :splat → Vercel: /:path* + source = source.replace(/\*$/, ':path*'); + destination = destination.replace(':splat', ':path*'); + + redirects.push({source, destination, permanent: true}); + } + + const vercelJson = JSON.parse(fs.readFileSync(VERCEL_JSON_PATH, 'utf8')); + vercelJson.redirects = redirects; + fs.writeFileSync( + VERCEL_JSON_PATH, + JSON.stringify(vercelJson, null, 2) + '\n' + ); + + console.log(`Synced ${redirects.length} redirects to vercel.json`); +} + +syncRedirects(); From 4d6e22a183b41a87bcb00dc1fc8b2839756ce8f0 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Thu, 23 Apr 2026 14:02:13 -0700 Subject: [PATCH 4/4] fix: Add permissions block to workflow, use replaceAll for wildcards --- .github/workflows/pre-merge.yml | 3 +++ scripts/src/sync-vercel-redirects.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pre-merge.yml b/.github/workflows/pre-merge.yml index 25777d7e46e..247c64ae0a0 100644 --- a/.github/workflows/pre-merge.yml +++ b/.github/workflows/pre-merge.yml @@ -5,6 +5,9 @@ on: branches: - main +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest diff --git a/scripts/src/sync-vercel-redirects.ts b/scripts/src/sync-vercel-redirects.ts index 42cea3f099e..951d3790536 100644 --- a/scripts/src/sync-vercel-redirects.ts +++ b/scripts/src/sync-vercel-redirects.ts @@ -29,7 +29,7 @@ function expandPartialWildcard( ): VercelRedirect[] { const segments = source.split('/'); const wildcardSeg = segments.find(seg => seg.includes('*') && seg !== '*')!; - const prefix = wildcardSeg.replace('*', ''); + const prefix = wildcardSeg.replaceAll('*', ''); // Infer docs directory from destination path // e.g. /docs/next/legacy/native-modules-:splat → docs/legacy/