Skip to content
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

experimental feature flag for rsc #8837

Merged
merged 10 commits into from
Jul 7, 2023
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
29 changes: 29 additions & 0 deletions packages/cli/src/commands/experimental/setupRsc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers'

import { getEpilogue } from './util'

export const command = 'setup-rsc'

export const description = 'Enable React Server Components (RSC)'

export const EXPERIMENTAL_TOPIC_ID = 5081

export const builder = (yargs) => {
yargs
.option('force', {
alias: 'f',
default: false,
description: 'Overwrite existing configuration',
type: 'boolean',
})
.epilogue(getEpilogue(command, description, EXPERIMENTAL_TOPIC_ID, true))
}

export const handler = async (options) => {
recordTelemetryAttributes({
command: ['experimental', command].join(' '),
force: options.force,
})
const { handler } = await import('./setupRscHandler.js')
return handler(options)
}
157 changes: 157 additions & 0 deletions packages/cli/src/commands/experimental/setupRscHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import fs from 'fs'
import path from 'path'

import { Listr } from 'listr2'

import { getConfig, getConfigPath } from '@redwoodjs/project-config'
import { errorTelemetry } from '@redwoodjs/telemetry'

import { getPaths, writeFile } from '../../lib'
import c from '../../lib/colors'
import { isTypeScriptProject } from '../../lib/project'

import {
command,
description,
EXPERIMENTAL_TOPIC_ID,
} from './setupStreamingSsr'
import { printTaskEpilogue } from './util'

