Skip to content

Commit c7a5b13

Browse files
authored
experimental feature flag for rsc (#8837)
1 parent d4888a9 commit c7a5b13

11 files changed

Lines changed: 258 additions & 0 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'
2+
3+
import { getEpilogue } from './util'
4+
5+
export const command = 'setup-rsc'
6+
7+
export const description = 'Enable React Server Components (RSC)'
8+
9+
export const EXPERIMENTAL_TOPIC_ID = 5081
10+
11+
export const builder = (yargs) => {
12+
yargs
13+
.option('force', {
14+
alias: 'f',
15+
default: false,
16+
description: 'Overwrite existing configuration',
17+
type: 'boolean',
18+
})
19+
.epilogue(getEpilogue(command, description, EXPERIMENTAL_TOPIC_ID, true))
20+
}
21+
22+
export const handler = async (options) => {
23+
recordTelemetryAttributes({
24+
command: ['experimental', command].join(' '),
25+
force: options.force,
26+
})
27+
const { handler } = await import('./setupRscHandler.js')
28+
return handler(options)
29+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
4+
import { Listr } from 'listr2'
5+
6+
import { getConfig, getConfigPath } from '@redwoodjs/project-config'
7+
import { errorTelemetry } from '@redwoodjs/telemetry'
8+
9+
import { getPaths, writeFile } from '../../lib'
10+
import c from '../../lib/colors'
11+
import { isTypeScriptProject } from '../../lib/project'
12+
13+
import {
14+
command,
15+
description,
16+
EXPERIMENTAL_TOPIC_ID,
17+
} from './setupStreamingSsr'
18+
import { printTaskEpilogue } from './util'
19+
20+
export const handler = async ({ force, verbose }) => {
21+
const rwPaths = getPaths()
22+
const redwoodTomlPath = getConfigPath()
23+
const configContent = fs.readFileSync(redwoodTomlPath, 'utf-8')
24+
25+
const tasks = new Listr(
26+
[
27+
{
28+
title: 'Check prerequisites',
29+
task: () => {
30+
if (!rwPaths.web.entryClient || !rwPaths.web.viteConfig) {
31+
throw new Error('Vite needs to be setup before you can enable RSCs')
32+
}
33+
34+
if (!getConfig().experimental?.streamingSsr?.enabled) {
35+
throw new Error(
36+
'The Streaming SSR experimental feature must be enabled before you can enable RSCs'
37+
)
38+
}
39+
40+
if (!isTypeScriptProject()) {
41+
throw new Error(
42+
'RSCs are only supported in TypeScript projects at this time'
43+
)
44+
}
45+
},
46+
},
47+
{
48+
title: 'Adding config to redwood.toml...',
49+
task: (_ctx, task) => {
50+
if (!configContent.includes('[experimental.rsc]')) {
51+
writeFile(
52+
redwoodTomlPath,
53+
configContent.concat('\n[experimental.rsc]\n enabled = true\n'),
54+
{
55+
overwriteExisting: true, // redwood.toml always exists
56+
}
57+
)
58+
} else {
59+
if (force) {
60+
task.output = 'Overwriting config in redwood.toml'
61+
62+
writeFile(
63+
redwoodTomlPath,
64+
configContent.replace(
65+
// Enable if it's currently disabled
66+
'\n[experimental.rsc]\n enabled = false\n',
67+
'\n[experimental.rsc]\n enabled = true\n'
68+
),
69+
{
70+
overwriteExisting: true, // redwood.toml always exists
71+
}
72+
)
73+
} else {
74+
task.skip(
75+
'The [experimental.rsc] config block already exists in your `redwood.toml` file.'
76+
)
77+
}
78+
}
79+
},
80+
options: { persistentOutput: true },
81+
},
82+
{
83+
title: 'Adding entries.ts...',
84+
task: async () => {
85+
const entriesTemplate = fs.readFileSync(
86+
path.resolve(__dirname, 'templates', 'rsc', 'entries.ts.template'),
87+
'utf-8'
88+
)
89+
const entriesPath = path.join(rwPaths.web.src, 'entries.ts')
90+
91+
writeFile(entriesPath, entriesTemplate, {
92+
overwriteExisting: force,
93+
})
94+
},
95+
},
96+
{
97+
title: 'Updating App.tsx...',
98+
task: async () => {
99+
const appTemplate = fs.readFileSync(
100+
path.resolve(__dirname, 'templates', 'rsc', 'App.tsx.template'),
101+
'utf-8'
102+
)
103+
const appPath = rwPaths.web.app
104+
105+
writeFile(appPath, appTemplate, {
106+
overwriteExisting: true,
107+
})
108+
},
109+
},
110+
{
111+
title: 'Adding Counter.tsx...',
112+
task: async () => {
113+
const counterTemplate = fs.readFileSync(
114+
path.resolve(__dirname, 'templates', 'rsc', 'Counter.tsx.template'),
115+
'utf-8'
116+
)
117+
const counterPath = path.join(rwPaths.web.src, 'Counter.tsx')
118+
119+
writeFile(counterPath, counterTemplate, {
120+
overwriteExisting: force,
121+
})
122+
},
123+
},
124+
{
125+
title: 'Updating index.html...',
126+
task: async () => {
127+
let indexHtml = fs.readFileSync(rwPaths.web.html, 'utf-8')
128+
indexHtml = indexHtml.replace(
129+
'href="/favicon.png" />',
130+
'href="/favicon.png" />\n <script type="module" src="entry.client.tsx"></script>'
131+
)
132+
133+
writeFile(rwPaths.web.html, indexHtml, {
134+
overwriteExisting: true,
135+
})
136+
},
137+
},
138+
{
139+
task: () => {
140+
printTaskEpilogue(command, description, EXPERIMENTAL_TOPIC_ID)
141+
},
142+
},
143+
],
144+
{
145+
rendererOptions: { collapseSubtasks: false, persistentOutput: true },
146+
renderer: verbose ? 'verbose' : 'default',
147+
}
148+
)
149+
150+
try {
151+
await tasks.run()
152+
} catch (e) {
153+
errorTelemetry(process.argv, e.message)
154+
console.error(c.error(e.message))
155+
process.exit(e?.exitCode || 1)
156+
}
157+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Counter } from './Counter'
2+
3+
const App = ({ name = 'Anonymous' }) => {
4+
return (
5+
<div style={{ border: '3px red dashed', margin: '1em', padding: '1em' }}>
6+
<h1>Hello {name}!!</h1>
7+
<h3>This is a server component.</h3>
8+
<Counter />
9+
</div>
10+
)
11+
}
12+
13+
export default App
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use client'
2+
3+
import React from 'react'
4+
5+
export const Counter = () => {
6+
const [count, setCount] = React.useState(0)
7+
8+
return (
9+
<div style={{ border: '3px blue dashed', margin: '1em', padding: '1em' }}>
10+
<p>Count: {count}</p>
11+
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
12+
<h3>This is a client component.</h3>
13+
</div>
14+
)
15+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export type GetEntry = (rscId: string) => Promise<
2+
| React.FunctionComponent
3+
| {
4+
default: React.FunctionComponent
5+
}
6+
| null
7+
>
8+
9+
export function defineEntries(getEntry: GetEntry) {
10+
return {
11+
getEntry,
12+
}
13+
}
14+
15+
export default defineEntries(
16+
// getEntry
17+
async (id) => {
18+
switch (id) {
19+
case 'App':
20+
return import('./App')
21+
default:
22+
return null
23+
}
24+
}
25+
)

packages/project-config/src/__tests__/config.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ describe('getConfig', () => {
5555
"apiSdk": undefined,
5656
"enabled": false,
5757
},
58+
"rsc": {
59+
"enabled": false,
60+
},
5861
"streamingSsr": {
5962
"enabled": false,
6063
},

packages/project-config/src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ export interface Config {
107107
streamingSsr: {
108108
enabled: boolean
109109
}
110+
rsc: {
111+
enabled: boolean
112+
}
110113
}
111114
}
112115

@@ -183,6 +186,9 @@ const DEFAULT_CONFIG: Config = {
183186
streamingSsr: {
184187
enabled: false,
185188
},
189+
rsc: {
190+
enabled: false,
191+
},
186192
},
187193
}
188194

packages/router/ambient.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ declare global {
1919
*/
2020
var RWJS_EXP_STREAMING_SSR: boolean
2121

22+
/**
23+
* Is the experimental RSC feature enabled?
24+
*/
25+
var RWJS_EXP_RSC: boolean
26+
2227
namespace NodeJS {
2328
interface Global {
2429
/**

packages/vite/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export default function redwoodPluginVite(): PluginOption[] {
102102
RWJS_EXP_STREAMING_SSR:
103103
rwConfig.experimental.streamingSsr &&
104104
rwConfig.experimental.streamingSsr.enabled,
105+
RWJS_EXP_RSC: rwConfig.experimental?.rsc?.enabled,
105106
},
106107
RWJS_DEBUG_ENV: {
107108
RWJS_SRC_ROOT: rwPaths.web.src,

packages/web/ambient.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ declare global {
1414
/** URL or absolute path to serverless functions */
1515
RWJS_API_URL: string
1616
RWJS_EXP_STREAMING_SSR: boolean
17+
RWJS_EXP_RSC: boolean
1718

1819
__REDWOOD__APP_TITLE: string
1920
__REDWOOD__APOLLO_STATE: NormalizedCacheObject
@@ -34,6 +35,8 @@ declare global {
3435
var RWJS_SRC_ROOT: string
3536
/** Flag for experimental Streaming and SSR support */
3637
var RWJS_EXP_STREAMING_SSR: boolean
38+
/** Flag for experimental RSC support */
39+
var RWJS_EXP_RSC: boolean
3740

3841
namespace NodeJS {
3942
interface Global {

0 commit comments

Comments
 (0)