From 3950a39b9d6267c06dde8152ee9b65355148b324 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Tue, 14 Apr 2026 12:56:16 -0500 Subject: [PATCH 1/9] docs: add framework styling explanation (#14976) --- docs/explanation/styling.md | 89 ++++++++++++++++++++++++++++ docs/start/framework/route-module.md | 5 ++ 2 files changed, 94 insertions(+) create mode 100644 docs/explanation/styling.md diff --git a/docs/explanation/styling.md b/docs/explanation/styling.md new file mode 100644 index 0000000000..747242971b --- /dev/null +++ b/docs/explanation/styling.md @@ -0,0 +1,89 @@ +--- +title: Styling +--- + +# Styling + +[MODES: framework] + +
+
+ +Framework mode uses the React Router Vite plugin, so the styling story is mostly just Vite's styling story. + +React Router does not have a separate CSS pipeline for Framework mode. In practice, there are three patterns that matter: + +1. Import CSS as a side effect +2. Use the route module `links` export +3. Render a stylesheet `` directly + +## Side-Effect CSS Imports + +Because Framework mode uses Vite, you can import CSS files as side effects: + +```tsx filename=app/root.tsx +import "./app.css"; +``` + +```tsx filename=app/routes/dashboard.tsx +import "./dashboard.css"; +``` + +This is often the simplest option. Global styles can be imported in `root.tsx`, and route or component styles can be imported next to the module that uses them. + +## `links` Export + +React Router also supports adding stylesheets through the route module `links` export. + +This is useful when you want a stylesheet URL from Vite and need React Router to render a real `` tag for the route: + +```tsx filename=app/routes/dashboard.tsx +import dashboardHref from "./dashboard.css?url"; + +export function links() { + return [ + { rel: "stylesheet", href: dashboardHref }, + ]; +} +``` + +The `links` export feeds the [``][links-component] component in your root route. This is the React Router-specific styling API in Framework mode. For more on route module exports, see [Route Module][route-module]. + +## Direct `` Rendering + +If you're using React 19, you can also render a stylesheet `` directly in your route component: + +```tsx filename=app/routes/dashboard.tsx +import dashboardHref from "./dashboard.css?url"; + +export default function Dashboard() { + return ( + <> + +

Dashboard

+ + ); +} +``` + +This uses React's built-in [``][react-link] support, which hoists the stylesheet into the document ``. That gives you another way to colocate stylesheet tags with the route that needs them. + +## Everything Else + +For CSS Modules, Tailwind, PostCSS, Sass, Vanilla Extract, and other styling tools, use the normal Vite setup for those tools. + +See: + +- [Vite CSS Features][vite-css] +- [Vite Static Asset Handling][vite-assets] +- [``][links-component] + +[links-component]: ../api/components/Links +[react-link]: https://react.dev/reference/react-dom/components/link +[route-module]: ../start/framework/route-module +[vite-assets]: https://vite.dev/guide/assets.html +[vite-css]: https://vite.dev/guide/features.html#css diff --git a/docs/start/framework/route-module.md b/docs/start/framework/route-module.md index 8bc17cbf5e..fe780e6773 100644 --- a/docs/start/framework/route-module.md +++ b/docs/start/framework/route-module.md @@ -411,6 +411,10 @@ export default function Root() { } ``` +See also: + +- [Styling][styling] + ## `meta` Route meta defines [meta tags][meta-element] to be rendered in the `` component, usually placed in the ``. @@ -520,3 +524,4 @@ Next: [Rendering Strategies](./rendering) [data-mode-should-revalidate]: ../data/route-object#shouldrevalidate [spa-mode]: ../../how-to/spa [client-data]: ../../how-to/client-data +[styling]: ../../explanation/styling From aa048bacebf5b8afdc7235c3033df10841d4a5d8 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Tue, 14 Apr 2026 17:57:03 +0000 Subject: [PATCH 2/9] chore: format --- docs/explanation/styling.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/explanation/styling.md b/docs/explanation/styling.md index 747242971b..6f1dd987a1 100644 --- a/docs/explanation/styling.md +++ b/docs/explanation/styling.md @@ -41,9 +41,7 @@ This is useful when you want a stylesheet URL from Vite and need React Router to import dashboardHref from "./dashboard.css?url"; export function links() { - return [ - { rel: "stylesheet", href: dashboardHref }, - ]; + return [{ rel: "stylesheet", href: dashboardHref }]; } ``` From a265e93bca444fa0c3359da96952e86939b6c8ee Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 14 Apr 2026 14:14:43 -0400 Subject: [PATCH 3/9] Add script to delete old changeset bot comments --- .../delete-changeset-bot-comments.yml | 34 +++++++ .../changes/delete-changeset-bot-comments.ts | 91 +++++++++++++++++++ scripts/utils/github.ts | 23 +++++ 3 files changed, 148 insertions(+) create mode 100644 .github/workflows/delete-changeset-bot-comments.yml create mode 100644 scripts/changes/delete-changeset-bot-comments.ts diff --git a/.github/workflows/delete-changeset-bot-comments.yml b/.github/workflows/delete-changeset-bot-comments.yml new file mode 100644 index 0000000000..32536b5065 --- /dev/null +++ b/.github/workflows/delete-changeset-bot-comments.yml @@ -0,0 +1,34 @@ +name: ๐Ÿ—‘๏ธ Delete Changeset Bot Comments + +on: + workflow_dispatch: + +jobs: + delete-comments: + name: ๐Ÿ—‘๏ธ Delete Changeset Bot Comments + if: github.repository == 'remix-run/react-router' + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v6 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: pnpm + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ—‘๏ธ Delete changeset-bot comments + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + node scripts/changes/delete-changeset-bot-comments.ts diff --git a/scripts/changes/delete-changeset-bot-comments.ts b/scripts/changes/delete-changeset-bot-comments.ts new file mode 100644 index 0000000000..d4b10a70cd --- /dev/null +++ b/scripts/changes/delete-changeset-bot-comments.ts @@ -0,0 +1,91 @@ +/** + * Finds and deletes comments from `changeset-bot` on open PRs created since 1/1/2026 + * + * Usage: + * node scripts/changes/delete-changeset-bot-comments.ts [--dry-run] + * + * Environment: + * GITHUB_TOKEN - Required. GitHub token with pull-requests:write permission. + */ +import { + createPrComment, + deletePrComment, + getPrComments, + listOpenPrs, +} from "../utils/github.ts"; + +const CHANGESET_BOT = "changeset-bot[bot]"; +const CUTOFF = new Date(2026, 0, 1); + +const ADD_CHANGE_FILE = + "๐Ÿ‘‹ We've moved away from Changesets to our own internal " + + "[changes process](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files)]. " + + "Please manually add a change file to this branch, or you can merge in the " + + "latest `dev` branch and run `pnpm run changes:add` to add a change file."; + +const MIGRATE_CHANGE_FILE = + "๐Ÿ‘‹ We've moved away from Changesets to our own internal " + + "[changes process](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files)]. " + + "Please convert your changesets file to a change file in the proper package directory " + + "(i.e., `packages/react-router/.changes/patch.fix-some-bug.md`)."; + +const dryRun = process.argv.includes("--dry-run"); + +if (dryRun) { + console.log("[DRY RUN] No comments will be deleted.\n"); +} + +console.log( + `Fetching open PRs created after ${CUTOFF.toISOString().slice(0, 10)}...\n`, +); + +let prs = await listOpenPrs({ + createdAfter: CUTOFF, + base: "dev", + // TODO: For testing + author: "brophdawg11", +}); +console.log(`Found ${prs.length} open PR${prs.length === 1 ? "" : "s"}.\n`); + +let totalDeleted = 0; +let totalSkipped = 0; + +for (let pr of prs) { + let comments = await getPrComments(pr.number); + let botComments = comments.filter((c) => c.user?.login === CHANGESET_BOT); + + if (botComments.length === 0) continue; + + console.log(`PR #${pr.number}: ${pr.title}`); + + for (let comment of botComments) { + let preview = (comment.body ?? "").slice(0, 80).replace(/\n/g, " "); + if (dryRun) { + console.log( + ` [DRY RUN] Would delete comment #${comment.id}: "${preview}"`, + ); + totalSkipped++; + } else { + let hasChangeFile = comment.body?.includes("Changeset detected"); + await deletePrComment(comment.id); + await createPrComment( + pr.number, + hasChangeFile ? MIGRATE_CHANGE_FILE : ADD_CHANGE_FILE, + ); + console.log(` Deleted comment #${comment.id}: "${preview}"`); + totalDeleted++; + } + } + + console.log(); +} + +if (dryRun) { + console.log( + `Done (dry run): ${totalSkipped} comment${totalSkipped === 1 ? "" : "s"} would be deleted.`, + ); +} else { + console.log( + `Done: ${totalDeleted} comment${totalDeleted === 1 ? "" : "s"} deleted.`, + ); +} diff --git a/scripts/utils/github.ts b/scripts/utils/github.ts index 5b5e413933..b100b0eeb3 100644 --- a/scripts/utils/github.ts +++ b/scripts/utils/github.ts @@ -80,6 +80,29 @@ export async function createRelease( } } +/** + * List open PRs + */ +export async function listOpenPrs( + options: { createdAfter?: Date; base?: string; author?: string } = {}, +) { + let response = await request("GET /repos/{owner}/{repo}/pulls", { + ...requestOptions(), + state: "open", + sort: "created", + direction: "desc", + per_page: 100, + ...(options.base ? { base: options.base } : {}), + }); + + return response.data.filter( + (pr) => + (!options.createdAfter || + new Date(pr.created_at) >= options.createdAfter) && + (!options.author || pr.user?.login === options.author), + ); +} + /** * Find an open PR from a specific branch to a base branch */ From b2f5b3be121b6972f83b95d9031dda0233284b68 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 14 Apr 2026 14:17:15 -0400 Subject: [PATCH 4/9] Fix typo in comments --- scripts/changes/delete-changeset-bot-comments.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/changes/delete-changeset-bot-comments.ts b/scripts/changes/delete-changeset-bot-comments.ts index d4b10a70cd..f4bec0a463 100644 --- a/scripts/changes/delete-changeset-bot-comments.ts +++ b/scripts/changes/delete-changeset-bot-comments.ts @@ -19,13 +19,13 @@ const CUTOFF = new Date(2026, 0, 1); const ADD_CHANGE_FILE = "๐Ÿ‘‹ We've moved away from Changesets to our own internal " + - "[changes process](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files)]. " + + "[changes process](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files). " + "Please manually add a change file to this branch, or you can merge in the " + "latest `dev` branch and run `pnpm run changes:add` to add a change file."; const MIGRATE_CHANGE_FILE = "๐Ÿ‘‹ We've moved away from Changesets to our own internal " + - "[changes process](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files)]. " + + "[changes process](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files). " + "Please convert your changesets file to a change file in the proper package directory " + "(i.e., `packages/react-router/.changes/patch.fix-some-bug.md`)."; From f47268b3c1023a7c8a002beb8c1db50c974965a1 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 14 Apr 2026 14:29:42 -0400 Subject: [PATCH 5/9] Reduce to an h3 in check-pr comment --- scripts/changes/check-pr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/changes/check-pr.ts b/scripts/changes/check-pr.ts index d3c0c095ea..59bb95e35e 100644 --- a/scripts/changes/check-pr.ts +++ b/scripts/changes/check-pr.ts @@ -21,12 +21,12 @@ import { const COMMENT_MARKER = ""; const COMMENT_FOUND = `${COMMENT_MARKER} -## โœ… Change File Found +### โœ… Change File Found A \`.changes\` file has been found in this PR. Thanks!`; const COMMENT_MISSING = `${COMMENT_MARKER} -## ๐Ÿ“ No Change File Found +### ๐Ÿ“ No Change File Found This PR doesn't include a change file which is used for automated release notes. If your change affects users, please add one (or more) and commit the generated file(s). From 4e02aadd46c3de1af9d6d18191001bc41ad797cf Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 14 Apr 2026 15:40:47 -0400 Subject: [PATCH 6/9] Re-enable experimental releases --- .github/workflows/release.yml | 60 ++++++++++++- GOVERNANCE.md | 4 +- package.json | 2 + scripts/experimental.ts | 151 +++++++++++++++++++++++++++++++++ scripts/publish.js | 154 ---------------------------------- scripts/version.js | 64 -------------- 6 files changed, 212 insertions(+), 223 deletions(-) create mode 100644 scripts/experimental.ts delete mode 100644 scripts/publish.js delete mode 100644 scripts/version.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 971fb120ee..4e81985163 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,12 @@ +# We use this singular file for all of our releases because we can only specify +# a singular GitHub workflow file in npm's Trusted Publishing configuration. +# See https://docs.npmjs.com/trusted-publishers for more info. +# +# Specific jobs only run on the proper trigger: +# +# - Change file driven stable releases (push to release/hotfix branches) +# - Experimental releases (from a workflow_dispatch trigger) + name: ๐Ÿšข Release/Publish on: @@ -5,6 +14,10 @@ on: branches: - "release" - "hotfix" + workflow_dispatch: + inputs: + branch: + required: true concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -13,7 +26,7 @@ concurrency: jobs: check: name: Check release readiness - if: github.repository == 'remix-run/react-router' + if: github.repository == 'remix-run/react-router' && github.event_name == 'push' runs-on: ubuntu-latest outputs: has_change_files: ${{ steps.check.outputs.has_change_files }} @@ -38,7 +51,7 @@ jobs: pull_request: name: Open pull request needs: check - if: needs.check.outputs.has_change_files == 'true' + if: github.event_name == 'push' && needs.check.outputs.has_change_files == 'true' runs-on: ubuntu-latest permissions: contents: write # enable pushing changes to the origin @@ -69,7 +82,7 @@ jobs: publish: name: Publish needs: check - if: needs.check.outputs.has_change_files == 'false' + if: github.event_name == 'push' && needs.check.outputs.has_change_files == 'false' runs-on: ubuntu-latest permissions: contents: write # enable pushing changes to the origin @@ -97,3 +110,44 @@ jobs: run: pnpm changes:publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + experimental-release: + name: ๐Ÿงช Experimental Release + if: github.repository == 'remix-run/react-router' && github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write # enable pushing changes to the origin + id-token: write # enable generation of an ID token for publishing + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.branch }} + # checkout using a custom token so that we can push later on + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: ๐Ÿ“ฆ Setup pnpm + uses: pnpm/action-setup@v4 + + - name: โŽ” Setup node + uses: actions/setup-node@v6 + with: + node-version: 24 # Needed for npm@11 for Trusted Publishing + cache: "pnpm" + + - name: ๐Ÿ“ฅ Install deps + run: pnpm install --frozen-lockfile + + - name: โคด๏ธ Update version + run: | + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + pnpm run experimental:version + git push origin --tags + + - name: ๐Ÿ— Build + run: pnpm build + + - name: ๐Ÿš€ Publish + run: pnpm run experimental:publish diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 738b293c85..c822815056 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -135,7 +135,7 @@ This table gives a high-level overview of the stages, but please see the individ - A proposal enters **Stage 3 โ€” Beta** once it receives **Stage 2 โ€” Alpha** PR approvals from 2 SC members and is merged to `dev` - An SC member authoring the `unstable_` PR counts as an implicit approval, so in those cases explicit approval is required from 1 additional SC member -- This will include the feature in `nightly` releases and the next normal SemVer release for broader beta testing under the `unstable_` flag +- This will include the feature in the next normal SemVer release for broader beta testing behind the `unstable_` flag ### Stage 4 โ€” Stabilization @@ -151,7 +151,7 @@ This table gives a high-level overview of the stages, but please see the individ - A proposal enters **Stage 5 โ€” Stable** once it receives **Stage 4 โ€” Stabilization** PR approvals from at least 50% of the SC members and is merged to `dev` - An SC member authoring the stabilization PR counts as an implicit approval -- This will include the stable feature in `nightly` releases and the next normal SemVer release +- This will include the stable feature in the next normal SemVer release ## Meeting Notes diff --git a/package.json b/package.json index c52c6ee3e6..d61af2ba8b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "docs": "pnpm run docs:typedoc && pnpm run docs:jsdoc", "docs:typedoc": "typedoc", "docs:jsdoc": "node --experimental-strip-types scripts/docs.ts", + "experimental:version": "node ./scripts/experimental.ts version", + "experimental:publish": "node ./scripts/experimental.ts publish", "format": "prettier --ignore-path .prettierignore --write .", "format:check": "prettier --ignore-path .prettierignore --check .", "lint": "eslint --cache .", diff --git a/scripts/experimental.ts b/scripts/experimental.ts new file mode 100644 index 0000000000..5ef44fed42 --- /dev/null +++ b/scripts/experimental.ts @@ -0,0 +1,151 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { colorize, colors } from "./utils/color.ts"; +import { logAndExec } from "./utils/process.ts"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, ".."); + +const packageDirNames = fs + .readdirSync("packages") + .filter((name) => fs.statSync(`packages/${name}`).isDirectory()); + +const command = process.argv[2]; +const args = process.argv.slice(3); +const dryRun = args.includes("--dry-run"); + +if (command === "version") { + await bumpVersion(); +} else if (command === "publish") { + await publishPackages(); +} else { + console.error( + `Usage: node scripts/experimental.ts [version | publish] [--dry-run] `, + ); + process.exit(1); +} + +async function bumpVersion() { + if (!dryRun) { + let status = logAndExec("git status --porcelain", true).trim(); + let lines = status.split("\n"); + invariant( + lines.every((line) => line === "" || line.startsWith("?")), + "Working directory is not clean. Please commit or stash your changes.", + ); + } + + let sha = logAndExec("git rev-parse --short HEAD", true); + invariant(sha != null, "Failed to get git SHA"); + let version = `0.0.0-experimental-${sha}`; + if (dryRun) { + console.log( + colorize( + ` [Dry Run] Would create and switch to branch experimental/${version}\n`, + colors.yellow, + ), + ); + } else { + logAndExec(`git checkout -b experimental/${version}`); + } + + for (let packageDirName of packageDirNames) { + if (dryRun) { + console.log( + colorize( + ` [Dry Run] Would update ${packageDirName} to version ${version}`, + colors.yellow, + ), + ); + } else { + let packageName = updatePackageJson(packageDirName, (pkg) => { + pkg["version"] = version; + }); + console.log( + colorize( + ` Updated ${packageName} to version ${version}`, + colors.green, + ), + ); + } + } + + if (!dryRun) { + logAndExec(`git commit -am "Version ${version}"`); + logAndExec(`git tag -am "Version ${version}" v${version}`); + console.log( + colorize(` Committed and tagged version ${version}`, colors.green), + ); + } +} + +async function publishPackages() { + // Ensure we are in CI. We don't do this manually without --dry-run + invariant( + dryRun || process.env.CI, + `You should always run the publish script from the CI environment!`, + ); + + // Get the current tag, which has the release version number + let version = logAndExec("git tag --list --points-at HEAD", true) + .split("\n") + .find((t) => t.includes("experimental")) + ?.replace(/^v/, ""); + + invariant(version, "Missing experimental version"); + + // Ensure build versions match the release version + for (let packageDirName of packageDirNames) { + let pkgVersion = readPackageJson(packageDirName).version; + invariant( + pkgVersion === version, + `Package ${packageDirName} is on version ${pkgVersion}, but should be on ${version}`, + ); + } + + // 4. Publish to npm + let tag = "experimental"; + let publishCommand = `pnpm publish --recursive --filter "./packages/*" --access public --tag ${tag} --no-git-checks --report-summary`; + if (dryRun) { + console.log( + colorize( + ` [Dry Run] Would publish version ${version} to npm with tag "${tag}" via command:\n` + + ` ${publishCommand}`, + colors.yellow, + ), + ); + } else { + console.log(` Publishing version ${version} to npm with tag "${tag}"`); + logAndExec(publishCommand); + console.log(` Publishing completed`); + } +} + +// --- Utilities --- + +function invariant(cond: unknown, message: string): asserts cond { + if (!cond) throw new Error(message); +} + +function readPackageJson(packageDirName: string): Record { + let file = path.join(rootDir, "packages", packageDirName, "package.json"); + let raw: unknown = JSON.parse(fs.readFileSync(file, "utf-8")); + invariant( + typeof raw === "object" && raw !== null, + `Invalid package.json at ${file}`, + ); + return raw as Record; +} + +function updatePackageJson( + packageDirName: string, + transform: (pkg: Record) => void, +): string | undefined { + let file = path.join(rootDir, "packages", packageDirName, "package.json"); + let pkg = readPackageJson(packageDirName); + transform(pkg); + fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n"); + let name = pkg["name"]; + return typeof name === "string" ? name : undefined; +} diff --git a/scripts/publish.js b/scripts/publish.js deleted file mode 100644 index 40aa7c75db..0000000000 --- a/scripts/publish.js +++ /dev/null @@ -1,154 +0,0 @@ -const path = require("path"); -const { execSync } = require("child_process"); - -const jsonfile = require("jsonfile"); -const semver = require("semver"); - -const rootDir = path.resolve(__dirname, ".."); - -/** - * @param {*} cond - * @param {string} message - * @returns {asserts cond} - */ -function invariant(cond, message) { - if (!cond) throw new Error(message); -} - -/** - * @returns {string} - */ -function getTaggedVersion() { - let output = execSync("git tag --list --points-at HEAD").toString(); - return output.replace(/^v|\n+$/g, ""); -} - -/** - * @param {string} packageName - * @param {string|number} version - */ -async function ensureBuildVersion(packageName, version) { - let file = path.join(rootDir, "packages", packageName, "package.json"); - let json = await jsonfile.readFile(file); - invariant( - json.version === version, - `Package ${packageName} is on version ${json.version}, but should be on ${version}`, - ); -} - -/** - * @param {string} packageName - * @param {string} tag - */ -function publishBuild(packageName, tag, releaseBranch) { - let buildDir = path.join(rootDir, "packages", packageName); - - let args = ["--access public"]; - if (tag) { - args.push(`--tag ${tag}`); - } - - if (tag === "experimental" || tag === "nightly") { - args.push("--no-git-checks"); - } else if (releaseBranch) { - args.push(`--publish-branch ${releaseBranch}`); - } else { - throw new Error( - "Expected a release branch name to be provided for non-experimental/nightly releases", - ); - } - console.log(); - console.log(` pnpm publish ${buildDir} --tag ${tag} --access public`); - console.log(); - execSync(`pnpm publish ${buildDir} ${args.join(" ")}`, { - stdio: "inherit", - }); -} - -/** - * @returns {Promise<1 | 0>} - */ -async function run() { - try { - // 0. Ensure we are in CI. We don't do this manually - invariant( - process.env.CI, - `You should always run the publish script from the CI environment!`, - ); - - // 1. Get the current tag, which has the release version number - let version = getTaggedVersion(); - invariant( - version !== "", - "Missing release version. Run the version script first.", - ); - - // 2. Determine the appropriate npm tag to use - let releaseBranch; - let tag; - if (version.includes("experimental")) { - tag = "experimental"; - } else if (version.includes("nightly")) { - tag = "nightly"; - } else if (version.startsWith("6.")) { - // !!! Note: publish.js is not used for prereleases and stable releases. - // We should be using the Changesets CI process for those. - // These code paths are only left here for emergency usages - releaseBranch = "release-v6"; - tag = null; - } else if (version.startsWith("7.")) { - // !!! Note: publish.js is not used for prereleases and stable releases. - // We should be using the Changesets CI process for those. - // These code paths are only left here for emergency usages - releaseBranch = "release-next"; - tag = semver.prerelease(version) == null ? "latest" : "pre"; - } - - console.log(); - console.log(` Publishing version ${version} to npm with tag "${tag}"`); - - // 3. Ensure build versions match the release version - await ensureBuildVersion("react-router", version); - await ensureBuildVersion("react-router-dom", version); - await ensureBuildVersion("react-router-dev", version); - await ensureBuildVersion("react-router-express", version); - await ensureBuildVersion("react-router-node", version); - await ensureBuildVersion("react-router-serve", version); - await ensureBuildVersion("react-router-architect", version); - await ensureBuildVersion("react-router-cloudflare", version); - await ensureBuildVersion("react-router-fs-routes", version); - await ensureBuildVersion( - "react-router-remix-routes-option-adapter", - version, - ); - await ensureBuildVersion("create-react-router", version); - - // 4. Publish to npm - publishBuild("react-router", tag, releaseBranch); - publishBuild("react-router-dom", tag, releaseBranch); - publishBuild("react-router-dev", tag, releaseBranch); - publishBuild("react-router-express", tag, releaseBranch); - publishBuild("react-router-node", tag, releaseBranch); - publishBuild("react-router-serve", tag, releaseBranch); - publishBuild("react-router-architect", tag, releaseBranch); - publishBuild("react-router-cloudflare", tag, releaseBranch); - publishBuild("react-router-fs-routes", tag, releaseBranch); - publishBuild( - "react-router-remix-routes-option-adapter", - tag, - releaseBranch, - ); - publishBuild("create-react-router", tag, releaseBranch); - } catch (error) { - console.log(); - console.error(` ${error.message}`); - console.log(); - return 1; - } - - return 0; -} - -run().then((code) => { - process.exit(code); -}); diff --git a/scripts/version.js b/scripts/version.js deleted file mode 100644 index 6fc21a9a94..0000000000 --- a/scripts/version.js +++ /dev/null @@ -1,64 +0,0 @@ -const fs = require("node:fs"); -const { execSync } = require("child_process"); -const pc = require("picocolors"); -const semver = require("semver"); - -const { - ensureCleanWorkingDirectory, - invariant, - updatePackageConfig, -} = require("./utils"); - -async function run() { - try { - let args = process.argv.slice(2); - let skipGit = args.includes("--skip-git"); - - let givenVersion = args[0]; - invariant( - givenVersion != null, - `Missing next version. Usage: node version.js [nextVersion]`, - ); - - // 0. Make sure the working directory is clean - if (!skipGit) { - ensureCleanWorkingDirectory(); - } - - // 1. Get the next version number - let version = semver.valid(givenVersion); - invariant(version != null, `Invalid version specifier: ${givenVersion}`); - - // 2. Bump package versions - let packageDirNamesToBump = fs - .readdirSync("packages") - .filter((name) => fs.statSync(`packages/${name}`).isDirectory()); - - for (let packageDirName of packageDirNamesToBump) { - let packageName; - await updatePackageConfig(packageDirName, (pkg) => { - packageName = pkg.name; - pkg.version = version; - }); - console.log(pc.green(` Updated ${packageName} to version ${version}`)); - } - - // 3. Commit and tag - if (!skipGit) { - execSync(`git commit --all --message="Version ${version}"`); - execSync(`git tag -a -m "Version ${version}" v${version}`); - console.log(pc.green(` Committed and tagged version ${version}`)); - } - } catch (error) { - console.log(); - console.error(pc.red(` ${error.message}`)); - console.log(); - return 1; - } - - return 0; -} - -run().then((code) => { - process.exit(code); -}); From a819b524656de13a586a4730add73dfba4442fcd Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 14 Apr 2026 15:53:23 -0400 Subject: [PATCH 7/9] Remove filter and shorten date range --- scripts/changes/delete-changeset-bot-comments.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/changes/delete-changeset-bot-comments.ts b/scripts/changes/delete-changeset-bot-comments.ts index f4bec0a463..4a94d7a009 100644 --- a/scripts/changes/delete-changeset-bot-comments.ts +++ b/scripts/changes/delete-changeset-bot-comments.ts @@ -15,7 +15,7 @@ import { } from "../utils/github.ts"; const CHANGESET_BOT = "changeset-bot[bot]"; -const CUTOFF = new Date(2026, 0, 1); +const CUTOFF = new Date(2026, 4, 1); const ADD_CHANGE_FILE = "๐Ÿ‘‹ We've moved away from Changesets to our own internal " + @@ -42,8 +42,6 @@ console.log( let prs = await listOpenPrs({ createdAfter: CUTOFF, base: "dev", - // TODO: For testing - author: "brophdawg11", }); console.log(`Found ${prs.length} open PR${prs.length === 1 ? "" : "s"}.\n`); From 56678dbee7fa1d324f3ee04a8d2637307a155562 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 14 Apr 2026 15:54:12 -0400 Subject: [PATCH 8/9] off by one --- scripts/changes/delete-changeset-bot-comments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/changes/delete-changeset-bot-comments.ts b/scripts/changes/delete-changeset-bot-comments.ts index 4a94d7a009..5efe93ee66 100644 --- a/scripts/changes/delete-changeset-bot-comments.ts +++ b/scripts/changes/delete-changeset-bot-comments.ts @@ -15,7 +15,7 @@ import { } from "../utils/github.ts"; const CHANGESET_BOT = "changeset-bot[bot]"; -const CUTOFF = new Date(2026, 4, 1); +const CUTOFF = new Date(2026, 3, 1); const ADD_CHANGE_FILE = "๐Ÿ‘‹ We've moved away from Changesets to our own internal " + From 45554ad37c959ca9f816dd46dbc7a0f2a390a6a9 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 14 Apr 2026 15:56:04 -0400 Subject: [PATCH 9/9] Revert back to 2026 date range --- scripts/changes/delete-changeset-bot-comments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/changes/delete-changeset-bot-comments.ts b/scripts/changes/delete-changeset-bot-comments.ts index 5efe93ee66..29d931e4d6 100644 --- a/scripts/changes/delete-changeset-bot-comments.ts +++ b/scripts/changes/delete-changeset-bot-comments.ts @@ -15,7 +15,7 @@ import { } from "../utils/github.ts"; const CHANGESET_BOT = "changeset-bot[bot]"; -const CUTOFF = new Date(2026, 3, 1); +const CUTOFF = new Date(2026, 0, 1); const ADD_CHANGE_FILE = "๐Ÿ‘‹ We've moved away from Changesets to our own internal " +