export const handler = async ({ force, verbose }) => {
const rwPaths = getPaths()
const redwoodTomlPath = getConfigPath()
const configContent = fs.readFileSync(redwoodTomlPath, 'utf-8')

const tasks = new Listr(
[
{
title: 'Check prerequisites',
task: () => {
if (!rwPaths.web.entryClient || !rwPaths.web.viteConfig) {
throw new Error('Vite needs to be setup before you can enable RSCs')
}

if (!getConfig().experimental?.streamingSsr?.enabled) {
throw new Error(
'The Streaming SSR experimental feature must be enabled before you can enable RSCs'
)
}

if (!isTypeScriptProject()) {
throw new Error(
'RSCs are only supported in TypeScript projects at this time'
)
}
},
},
{
title: 'Adding config to redwood.toml...',
task: (_ctx, task) => {
if (!configContent.includes('[experimental.rsc]')) {
writeFile(
redwoodTomlPath,
configContent.concat('\n[experimental.rsc]\n enabled = true\n'),
{
overwriteExisting: true, // redwood.toml always exists
}
)
} else {
if (force) {
task.output = 'Overwriting config in redwood.toml'

writeFile(
redwoodTomlPath,
configContent.replace(
// Enable if it's currently disabled
'\n[experimental.rsc]\n enabled = false\n',
'\n[experimental.rsc]\n enabled = true\n'
),
{
overwriteExisting: true, // redwood.toml always exists
}
)
} else {
task.skip(
'The [experimental.rsc] config block already exists in your `redwood.toml` file.'
)
}
}
},
options: { persistentOutput: true },
},
{
title: 'Adding entries.ts...',
task: async () => {
const entriesTemplate = fs.readFileSync(
path.resolve(__dirname, 'templates', 'rsc', 'entries.ts.template'),
'utf-8'
)
const entriesPath = path.join(rwPaths.web.src, 'entries.ts')

writeFile(entriesPath, entriesTemplate, {
overwriteExisting: force,
})
},
},
{
title: 'Updating App.tsx...',
task: async () => {
const appTemplate = fs.readFileSync(
path.resolve(__dirname, 'templates', 'rsc', 'App.tsx.template'),
'utf-8'
)
const appPath = rwPaths.web.app

writeFile(appPath, appTemplate, {
overwriteExisting: true,
})
},
},
{
title: 'Adding Counter.tsx...',
task: async () => {
const counterTemplate = fs.readFileSync(
path.resolve(__dirname, 'templates', 'rsc', 'Counter.tsx.template'),
'utf-8'
)
const counterPath = path.join(rwPaths.web.src, 'Counter.tsx')

writeFile(counterPath, counterTemplate, {
overwriteExisting: force,
})
},
},
{
title: 'Updating index.html...',
task: async () => {
let indexHtml = fs.readFileSync(rwPaths.web.html, 'utf-8')
indexHtml = indexHtml.replace(
'href="/favicon.png" />',
'href="/favicon.png" />\n <script type="module" src="entry.client.tsx"></script>'
)

writeFile(rwPaths.web.html, indexHtml, {
overwriteExisting: true,
})
},
},
{
task: () => {
printTaskEpilogue(command, description, EXPERIMENTAL_TOPIC_ID)
},
},
],
{
rendererOptions: { collapseSubtasks: false, persistentOutput: true },
renderer: verbose ? 'verbose' : 'default',
}
)

try {
await tasks.run()
} catch (e) {
errorTelemetry(process.argv, e.message)
console.error(c.error(e.message))
process.exit(e?.exitCode || 1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Counter } from './Counter'

const App = ({ name = 'Anonymous' }) => {
return (
<div style={{ border: '3px red dashed', margin: '1em', padding: '1em' }}>
<h1>Hello {name}!!</h1>
<h3>This is a server component.</h3>
<Counter />
</div>
)
}

export default App
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client'

import React from 'react'

export const Counter = () => {
const [count, setCount] = React.useState(0)

return (
<div style={{ border: '3px blue dashed', margin: '1em', padding: '1em' }}>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<h3>This is a client component.</h3>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type GetEntry = (rscId: string) => Promise<
| React.FunctionComponent
| {
default: React.FunctionComponent
}
| null
>

export function defineEntries(getEntry: GetEntry) {
return {
getEntry,
}
}

export default defineEntries(
// getEntry
async (id) => {
switch (id) {
case 'App':
return import('./App')
default:
return null
}
}
)
3 changes: 3 additions & 0 deletions packages/project-config/src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ describe('getConfig', () => {
"apiSdk": undefined,
"enabled": false,
},
"rsc": {
"enabled": false,
},
"streamingSsr": {
"enabled": false,
},
Expand Down
6 changes: 6 additions & 0 deletions packages/project-config/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export interface Config {
streamingSsr: {
enabled: boolean
}
rsc: {
enabled: boolean
}
}
}

Expand Down Expand Up @@ -183,6 +186,9 @@ const DEFAULT_CONFIG: Config = {
streamingSsr: {
enabled: false,
},
rsc: {
enabled: false,
},
},
}

Expand Down
5 changes: 5 additions & 0 deletions packages/router/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ declare global {
*/
var RWJS_EXP_STREAMING_SSR: boolean

/**
* Is the experimental RSC feature enabled?
*/
var RWJS_EXP_RSC: boolean

namespace NodeJS {
interface Global {
/**
Expand Down
1 change: 1 addition & 0 deletions packages/vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export default function redwoodPluginVite(): PluginOption[] {
RWJS_EXP_STREAMING_SSR:
rwConfig.experimental.streamingSsr &&
rwConfig.experimental.streamingSsr.enabled,
RWJS_EXP_RSC: rwConfig.experimental?.rsc?.enabled,
},
RWJS_DEBUG_ENV: {
RWJS_SRC_ROOT: rwPaths.web.src,
Expand Down
3 changes: 3 additions & 0 deletions packages/web/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare global {
/** URL or absolute path to serverless functions */
RWJS_API_URL: string
RWJS_EXP_STREAMING_SSR: boolean
RWJS_EXP_RSC: boolean

__REDWOOD__APP_TITLE: string
__REDWOOD__APOLLO_STATE: NormalizedCacheObject
Expand All @@ -34,6 +35,8 @@ declare global {
var RWJS_SRC_ROOT: string
/** Flag for experimental Streaming and SSR support */
var RWJS_EXP_STREAMING_SSR: boolean
/** Flag for experimental RSC support */
var RWJS_EXP_RSC: boolean

namespace NodeJS {
interface Global {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ globalThis.RWJS_API_GRAPHQL_URL = RWJS_ENV.RWJS_API_GRAPHQL_URL as string
globalThis.RWJS_API_URL = RWJS_ENV.RWJS_API_URL as string
globalThis.__REDWOOD__APP_TITLE = RWJS_ENV.__REDWOOD__APP_TITLE as string
globalThis.RWJS_EXP_STREAMING_SSR = RWJS_ENV.RWJS_EXP_STREAMING_SSR as boolean
globalThis.RWJS_EXP_RSC = RWJS_ENV.RWJS_EXP_RSC as boolean