forked from facebook/react
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement react-server-dom-parcel (facebook#31725)
This adds a new `react-server-dom-parcel-package`, which is an RSC integration for the Parcel bundler. It is mostly copied from the existing webpack/turbopack integrations, with some changes to utilize Parcel runtime APIs for loading and executing bundles/modules. See parcel-bundler/parcel#10043 for the Parcel side of this, which includes the plugin needed to generate client and server references. https://github.com/parcel-bundler/rsc-examples also includes examples of various ways to use RSCs with Parcel. Differences from other integrations: * Client and server modules are all part of the same graph, and we use Parcel's [environments](https://parceljs.org/plugin-system/transformer/#the-environment) to distinguish them. The server is the Parcel build entry point, and it imports and renders server components in route handlers. When a `"use client"` directive is seen, the environment changes and Parcel creates a new client bundle for the page, combining all client modules together. CSS from both client and server components are also combined automatically. * There is no separate manifest file that needs to be passed around by the user. A [Runtime](https://parceljs.org/plugin-system/runtime/) plugin injects client and server references as needed into the relevant bundles, and registers server action ids using `react-server-dom-parcel` automatically. * A special `<Resources>` component is also generated by Parcel to render the `<script>` and `<link rel="stylesheet">` elements needed for a page, using the relevant info from the bundle graph. Note: I've already published a 0.0.x version of this package to npm for testing purposes but happy to add whoever needs access to it as well. ### Questions * How to test this in the React repo. I'll have integration tests in Parcel, but setting up all the different mocks and environments to simulate that here seems challenging. I could try to copy how Webpack/Turbopack do it but it's a bit different. * Where to put TypeScript types. Right now I have some ambient types in my [example repo](https://github.com/parcel-bundler/rsc-examples/blob/main/types.d.ts) but it would be nice for users not to copy and paste these. Can I include them in the package or do they need to maintained separately in definitelytyped? I would really prefer not to have to maintain code in three different repos ideally. --------- Co-authored-by: Sebastian Markbage <[email protected]>
- Loading branch information
1 parent
a496498
commit ca58742
Showing
70 changed files
with
5,212 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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 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 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,5 @@ | ||
.parcel-cache | ||
.DS_Store | ||
node_modules | ||
dist | ||
todos.json |
This file contains 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,4 @@ | ||
{ | ||
"extends": "@parcel/config-default", | ||
"runtimes": ["...", "@parcel/runtime-rsc"] | ||
} |
This file contains 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,51 @@ | ||
{ | ||
"name": "flight-parcel", | ||
"private": true, | ||
"workspaces": [ | ||
"examples/*" | ||
], | ||
"server": "dist/server.js", | ||
"targets": { | ||
"server": { | ||
"source": "src/server.tsx", | ||
"context": "react-server", | ||
"outputFormat": "commonjs", | ||
"includeNodeModules": { | ||
"express": false | ||
} | ||
} | ||
}, | ||
"scripts": { | ||
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/", | ||
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/", | ||
"dev": "concurrently \"npm run dev:watch\" \"npm run dev:start\"", | ||
"dev:watch": "NODE_ENV=development parcel watch", | ||
"dev:start": "NODE_ENV=development node dist/server.js", | ||
"build": "parcel build", | ||
"start": "node dist/server.js" | ||
}, | ||
"@parcel/resolver-default": { | ||
"packageExports": true | ||
}, | ||
"dependencies": { | ||
"@parcel/config-default": "2.0.0-dev.1789", | ||
"@parcel/runtime-rsc": "2.13.3-dev.3412", | ||
"@types/parcel-env": "^0.0.6", | ||
"@types/express": "*", | ||
"@types/node": "^22.10.1", | ||
"@types/react": "^19", | ||
"@types/react-dom": "^19", | ||
"concurrently": "^7.3.0", | ||
"express": "^4.18.2", | ||
"parcel": "2.0.0-dev.1787", | ||
"process": "^0.11.10", | ||
"react": "experimental", | ||
"react-dom": "experimental", | ||
"react-server-dom-parcel": "experimental", | ||
"rsc-html-stream": "^0.0.4", | ||
"ws": "^8.8.1" | ||
}, | ||
"@parcel/bundler-default": { | ||
"minBundleSize": 0 | ||
} | ||
} |
This file contains 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,21 @@ | ||
'use client'; | ||
|
||
import {ReactNode, useRef} from 'react'; | ||
|
||
export function Dialog({ | ||
trigger, | ||
children, | ||
}: { | ||
trigger: ReactNode; | ||
children: ReactNode; | ||
}) { | ||
let ref = useRef<HTMLDialogElement | null>(null); | ||
return ( | ||
<> | ||
<button onClick={() => ref.current?.showModal()}>{trigger}</button> | ||
<dialog ref={ref} onSubmit={() => ref.current?.close()}> | ||
{children} | ||
</dialog> | ||
</> | ||
); | ||
} |
This file contains 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,18 @@ | ||
import {createTodo} from './actions'; | ||
|
||
export function TodoCreate() { | ||
return ( | ||
<form action={createTodo}> | ||
<label> | ||
Title: <input name="title" /> | ||
</label> | ||
<label> | ||
Description: <textarea name="description" /> | ||
</label> | ||
<label> | ||
Due date: <input type="date" name="dueDate" /> | ||
</label> | ||
<button>Add todo</button> | ||
</form> | ||
); | ||
} |
This file contains 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,25 @@ | ||
import {getTodo, updateTodo} from './actions'; | ||
|
||
export async function TodoDetail({id}: {id: number}) { | ||
let todo = await getTodo(id); | ||
if (!todo) { | ||
return <p>Todo not found</p>; | ||
} | ||
|
||
return ( | ||
<form className="todo" action={updateTodo.bind(null, todo.id)}> | ||
<label> | ||
Title: <input name="title" defaultValue={todo.title} /> | ||
</label> | ||
<label> | ||
Description:{' '} | ||
<textarea name="description" defaultValue={todo.description} /> | ||
</label> | ||
<label> | ||
Due date:{' '} | ||
<input type="date" name="dueDate" defaultValue={todo.dueDate} /> | ||
</label> | ||
<button type="submit">Update todo</button> | ||
</form> | ||
); | ||
} |
This file contains 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,37 @@ | ||
'use client'; | ||
|
||
import {startTransition, useOptimistic} from 'react'; | ||
import {deleteTodo, setTodoComplete, type Todo as ITodo} from './actions'; | ||
|
||
export function TodoItem({ | ||
todo, | ||
isSelected, | ||
}: { | ||
todo: ITodo; | ||
isSelected: boolean; | ||
}) { | ||
let [isOptimisticComplete, setOptimisticComplete] = useOptimistic( | ||
todo.isComplete, | ||
); | ||
|
||
return ( | ||
<li data-selected={isSelected || undefined}> | ||
<input | ||
type="checkbox" | ||
checked={isOptimisticComplete} | ||
onChange={e => { | ||
startTransition(async () => { | ||
setOptimisticComplete(e.target.checked); | ||
await setTodoComplete(todo.id, e.target.checked); | ||
}); | ||
}} | ||
/> | ||
<a | ||
href={`/todos/${todo.id}`} | ||
aria-current={isSelected ? 'page' : undefined}> | ||
{todo.title} | ||
</a> | ||
<button onClick={() => deleteTodo(todo.id)}>x</button> | ||
</li> | ||
); | ||
} |
This file contains 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,13 @@ | ||
import {TodoItem} from './TodoItem'; | ||
import {getTodos} from './actions'; | ||
|
||
export async function TodoList({id}: {id: number | undefined}) { | ||
let todos = await getTodos(); | ||
return ( | ||
<ul className="todo-list"> | ||
{todos.map(todo => ( | ||
<TodoItem key={todo.id} todo={todo} isSelected={todo.id === id} /> | ||
))} | ||
</ul> | ||
); | ||
} |
This file contains 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,63 @@ | ||
body { | ||
font-family: system-ui; | ||
color-scheme: light dark; | ||
} | ||
|
||
form { | ||
display: grid; | ||
grid-template-columns: auto 1fr; | ||
flex-direction: column; | ||
max-width: 400px; | ||
gap: 8px; | ||
} | ||
|
||
label { | ||
display: contents; | ||
} | ||
|
||
main { | ||
display: flex; | ||
gap: 32px; | ||
} | ||
|
||
.todo-column { | ||
width: 250px; | ||
} | ||
|
||
header { | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
max-width: 250px; | ||
padding: 8px; | ||
padding-right: 40px; | ||
box-sizing: border-box; | ||
} | ||
|
||
.todo-list { | ||
max-width: 250px; | ||
padding: 0; | ||
list-style: none; | ||
padding-right: 32px; | ||
border-right: 1px solid gray; | ||
|
||
li { | ||
display: flex; | ||
gap: 8px; | ||
padding: 8px; | ||
border-radius: 8px; | ||
accent-color: light-dark(black, white); | ||
|
||
a { | ||
color: inherit; | ||
text-decoration: none; | ||
width: 100%; | ||
} | ||
|
||
&[data-selected] { | ||
background-color: light-dark(#222, #ddd); | ||
color: light-dark(#ddd, #222); | ||
accent-color: light-dark(white, black); | ||
} | ||
} | ||
} |
This file contains 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,35 @@ | ||
'use server-entry'; | ||
|
||
import './client'; | ||
import './Todos.css'; | ||
import {Resources} from '@parcel/runtime-rsc'; | ||
import {Dialog} from './Dialog'; | ||
import {TodoDetail} from './TodoDetail'; | ||
import {TodoCreate} from './TodoCreate'; | ||
import {TodoList} from './TodoList'; | ||
|
||
export async function Todos({id}: {id?: number}) { | ||
return ( | ||
<html style={{colorScheme: 'dark light'}}> | ||
<head> | ||
<title>Todos</title> | ||
<Resources /> | ||
</head> | ||
<body> | ||
<header> | ||
<h1>Todos</h1> | ||
<Dialog trigger="+"> | ||
<h2>Add todo</h2> | ||
<TodoCreate /> | ||
</Dialog> | ||
</header> | ||
<main> | ||
<div className="todo-column"> | ||
<TodoList id={id} /> | ||
</div> | ||
{id != null ? <TodoDetail key={id} id={id} /> : <p>Select a todo</p>} | ||
</main> | ||
</body> | ||
</html> | ||
); | ||
} |
This file contains 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,75 @@ | ||
'use server'; | ||
|
||
import fs from 'fs/promises'; | ||
|
||
export interface Todo { | ||
id: number; | ||
title: string; | ||
description: string; | ||
dueDate: string; | ||
isComplete: boolean; | ||
} | ||
|
||
export async function getTodos(): Promise<Todo[]> { | ||
try { | ||
let contents = await fs.readFile('todos.json', 'utf8'); | ||
return JSON.parse(contents); | ||
} catch { | ||
await fs.writeFile('todos.json', '[]'); | ||
return []; | ||
} | ||
} | ||
|
||
export async function getTodo(id: number): Promise<Todo | undefined> { | ||
let todos = await getTodos(); | ||
return todos.find(todo => todo.id === id); | ||
} | ||
|
||
export async function createTodo(formData: FormData) { | ||
let todos = await getTodos(); | ||
let title = formData.get('title'); | ||
let description = formData.get('description'); | ||
let dueDate = formData.get('dueDate'); | ||
let id = todos.length > 0 ? Math.max(...todos.map(todo => todo.id)) + 1 : 0; | ||
todos.push({ | ||
id, | ||
title: typeof title === 'string' ? title : '', | ||
description: typeof description === 'string' ? description : '', | ||
dueDate: typeof dueDate === 'string' ? dueDate : new Date().toISOString(), | ||
isComplete: false, | ||
}); | ||
await fs.writeFile('todos.json', JSON.stringify(todos)); | ||
} | ||
|
||
export async function updateTodo(id: number, formData: FormData) { | ||
let todos = await getTodos(); | ||
let title = formData.get('title'); | ||
let description = formData.get('description'); | ||
let dueDate = formData.get('dueDate'); | ||
let todo = todos.find(todo => todo.id === id); | ||
if (todo) { | ||
todo.title = typeof title === 'string' ? title : ''; | ||
todo.description = typeof description === 'string' ? description : ''; | ||
todo.dueDate = | ||
typeof dueDate === 'string' ? dueDate : new Date().toISOString(); | ||
await fs.writeFile('todos.json', JSON.stringify(todos)); | ||
} | ||
} | ||
|
||
export async function setTodoComplete(id: number, isComplete: boolean) { | ||
let todos = await getTodos(); | ||
let todo = todos.find(todo => todo.id === id); | ||
if (todo) { | ||
todo.isComplete = isComplete; | ||
await fs.writeFile('todos.json', JSON.stringify(todos)); | ||
} | ||
} | ||
|
||
export async function deleteTodo(id: number) { | ||
let todos = await getTodos(); | ||
let index = todos.findIndex(todo => todo.id === id); | ||
if (index >= 0) { | ||
todos.splice(index, 1); | ||
await fs.writeFile('todos.json', JSON.stringify(todos)); | ||
} | ||
} |
Oops, something went wrong.