Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .agents/skills/prepare-release-notes/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
name: prepare-release-notes
description: Prepare React Router release notes before running the changes/versioning scripts. Use when asked to review, polish, normalize, or prepare pending change files under packages/*/.changes, remove semantic commit prefixes from release bullets, enforce imperative tense, decide whether a manual scripts/changes/whats-changed.md section is warranted, or draft long-form release notes for new features, stable future flags, or unstable flags.
---

# Prepare Release Notes

Polish pending React Router change files and add manual release notes only when the release needs narrative context beyond the generated change lists.

## Workflow

1. Inspect local state:

```sh
git status --short
find packages -path '*/.changes/*.md' -not -name README.md -not -name .gitkeep -print | sort
```

2. Read every pending change file. Do not edit generated changelogs or released notes directly.

3. Normalize each change file:
- Remove `feat:`, `feat(...)`, `fix:`, and `fix(...)` semantic-commit prefixes from prose
- Use present or imperative tense for the first line and top-level release bullets: prefer `Add`, `Fix`, `Remove`, `Support`, `Stabilize`, `Preserve`, `Update`, `Avoid`, `Prevent`, `Throw`, `Warn`, `Expose`
- Nested detail bullets can stay explanatory when they expand on the parent bullet; do not rewrite them solely to force present or imperative tense
- Remove terminal sentence periods from bullet items because release generation appends PR/commit links after the first bullet line
- Remove terminal sentence periods from nested bullet items too, unless the punctuation is part of code, a URL, an abbreviation, a version number, or another token where removing it would be wrong
- If one bullet contains multiple sentences, split it into a shorter parent bullet plus nested bullet items
- Keep the first line concise and user-facing; use nested bullets for details or migration notes

4. Review whether `scripts/changes/whats-changed.md` is needed:
- Read `CHANGELOG.md` examples or `references/whats-changed.md` when uncertain
- Add `scripts/changes/whats-changed.md` only for features, future flag stabilizations, unstable flags, migration guidance, breaking bug fixes, or complex behavior that needs long-form text or examples
- Do not add it for ordinary bug fixes, dependency cleanup, internal refactors, or release bullets that are already clear
- If adding it, write the body only; the release script adds `### What's Changed` when missing

5. Validate:

```sh
pnpm changes:validate
pnpm changes:preview
```

Use `changes:preview` to inspect the generated root release notes and confirm the PR/commit link placement, section ordering, and any manual What's Changed placement. If dependencies are missing or the command is too expensive for the context, state what was skipped.

## Change File Style

Single-line entries should read well with an auto-appended PR link:

```markdown
Fix `href()` to stringify and URL-encode param values like `generatePath()`
```

Use nested bullets for additional sentences:

```markdown
Fix route ranking for dynamic parameters with static extension suffixes

- Identify `/:name.xml` as a dynamic segment instead of a static segment
- Preserve static route priority for paths like `/sitemap.xml`
```

Avoid semantic commit prefixes:

```markdown
Add support for nub as a package manager
```

not:

```markdown
feat: add support for nub as a package manager.
```

## What's Changed Guidance

Use `scripts/changes/whats-changed.md` for release-level narrative, not package-specific bullets. Good candidates include:

- A new user-facing API or feature that benefits from example code
- Stabilization or renaming of unstable APIs/flags, especially when adopters must migrate
- A stable future flag that changes behavior and needs adoption guidance
- A breaking bug fix or adapter/runtime behavior change that may require deployment checks
- A cluster of related changes whose combined effect matters more than the individual bullet list

Keep the tone direct and practical. Prefer headings under the generated `### What's Changed` section:

````markdown
#### Feature Name

Explain what changed, who it affects, and how to adopt it.

```ts
// Optional short example
```
````

Do not duplicate every bullet from Minor/Patch/Unstable Changes. Let generated change files carry ordinary PR-level details.

See `references/whats-changed.md` for examples distilled from the existing changelog.
4 changes: 4 additions & 0 deletions .agents/skills/prepare-release-notes/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Prepare Release Notes"
short_description: "Polish React Router release notes"
default_prompt: "Use $prepare-release-notes to review pending React Router change files and draft any needed What's Changed notes."
35 changes: 35 additions & 0 deletions .agents/skills/prepare-release-notes/references/whats-changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# What's Changed Reference

Load this when deciding whether a React Router release needs `scripts/changes/whats-changed.md`.

## Include A Manual Section For

- Major releases with baseline changes, removed packages, removed deprecated APIs, new runtime requirements, or migration guidance
- Stabilizations and flag renames where adopters of unstable APIs need explicit before/after guidance
- New APIs or flags that deserve a narrative introduction or usage example
- Cross-cutting performance or behavior work where several bullets form one user-facing story
- Breaking bug fixes, deployment-sensitive fixes, or adapter/runtime behavior changes where users may need to test or update config

## Usually Do Not Include One For

- Straightforward bug fixes
- Dependency removals or upgrades where the bullet is sufficient
- Internal refactors with no public API change
- A minor feature that is fully understandable from one concise bullet and nested detail bullets
- Releases where the only content is a normal patch list

## Existing Changelog Patterns

- `v8.1.0`: Uses headings for agent skill installation and observability metadata, including an instrumentation code example. The generated minor bullets still carry the per-package details.
- `v8.0.0`: Uses long-form migration notes for a major release: baseline support, adopted future flags, removed packages/APIs, and behavior changes.
- `v7.18.0`: Explains a CSRF check fix that may be a breaking bug fix for reverse-proxy deployments and tells users what to test.
- `v7.15.0`: Groups several unstable-to-stable API renames and route matching optimizations into narrative sections before listing individual minor bullets.
- `v7.15.1`: Uses a "What's New" style section for an unstable hook with a code example. For new release notes, prefer the current `What's Changed` file path; the generated heading will be `### What's Changed`.

## Drafting Tips

- Use `####` headings inside `scripts/changes/whats-changed.md`; the release script wraps the file in `### What's Changed` if needed
- Mention applicable React Router modes when the distinction matters: Declarative, Data, Framework, RSC Data, or RSC Framework
- Keep examples short and directly tied to adoption
- Avoid repeating the package prefix and PR-link details already provided by generated change sections
- If unsure, run `pnpm changes:preview` with and without the manual section and keep it only if the generated notes are meaningfully clearer with the narrative
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ We manage release notes in this file instead of the paginated Github Releases Pa
<summary>Table of Contents</summary>

- [React Router Releases](#react-router-releases)
- [v8.2.0](#v820)
- [Web Streams Default Server Entry](#web-streams-default-server-entry)
- [v8.1.0](#v810)
- [Agent Skills Installation via `create-react-router`](#agent-skills-installation-via-create-react-router)
- [Observability Metadata](#observability-metadata)
Expand Down Expand Up @@ -107,6 +109,65 @@ We manage release notes in this file instead of the paginated Github Releases Pa

</details>

## v8.2.0

Date: 2026-07-08

### What's Changed

#### Web Streams Default Server Entry

Non-Node runtime Framework Mode apps no longer need a custom `entry.server.tsx` file using React's `renderToReadableStream` API. Apps with `@react-router/{node,express,serve}` dependencies will continue to default to `renderToPipeableStream`, while non-Node apps default to `renderToReadableStream`.

Because Web Streams are stable in Node 22+, Node apps can also opt-into the Web Streams default entry with the new `future.unstable_enableNodeReadableStream` flag:

```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
future: {
unstable_enableNodeReadableStream: true,
},
} satisfies Config;
```

This flag has no effect if you have a custom `entry.server.tsx` keep using their custom entry file. It only applies to the default entry used if one doesn't exist.

Node apps opting-into the Web Streams API _might_ even see a small performance boost because React Router already uses Web Streams internally, so this avoids additional conversions between Web/Node streams. If you see perf changes one way or another upon adopting this flag, please let us know!

### Minor Changes

- `@react-router/dev` - Add a Web Streams default server entry for non-Node Framework mode apps ([#15290](https://github.com/remix-run/react-router/pull/15290))
- Apps using `@react-router/node`, `@react-router/express`, or `@react-router/serve` continue to use the `renderToPipeableStream` default server entry
- Apps without those Node server adapter dependencies use a `renderToReadableStream` default server entry
- Non-Node apps with their own `entry.server.tsx` may be able to remove it in favor of the default if it is not doing anything custom
- `@react-router/dev` - Detect `nub` as a supported package manager when installing framework dependencies ([#15276](https://github.com/remix-run/react-router/pull/15276))
- `create-react-router` - Detect `nub` as a supported package manager when creating new projects ([#15276](https://github.com/remix-run/react-router/pull/15276))

### Patch Changes

- `react-router` - Fix `href()` to properly stringify and URL-encode param values, matching `generatePath()` ([#15277](https://github.com/remix-run/react-router/pull/15277))
- splat params preserve path separators while encoding each segment individually
- `react-router` - Fix dynamic param extraction for routes with optional static segments ([#15200](https://github.com/remix-run/react-router/pull/15200))
- When a route path contains optional static segments (e.g. `/school?/user/:id`), the internal regex's incorrectly shifted parameter indices resulting in incorrect parameter extraction
- Consecutive optional static segments (e.g. `/one?/two?`) were only partially handled
- `react-router` - Preserve navigation blocker state through a revalidation ([#15246](https://github.com/remix-run/react-router/pull/15246))
- `react-router` - Fix route ranking for dynamic parameters with static extension suffixes ([#15273](https://github.com/remix-run/react-router/pull/15273))
- These were not being detected as dynamic param segments and instead got incorrectly scored higher as a static segment
- This meant they could potentially tie truly static routes like `/sitemap.xml` and outrank them based on definition order
- These are now correctly identified as dynamic parameter segments and scored correctly
- `react-router` - Use ReactFormState types instead of unknown ([#15263](https://github.com/remix-run/react-router/pull/15263))
- `@react-router/dev` - Detect user `rolldownOptions` config in Vite 8+ ([#15278](https://github.com/remix-run/react-router/pull/15278))

### Unstable Changes

⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_

- `@react-router/dev` - Add the [`future.unstable_enableNodeReadableStream`](https://reactrouter.com/upgrading/future#futureunstable_enablenodereadablestream) flag to opt Node Framework mode apps into using `renderToReadableStream` instead of `renderToPipeableStream` ([#15290](https://github.com/remix-run/react-router/pull/15290))
- This flag has no effect if you have your own `entry.server.tsx`

**Full Changelog**: [`v8.1.0...v8.2.0`](https://github.com/remix-run/react-router/compare/react-router@8.1.0...react-router@8.2.0)

## v8.1.0

Date: 2026-06-29
Expand Down
33 changes: 33 additions & 0 deletions docs/upgrading/future.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,39 @@ _No known planned breaking changes yet_

We document some [unstable] flags here as a reference for folks contributing to the project via beta testing, but they are not generally recommended for production use and may have breaking changes in patch or minor releases - adopt with caution!

### `future.unstable_enableNodeReadableStream`

[MODES: framework]

<br/>
<br/>

**Background**

Now that the Web Streams API is [stable](https://nodejs.org/docs/latest-v22.x/api/webstreams.html) in Node 22+, it's viable for React Router to use React's [`renderToReadableStream`](https://react.dev/reference/react-dom/server/renderToReadableStream) in the server entry.

When no `entry.server.tsx` file is present, React Router defaults to [`renderToPipeableStream`](https://react.dev/reference/react-dom/server/renderToPipeableStream) when a Node runtime is detected, and `renderToReadableStream` otherwise.

With this flag enabled, React Router will default to `renderToReadableStream` on all runtimes, including Node. You can continue to use `renderToPipeableStream` via a custom `entry.server.tsx` file if needed.

<docs-info>Enabling this flag might even provide slight performance gains because we are already using Web Streams internally, so this flag removes some unnecessary transforms between Web and Node streams.</docs-info>

👉 **Enable the Flag**

```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
future: {
unstable_enableNodeReadableStream: true,
},
} satisfies Config;
```

**Update your Code**

No code changes are required. If your app has a custom `entry.server.tsx`, this flag will not change your runtime behavior.

### `future.unstable_optimizeDeps`

[MODES: framework]
Expand Down
62 changes: 61 additions & 1 deletion integration/cli-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
Expand All @@ -15,7 +16,7 @@ import { expect, test } from "@playwright/test";
import dedent from "dedent";
import semver from "semver";

import { createProject } from "./helpers/vite";
import { build, createProject, reactRouterConfig } from "./helpers/vite";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDirectory = path.resolve(__dirname, "..");
Expand Down Expand Up @@ -198,6 +199,51 @@ test.describe("cli", () => {

expect(existsSync(entryServerFile)).toBeTruthy();
expect(existsSync(entryClientFile)).toBeTruthy();
expect(readFileSync(entryServerFile, "utf-8")).toContain(
"renderToPipeableStream",
);
});

test("generates a web server entry for non-Node projects", async () => {
const cwd = await createProject();
let packageJsonPath = path.join(cwd, "package.json");
let pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
delete pkg.dependencies["@react-router/express"];
delete pkg.dependencies["@react-router/node"];
delete pkg.dependencies["@react-router/serve"];
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));

let entryServerFile = path.join(cwd, "app", "entry.server.tsx");

expect(existsSync(entryServerFile)).toBeFalsy();

run(["reveal", "entry.server"], { cwd });

expect(existsSync(entryServerFile)).toBeTruthy();
expect(readFileSync(entryServerFile, "utf-8")).toContain(
"renderToReadableStream",
);
});

test("generates a web server entry for Node projects with the readable stream future flag", async () => {
const cwd = await createProject({
"react-router.config.ts": reactRouterConfig({
future: {
unstable_enableNodeReadableStream: true,
},
}),
});

let entryServerFile = path.join(cwd, "app", "entry.server.tsx");

expect(existsSync(entryServerFile)).toBeFalsy();

run(["reveal", "entry.server"], { cwd });

expect(existsSync(entryServerFile)).toBeTruthy();
expect(readFileSync(entryServerFile, "utf-8")).toContain(
"renderToReadableStream",
);
});

test("rsc generates entry.{ssr,rsc,client}.tsx in the app directory", async () => {
Expand Down Expand Up @@ -250,4 +296,18 @@ test.describe("cli", () => {
expect(existsSync(entryClientFile)).toBeTruthy();
});
});

test("builds a Node project with the readable stream future flag and default server entry", async () => {
const cwd = await createProject({
"react-router.config.ts": reactRouterConfig({
future: {
unstable_enableNodeReadableStream: true,
},
}),
});

const buildResult = build({ cwd });

expect(buildResult.status).toBe(0);
});
});
20 changes: 19 additions & 1 deletion integration/vite-plugin-cloudflare-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { expect } from "@playwright/test";
import dedent from "dedent";
import getPort from "get-port";

import { type Files, test, viteConfig } from "./helpers/vite.js";
import {
build,
createProject,
type Files,
test,
viteConfig,
} from "./helpers/vite.js";

const tsx = dedent;
const css = dedent;
Expand Down Expand Up @@ -177,4 +183,16 @@ test.describe("vite-plugin-cloudflare", () => {
"20px",
);
});

test("builds project with default server entry", async () => {
const files = defineFiles();
const cwd = await createProject(
await files({ port: 0 }),
"vite-plugin-cloudflare-template",
);

const buildResult = build({ cwd });

expect(buildResult.status).toBe(0);
});
});
1 change: 1 addition & 0 deletions integration/vite-presets-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ test.describe("Vite / presets", async () => {

// Ensure future flags from presets are properly merged
expect(buildEndArgsMeta.futureFlags).toEqual({
unstable_enableNodeReadableStream: false,
unstable_optimizeDeps: true,
});
expect(buildEndArgsMeta.splitRouteModules).toBe(true);
Expand Down

This file was deleted.

6 changes: 6 additions & 0 deletions packages/create-react-router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# `create-react-router`

## v8.2.0

### Minor Changes

- Detect `nub` as a supported package manager when creating new projects ([#15276](https://github.com/remix-run/react-router/pull/15276))

## v8.1.0

### Minor Changes
Expand Down
Loading