-
Notifications
You must be signed in to change notification settings - Fork 0
chore: release 0.0.1 beta.1 #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 9 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
dcfa828
chore: refactor static example
SyMind 6fc4d4c
feat: generate static resource
SyMind fd04418
pnpm run check
SyMind 27fd693
update readme
SyMind 0768c13
fix: dependencies
SyMind 9fa7183
fix: import in windows
SyMind 92b93ac
fix: code style
SyMind efcce91
merge origin main
SyMind 9b3ec2f
chore: release 1.0.0-beta.0
SyMind 33c3a1e
fix: release ci
SyMind 350e69f
fix: release ci
SyMind File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import path from 'node:path'; | ||
| import { type Build, type Dev, expect, test } from '@e2e/helper'; | ||
| import type { Page } from 'playwright'; | ||
|
|
||
| const PROJECT_DIR = path.resolve( | ||
| import.meta.dirname, | ||
| '../../../examples/static', | ||
| ); | ||
|
|
||
| const setup = async (dev: Dev, build: Build, page: Page) => { | ||
| const rsbuild = | ||
| process.env.TEST_MODE === 'dev' | ||
| ? await dev({ cwd: PROJECT_DIR }) | ||
| : await build({ cwd: PROJECT_DIR, preview: true }); | ||
|
|
||
| await page.goto(`http://localhost:${rsbuild.port}`); | ||
| return rsbuild; | ||
| }; | ||
|
|
||
| test('should load the page and display the title', async ({ | ||
| page, | ||
| dev, | ||
| build, | ||
| }) => { | ||
| await setup(dev, build, page); | ||
|
|
||
| await expect(page).toHaveTitle('Static RSC'); | ||
|
|
||
| const heading = page.locator('h1'); | ||
| await expect(heading).toBeVisible(); | ||
| await expect(heading).toHaveText('This is an RSC!'); | ||
|
|
||
| const links = page.locator('link[rel="stylesheet"]'); | ||
| await expect(links).toHaveCount(1); | ||
| }); | ||
|
|
||
| test('should display and interact with Counter component', async ({ | ||
| page, | ||
| dev, | ||
| build, | ||
| }) => { | ||
| await setup(dev, build, page); | ||
|
|
||
| const counterButton = page.locator('button:has-text("Count:")'); | ||
| await expect(counterButton).toBeVisible(); | ||
| await expect(counterButton).toHaveText('Count: 0'); | ||
|
|
||
| await counterButton.click(); | ||
| await expect(counterButton).toHaveText('Count: 1'); | ||
|
|
||
| await counterButton.click(); | ||
| await expect(counterButton).toHaveText('Count: 2'); | ||
|
|
||
| await counterButton.click(); | ||
| await expect(counterButton).toHaveText('Count: 3'); | ||
| }); | ||
|
|
||
| test('should navigate to Other page via client-side navigation', async ({ | ||
| page, | ||
| dev, | ||
| build, | ||
| }) => { | ||
| await setup(dev, build, page); | ||
|
|
||
| // Click the "Other" nav link | ||
| const otherLink = page.locator('nav a:has-text("Other")'); | ||
| await expect(otherLink).toBeVisible(); | ||
| await otherLink.click(); | ||
|
|
||
| // Verify heading changes | ||
| const heading = page.locator('h1'); | ||
| await expect(heading).toHaveText('This is another RSC!'); | ||
|
|
||
| // Verify URL changed | ||
| await expect(page).toHaveURL(/\/other$/); | ||
| }); | ||
|
|
||
| test('should apply CSS styles to current nav link', async ({ | ||
| page, | ||
| dev, | ||
| build, | ||
| }) => { | ||
| await setup(dev, build, page); | ||
|
|
||
| const currentLink = page.locator('nav a[aria-current="page"]'); | ||
| await expect(currentLink).toBeVisible(); | ||
|
|
||
| const backgroundColor = await currentLink.evaluate((el) => | ||
| window.getComputedStyle(el).getPropertyValue('background-color'), | ||
| ); | ||
| expect(backgroundColor).toBe('rgb(0, 0, 0)'); | ||
|
|
||
| const color = await currentLink.evaluate((el) => | ||
| window.getComputedStyle(el).getPropertyValue('color'), | ||
| ); | ||
| expect(color).toBe('rgb(255, 255, 255)'); | ||
| }); | ||
|
|
||
| test('should reset counter state on navigation', async ({ | ||
| page, | ||
| dev, | ||
| build, | ||
| }) => { | ||
| await setup(dev, build, page); | ||
|
|
||
| // Increment counter | ||
| const counterButton = page.locator('button:has-text("Count:")'); | ||
| await counterButton.click(); | ||
| await counterButton.click(); | ||
| await expect(counterButton).toHaveText('Count: 2'); | ||
|
|
||
| // Navigate to Other page | ||
| const otherLink = page.locator('nav a:has-text("Other")'); | ||
| await otherLink.click(); | ||
| await expect(page.locator('h1')).toHaveText('This is another RSC!'); | ||
|
|
||
| // Navigate back to Index page | ||
| const indexLink = page.locator('nav a:has-text("Index")'); | ||
| await indexLink.click(); | ||
| await expect(page.locator('h1')).toHaveText('This is an RSC!'); | ||
|
|
||
| // Counter should be reset | ||
| const resetCounter = page.locator('button:has-text("Count:")'); | ||
| await expect(resetCounter).toHaveText('Count: 0'); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,184 @@ | ||
| # Parcel RSC Static Site Generator Example | ||
| # Rsbuild RSC Static Rendering Example | ||
|
|
||
| This example is a simple static site generator built with Parcel and React Server Components. | ||
| This example demonstrates a multi-page static site built with Rsbuild and React Server Components (RSC). It showcases file-based page routing, static site generation (SSG), client-side hydration, and client-side navigation between pages — all powered by `rsbuild-plugin-rsc`. | ||
|
|
||
| Note: the plugins used in this example will move into Parcel eventually. | ||
| ## Getting Started | ||
|
|
||
| ## Setup | ||
| ```bash | ||
| # Start development server | ||
| pnpm dev | ||
|
|
||
| ### pages/*.tsx | ||
| # Build for production (includes static HTML generation) | ||
| pnpm build | ||
|
|
||
| These are the entry points of the build. They are React Server Components that render the root `<html>` element of the page, and any other client or server components. A Parcel packager plugin executes the server components during the build to render them to static HTML. A separate `.rsc` file for each page is also generated for use during client side navigations. | ||
| # Preview the production build | ||
| pnpm preview | ||
| ``` | ||
|
|
||
| ### components/client.tsx | ||
| ## Project Structure | ||
|
|
||
| This is the main client entrypoint, imported from each page. It uses the Parcel-specific `"use client-entry"` directive to mark that it should only run on the client, and not on the server (even during SSR). The client is responsible for hydrating the initial page, and intercepting link clicks and navigations to perform client side routing. | ||
| ``` | ||
| src/ | ||
| ├── pages/ # Page components (file-based routing) | ||
| │ ├── Index.tsx | ||
| │ └── Other.tsx | ||
| ├── components/ # Shared components | ||
| │ ├── Counter.tsx # Client component | ||
| │ ├── Nav.tsx # Server component | ||
| │ └── style.css | ||
| └── framework/ # Framework layer | ||
| ├── entry.rsc.tsx # RSC entrypoint (server) | ||
| ├── entry.ssr.tsx # SSR entrypoint (server) | ||
| ├── entry.client.tsx # Client entrypoint (browser) | ||
| ├── request.tsx # RSC/SSR request routing | ||
| ├── shared.tsx # Shared types | ||
| └── ssg.tsx # Page types | ||
| generate.mjs # Post-build static generation script | ||
| ``` | ||
|
|
||
| See the [client side routing](../server/README.md#client-side-routing) section of the server readme for a description of how this works. One difference is that we fetch statically pre-generated `.rsc` files instead of dynamically generated content from the server. | ||
| ### Build Output | ||
|
|
||
| ### components/Counter.tsx | ||
| After `pnpm build`, the `dist/` directory contains a fully static site: | ||
|
|
||
| This is a client component. | ||
| ``` | ||
| dist/ | ||
| ├── index.html # Pre-rendered HTML for /index | ||
| ├── other.html # Pre-rendered HTML for /other | ||
| ├── index_.rsc # RSC payload for client-side navigation | ||
| ├── other_.rsc # RSC payload for client-side navigation | ||
| ├── static/ | ||
| │ ├── js/ # Client JavaScript bundles | ||
| │ └── css/ # Client CSS bundles | ||
| └── server/ | ||
| └── index.js # Server bundle (used during generation only) | ||
| ``` | ||
|
|
||
| ### components/Nav.tsx | ||
| ### Page Components (`src/pages/*.tsx`) | ||
|
|
||
| This is a server component that renders a list of links to all of the pages on the site. This is passed as a prop to each entry page component by the SSG packager plugin, and then through to this component. | ||
| Each file under `src/pages/` represents a page. Pages are React Server Components that render the full `<html>` document tree using the `"use server-entry"` directive. The file name determines the route (e.g., `Index.tsx` → `/index`, `Other.tsx` → `/other`). | ||
|
|
||
| ```tsx | ||
| 'use server-entry'; | ||
|
|
||
| import { Counter } from '../components/Counter'; | ||
| import { Nav } from '../components/Nav'; | ||
|
|
||
| export default function Index({ pages, currentPage }: PageProps) { | ||
| return ( | ||
| <html lang="en"> | ||
| <head><title>Static RSC</title></head> | ||
| <body> | ||
| <h1>This is an RSC!</h1> | ||
| <Nav pages={pages} currentPage={currentPage} /> | ||
| <Counter /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| Pages are automatically discovered at build time via `import.meta.webpackContext` in `entry.rsc.tsx`, so adding a new `.tsx` file to `src/pages/` is all that's needed to create a new route. | ||
|
|
||
| ### Server Components (`src/components/Nav.tsx`) | ||
|
|
||
| Server components run only on the server. `Nav` renders a list of links for all pages, highlighting the current page via `aria-current`. | ||
|
|
||
| ### Client Components (`src/components/Counter.tsx`) | ||
|
|
||
| Client components use the `"use client"` directive and run in the browser. `Counter` demonstrates interactive state with `useState`. | ||
|
|
||
| ## Framework Layer | ||
|
|
||
| ### RSC Entrypoint (`src/framework/entry.rsc.tsx`) | ||
|
|
||
| The RSC entrypoint runs in the `react-server-components` layer. It: | ||
|
|
||
| 1. Discovers all pages under `src/pages/` using `import.meta.webpackContext` | ||
| 2. Exports `getStaticPaths()` to list all routes for static generation | ||
| 3. Exports `renderStaticPage(route)` and `renderStaticRsc(route)` for SSG | ||
| 4. Provides a dev server handler for development mode | ||
|
|
||
| CSS and JS bootstrap files are automatically resolved via `entryCssFiles` and `entryJsFiles` injected by the RSC plugin on `"use server-entry"` components. | ||
|
|
||
| ### SSR Entrypoint (`src/framework/entry.ssr.tsx`) | ||
|
|
||
| The SSR entrypoint consumes the RSC stream and renders it to an HTML stream using `react-dom/server`. It supports both SSG (via `prerender()`) and runtime SSR (via `renderToReadableStream()`). The RSC payload is injected into the HTML via `rsc-html-stream` for seamless client hydration. | ||
|
|
||
| ### Client Entrypoint (`src/framework/entry.client.tsx`) | ||
|
|
||
| The client entrypoint hydrates the server-rendered HTML using the embedded RSC payload. It also implements a simple client-side router that: | ||
|
|
||
| - Intercepts link clicks to perform client-side navigation | ||
| - Fetches pre-generated RSC payloads for new pages (via `_.rsc` URL convention) | ||
| - Updates the page without a full browser reload | ||
|
|
||
| ## Rsbuild Configuration | ||
|
|
||
| The `rsbuild.config.ts` configures two environments and a static generation plugin: | ||
|
|
||
| - **server**: Builds `entry.rsc.tsx` with the RSC layer, outputs to `dist/server/` | ||
| - **client**: Builds `entry.client.tsx` for browser hydration and navigation | ||
| - **pluginStaticGenerate**: Runs `generate.mjs` after build to produce static HTML and RSC payloads | ||
|
|
||
| ```ts | ||
| import { Layers, pluginRSC } from 'rsbuild-plugin-rsc'; | ||
|
|
||
| export default defineConfig({ | ||
| plugins: [ | ||
| pluginReact(), | ||
| pluginRSC({ | ||
| layers: { | ||
| ssr: path.join(import.meta.dirname, './src/framework/entry.ssr.tsx'), | ||
| }, | ||
| }), | ||
| pluginStaticGenerate(), | ||
| ], | ||
| environments: { | ||
| server: { | ||
| source: { | ||
| entry: { | ||
| index: { | ||
| import: './src/framework/entry.rsc.tsx', | ||
| layer: Layers.rsc, | ||
| }, | ||
| }, | ||
| }, | ||
| output: { | ||
| distPath: { root: 'dist/server' }, | ||
| }, | ||
| }, | ||
| client: { | ||
| source: { | ||
| entry: { | ||
| index: './src/framework/entry.client.tsx', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ## How It Works | ||
|
|
||
| ### Build-Time Static Generation (SSG) | ||
|
|
||
| 1. Rsbuild builds both the server bundle and client assets | ||
| 2. The `pluginStaticGenerate` plugin runs `generate.mjs` after the build | ||
| 3. `generate.mjs` loads the server bundle and calls `getStaticPaths()` to discover all routes | ||
| 4. For each route, it generates: | ||
| - An HTML file with pre-rendered content, embedded RSC payload, and client bootstrap scripts/CSS | ||
| - An RSC payload file (`_.rsc`) for client-side navigation | ||
| 5. The result is a fully static site that can be served by any static file server | ||
|
|
||
| ### Initial Page Load | ||
|
|
||
| 1. Browser requests a URL (e.g., `/index.html`) | ||
| 2. The browser receives pre-rendered HTML with embedded RSC payload | ||
| 3. Client JavaScript hydrates the page using the embedded RSC payload | ||
|
|
||
| ### Client-Side Navigation | ||
|
|
||
| 1. User clicks a link (e.g., from `Index` to `Other`) | ||
| 2. The client router intercepts the click and calls `history.pushState` | ||
| 3. The client fetches the pre-generated RSC payload for the new page (e.g., `/other_.rsc`) | ||
| 4. React updates the page in-place without a full reload |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.