diff --git a/.github/actions/website-install/action.yml b/.github/actions/website-install/action.yml
new file mode 100644
index 0000000000..67b5eaf1e4
--- /dev/null
+++ b/.github/actions/website-install/action.yml
@@ -0,0 +1,15 @@
+name: Install Website Dependencies
+
+runs:
+ using: "composite"
+ steps:
+ - uses: actions/setup-node@v6
+ name: Setup Node With Yarn Cache
+ with:
+ node-version: lts/*
+ cache: yarn
+ cache-dependency-path: website/yarn.lock
+ - name: Install Dependencies
+ run: yarn --frozen-lockfile
+ working-directory: website
+ shell: bash
diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml
index 643d2b98d1..098604183d 100644
--- a/.github/workflows/deploy-website.yml
+++ b/.github/workflows/deploy-website.yml
@@ -20,35 +20,15 @@ jobs:
if: github.repository == 'react/metro'
- env:
- working-directory: website
- node-version: lts/*
-
steps:
- name: Checkout code
uses: actions/checkout@v7
- - name: Use Node.js ${{ env.node-version }}
- uses: actions/setup-node@v6
- with:
- node-version: ${{ env.node-version }}
-
- - name: Get yarn cache directory
- id: yarn
- run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
-
- - name: Restore yarn cache
- uses: actions/cache@v6
- with:
- path: ${{ steps.yarn.outputs.dir }}
- key: ${{ runner.os }}-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/website/yarn.lock')) }}
-
- name: Install packages
- working-directory: ${{ env.working-directory }}
- run: yarn --frozen-lockfile
+ uses: ./.github/actions/website-install
- name: Deploy to GitHub Pages
- working-directory: ${{ env.working-directory }}
+ working-directory: website
run: |
git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
git config --global user.name "${GITHUB_ACTOR}"
diff --git a/.github/workflows/link-website-preview.yml b/.github/workflows/link-website-preview.yml
new file mode 100644
index 0000000000..0a671d87d4
--- /dev/null
+++ b/.github/workflows/link-website-preview.yml
@@ -0,0 +1,93 @@
+# Comments on PRs from forks that change the website with a link to the website
+# preview that preview-website.yml publishes from the fork, once the preview
+# serves the PR head, and with a warning that the fork owner hosts and controls
+# the preview. Edits the same comment on later pushes.
+#
+# Never checks out PR code.
+name: Link website preview
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+ paths:
+ - "docs/**"
+ - "website/**"
+ - ".github/workflows/preview-website.yml"
+ - ".github/workflows/link-website-preview.yml"
+ - ".github/actions/website-install/**"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: link-website-preview-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ link:
+ name: Website preview
+ if: github.event.pull_request.head.repo.fork && github.event.pull_request.head.ref != 'gh-pages'
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: actions/github-script@v8
+ with:
+ script: |
+ const {number, head} = context.payload.pull_request;
+ // Matches the preview directory in preview-website.yml.
+ const previewDir = head.ref.replace(/[^A-Za-z0-9._\/-]/gu, '-');
+ const url = `https://${head.repo.owner.login}.github.io/${head.repo.name}/${previewDir}/`;
+ const sha = head.sha.slice(0, 7);
+
+ let live = false;
+ for (let i = 0; i < 90 && !live; i++) {
+ if (i > 0) await new Promise(resolve => setTimeout(resolve, 10_000));
+ try {
+ const res = await fetch(`${url}.sha?${head.sha}`);
+ live = res.ok && (await res.text()).trim() === head.sha;
+ } catch (error) {
+ core.info(`Fetching ${url}.sha failed: ${error.message}`);
+ }
+ }
+ if (!live) {
+ const title = 'Website preview not deployed';
+ await core.summary
+ .addHeading(title, 3)
+ .addRaw(`This link didn't serve ${sha} within 15 minutes:`, true)
+ .addCodeBlock(url)
+ .addRaw('To publish the preview from your fork:', true)
+ .addList([
+ 'Enable workflows in the Actions tab.',
+ 'Set Settings > Pages > Source to "Deploy from a branch", gh-pages, / (root).',
+ 'Push the branch again, and check that the "Preview website" workflow passes in the Actions tab.',
+ ])
+ .write();
+ core.error(`${url} didn't serve ${sha} within 15 minutes.`, {title});
+ process.exitCode = core.ExitCode.Failure;
+ return;
+ }
+
+ const MARKER = '';
+ const BOT = 'github-actions[bot]';
+ const body = [
+ MARKER,
+ '## Website preview',
+ '',
+ '> [!WARNING]',
+ `> **Hosted by \`${head.repo.owner.login}\` on the GitHub Pages of \`${head.repo.full_name}\`, not by this repository. Its owner controls what it serves.**`,
+ '>',
+ `> ${url}`,
+ '>',
+ `> _Built from ${sha}._`,
+ ].join('\n');
+ const {owner, repo} = context.repo;
+ const comments = await github.paginate(github.rest.issues.listComments, {owner, repo, issue_number: number, per_page: 100});
+ const existing = comments.find(c => c.user.login === BOT && c.body.startsWith(MARKER));
+ if (existing) {
+ await github.rest.issues.updateComment({owner, repo, comment_id: existing.id, body});
+ } else {
+ await github.rest.issues.createComment({owner, repo, issue_number: number, body});
+ }
+ core.notice(`Preview: ${url}`);
diff --git a/.github/workflows/preview-website.yml b/.github/workflows/preview-website.yml
new file mode 100644
index 0000000000..b753ffc7f1
--- /dev/null
+++ b/.github/workflows/preview-website.yml
@@ -0,0 +1,79 @@
+# Builds the website, which checks links between doc pages.
+#
+# In forks, also publishes a preview of the website from each pushed branch, at
+# https://.github.io///, with characters in other
+# than letters, digits, `.`, `_`, `/` and `-` replaced by `-`. To enable it,
+# enable workflows in the Actions tab, then set Settings > Pages > Source to
+# "Deploy from a branch", `gh-pages`, `/ (root)`.
+name: Preview website
+
+on:
+ pull_request:
+ paths:
+ - "docs/**"
+ - "website/**"
+ - ".github/workflows/preview-website.yml"
+ - ".github/actions/website-install/**"
+ push:
+ branches-ignore:
+ - "gh-pages"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: preview-website-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ if: github.event_name == 'pull_request' || github.repository != 'react/metro'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # to push previews to gh-pages
+
+ steps:
+ - uses: actions/checkout@v7
+ - uses: ./.github/actions/website-install
+ - name: Set the preview directory
+ if: github.event_name == 'push'
+ env:
+ REF_NAME: ${{ github.ref_name }}
+ REPO_NAME: ${{ github.event.repository.name }}
+ run: |
+ # Matches link-website-preview.yml.
+ preview_dir=$(printf '%s' "$REF_NAME" | LC_ALL=C.UTF-8 sed 's|[^A-Za-z0-9._/-]|-|g')
+ echo "PREVIEW_DIR=$preview_dir" >> "$GITHUB_ENV"
+ echo "WEBSITE_BASE_URL=/$REPO_NAME/$preview_dir/" >> "$GITHUB_ENV"
+ - run: yarn build
+ working-directory: website
+ - name: Push preview to gh-pages
+ if: github.event_name == 'push'
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ if git fetch --depth=1 origin gh-pages; then
+ git worktree add gh-pages FETCH_HEAD
+ else
+ git worktree add --orphan -b gh-pages gh-pages
+ fi
+ # Runs for other branches push to gh-pages concurrently. Each writes only
+ # its own directory, so a rejected push is redone on the new tip.
+ for attempt in 1 2 3 4 5; do
+ if [ "$attempt" -gt 1 ]; then
+ git -C gh-pages fetch --depth=1 origin gh-pages
+ git -C gh-pages reset -q --hard FETCH_HEAD
+ fi
+ rm -rf "gh-pages/$PREVIEW_DIR"
+ mkdir -p "gh-pages/$PREVIEW_DIR"
+ cp -R website/build/. "gh-pages/$PREVIEW_DIR"
+ echo "$GITHUB_SHA" > "gh-pages/$PREVIEW_DIR/.sha"
+ touch gh-pages/.nojekyll
+ git -C gh-pages add -A
+ # Unchanged when re-run for an already published commit.
+ git -C gh-pages diff --cached --quiet && break
+ git -C gh-pages commit -q -m "Preview $PREVIEW_DIR at $GITHUB_SHA"
+ git -C gh-pages push origin HEAD:gh-pages && break
+ [ "$attempt" -lt 5 ] || exit 1
+ done
+ echo "Preview: https://${GITHUB_REPOSITORY_OWNER}.github.io${WEBSITE_BASE_URL}" >> "$GITHUB_STEP_SUMMARY"
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index cd1fa75c78..e540ebbc26 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -49,7 +49,7 @@ const siteConfig = {
organizationName: 'react',
projectName: 'metro',
url: 'https://metrobundler.dev',
- baseUrl: '/',
+ baseUrl: process.env.WEBSITE_BASE_URL ?? '/',
favicon: 'img/favicon.png',
onBrokenAnchors: 'throw',
themeConfig: {
diff --git a/website/src/pages/help/index.js b/website/src/pages/help/index.js
index a546288693..6ab921cc32 100644
--- a/website/src/pages/help/index.js
+++ b/website/src/pages/help/index.js
@@ -8,6 +8,7 @@
* @oncall react_native
*/
+import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import * as React from 'react';
@@ -19,17 +20,18 @@ const supportLinks = [
content: (
-
- Learn how to get started with
+ Learn how to get started with
Metro.
-
- Troubleshoot problems with Metro.
+ Troubleshoot problems with
+ Metro.
-
- Learn how to configure Metro.
+ Learn how to configure Metro.
-
- Look at the full API Reference.
+ Look at the full API Reference.
),