From c7a5b138c43e888b49a20ddddfbf3bbc2ab2c00d Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Fri, 7 Jul 2023 04:06:35 +0200 Subject: [PATCH] experimental feature flag for rsc (#8837) --- .../cli/src/commands/experimental/setupRsc.js | 29 ++++ .../commands/experimental/setupRscHandler.js | 157 ++++++++++++++++++ .../templates/rsc/App.tsx.template | 13 ++ .../templates/rsc/Counter.tsx.template | 15 ++ .../templates/rsc/entries.ts.template | 25 +++ .../src/__tests__/config.test.ts | 3 + packages/project-config/src/config.ts | 6 + packages/router/ambient.d.ts | 5 + packages/vite/src/index.ts | 1 + packages/web/ambient.d.ts | 3 + packages/web/src/config.ts | 1 + 11 files changed, 258 insertions(+) create mode 100644 packages/cli/src/commands/experimental/setupRsc.js create mode 100644 packages/cli/src/commands/experimental/setupRscHandler.js create mode 100644 packages/cli/src/commands/experimental/templates/rsc/App.tsx.template create mode 100644 packages/cli/src/commands/experimental/templates/rsc/Counter.tsx.template create mode 100644 packages/cli/src/commands/experimental/templates/rsc/entries.ts.template diff --git a/packages/cli/src/commands/experimental/setupRsc.js b/packages/cli/src/commands/experimental/setupRsc.js new file mode 100644 index 000000000000..9ad8ca5aca6b --- /dev/null +++ b/packages/cli/src/commands/experimental/setupRsc.js @@ -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) +} diff --git a/packages/cli/src/commands/experimental/setupRscHandler.js b/packages/cli/src/commands/experimental/setupRscHandler.js new file mode 100644 index 000000000000..4612e883f626 --- /dev/null +++ b/packages/cli/src/commands/experimental/setupRscHandler.js @@ -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 ' + ) + + 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) + } +} diff --git a/packages/cli/src/commands/experimental/templates/rsc/App.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/App.tsx.template new file mode 100644 index 000000000000..f060d7838319 --- /dev/null +++ b/packages/cli/src/commands/experimental/templates/rsc/App.tsx.template @@ -0,0 +1,13 @@ +import { Counter } from './Counter' + +const App = ({ name = 'Anonymous' }) => { + return ( +
+

Hello {name}!!

+

This is a server component.

+ +
+ ) +} + +export default App diff --git a/packages/cli/src/commands/experimental/templates/rsc/Counter.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/Counter.tsx.template new file mode 100644 index 000000000000..af7d4db69f8a --- /dev/null +++ b/packages/cli/src/commands/experimental/templates/rsc/Counter.tsx.template @@ -0,0 +1,15 @@ +'use client' + +import React from 'react' + +export const Counter = () => { + const [count, setCount] = React.useState(0) + + return ( +
+

Count: {count}

+ +

This is a client component.

+
+ ) +} diff --git a/packages/cli/src/commands/experimental/templates/rsc/entries.ts.template b/packages/cli/src/commands/experimental/templates/rsc/entries.ts.template new file mode 100644 index 000000000000..bd03abc6d606 --- /dev/null +++ b/packages/cli/src/commands/experimental/templates/rsc/entries.ts.template @@ -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 + } + } +) diff --git a/packages/project-config/src/__tests__/config.test.ts b/packages/project-config/src/__tests__/config.test.ts index e0dac47ddc45..6e8cfc011e6f 100644 --- a/packages/project-config/src/__tests__/config.test.ts +++ b/packages/project-config/src/__tests__/config.test.ts @@ -55,6 +55,9 @@ describe('getConfig', () => { "apiSdk": undefined, "enabled": false, }, + "rsc": { + "enabled": false, + }, "streamingSsr": { "enabled": false, }, diff --git a/packages/project-config/src/config.ts b/packages/project-config/src/config.ts index a0544b9d1b0d..308b213a88a8 100644 --- a/packages/project-config/src/config.ts +++ b/packages/project-config/src/config.ts @@ -107,6 +107,9 @@ export interface Config { streamingSsr: { enabled: boolean } + rsc: { + enabled: boolean + } } } @@ -183,6 +186,9 @@ const DEFAULT_CONFIG: Config = { streamingSsr: { enabled: false, }, + rsc: { + enabled: false, + }, }, } diff --git a/packages/router/ambient.d.ts b/packages/router/ambient.d.ts index 7280b589e549..67f0ae224c47 100644 --- a/packages/router/ambient.d.ts +++ b/packages/router/ambient.d.ts @@ -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 { /** diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 939c81b4a350..22ae0a5a299c 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -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, diff --git a/packages/web/ambient.d.ts b/packages/web/ambient.d.ts index e3cf88ff7aeb..e452d2016432 100644 --- a/packages/web/ambient.d.ts +++ b/packages/web/ambient.d.ts @@ -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 @@ -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 { diff --git a/packages/web/src/config.ts b/packages/web/src/config.ts index dcb5f237ecd9..9dded9c11a06 100644 --- a/packages/web/src/config.ts +++ b/packages/web/src/config.ts @@ -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