From 69a17708e868cb49a13d88085e36c1dca34e2940 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Tue, 5 Dec 2023 12:03:44 +0100 Subject: [PATCH 01/12] RSC: Add RW env var definitions to Vite config and include FatalErrorBoundary (#9622) --- .../test-project-rsa/web/src/AboutCounter.tsx | 20 ++++ .../test-project-rsa/web/src/AboutPage.tsx | 5 +- .../test-project-rsa/web/src/entry.client.tsx | 18 +-- .../web/src/AboutCounter.tsx | 20 ++++ .../web/src/AboutPage.tsx | 5 +- .../web/src/entry.client.tsx | 18 +-- .../commands/experimental/setupRscHandler.js | 19 +++ .../templates/rsc/AboutCounter.tsx.template | 20 ++++ .../templates/rsc/AboutPage.tsx.template | 5 +- .../templates/rsc/entry.client.tsx.template | 18 +-- packages/vite/src/buildFeServer.ts | 1 - packages/vite/src/buildRscFeServer.ts | 7 +- .../react-server-dom-webpack/node-loader.ts | 1 + packages/vite/src/rsc/rscBuildClient.ts | 58 ++++++++- packages/vite/src/rsc/rscBuildRwEnvVars.ts | 38 ++++++ packages/vite/src/rsc/rscBuildServer.ts | 112 +++++++++++++----- packages/vite/src/waku-lib/vite-plugin-rsc.ts | 4 +- packages/web/src/config.ts | 4 + tasks/smoke-tests/rsc/tests/rsc.spec.ts | 9 ++ 19 files changed, 321 insertions(+), 61 deletions(-) create mode 100644 __fixtures__/test-project-rsa/web/src/AboutCounter.tsx create mode 100644 __fixtures__/test-project-rsc-external-packages/web/src/AboutCounter.tsx create mode 100644 packages/cli/src/commands/experimental/templates/rsc/AboutCounter.tsx.template create mode 100644 packages/vite/src/rsc/rscBuildRwEnvVars.ts diff --git a/__fixtures__/test-project-rsa/web/src/AboutCounter.tsx b/__fixtures__/test-project-rsa/web/src/AboutCounter.tsx new file mode 100644 index 000000000000..c86915e87f8b --- /dev/null +++ b/__fixtures__/test-project-rsa/web/src/AboutCounter.tsx @@ -0,0 +1,20 @@ +'use client' + +import React from 'react' + +// @ts-expect-error no types +import styles from './Counter.module.css' +import './Counter.css' + +export const AboutCounter = () => { + const [count, setCount] = React.useState(0) + + return ( +
+

Count: {count}

+ +

This is a client component.

+

RSC on client: {globalThis.RWJS_EXP_RSC ? 'enabled' : 'disabled'}

+
+ ) +} diff --git a/__fixtures__/test-project-rsa/web/src/AboutPage.tsx b/__fixtures__/test-project-rsa/web/src/AboutPage.tsx index f35708e1fa20..0caf6a955090 100644 --- a/__fixtures__/test-project-rsa/web/src/AboutPage.tsx +++ b/__fixtures__/test-project-rsa/web/src/AboutPage.tsx @@ -1,7 +1,7 @@ import { Assets } from '@redwoodjs/vite/assets' import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' -import { Counter } from './Counter' +import { AboutCounter } from './AboutCounter' import './AboutPage.css' @@ -17,7 +17,8 @@ const AboutPage = () => {

About Redwood

- + +

RSC on server: {globalThis.RWJS_EXP_RSC ? 'enabled' : 'disabled'}

) diff --git a/__fixtures__/test-project-rsa/web/src/entry.client.tsx b/__fixtures__/test-project-rsa/web/src/entry.client.tsx index a020c7fa31eb..f6d17f5aed82 100644 --- a/__fixtures__/test-project-rsa/web/src/entry.client.tsx +++ b/__fixtures__/test-project-rsa/web/src/entry.client.tsx @@ -2,8 +2,10 @@ import { createRoot } from 'react-dom/client' import { Route, Router, Set } from '@redwoodjs/router' import { serve } from '@redwoodjs/vite/client' +import { FatalErrorBoundary } from '@redwoodjs/web' import NavigationLayout from './layouts/NavigationLayout/NavigationLayout' +import FatalErrorPage from './pages/FatalErrorPage/FatalErrorPage' import NotFoundPage from './pages/NotFoundPage/NotFoundPage' const redwoodAppElement = document.getElementById('redwood-app') @@ -15,13 +17,15 @@ const root = createRoot(redwoodAppElement) const App = () => { return ( - - - - - - - + + + + + + + + + ) } diff --git a/__fixtures__/test-project-rsc-external-packages/web/src/AboutCounter.tsx b/__fixtures__/test-project-rsc-external-packages/web/src/AboutCounter.tsx new file mode 100644 index 000000000000..c86915e87f8b --- /dev/null +++ b/__fixtures__/test-project-rsc-external-packages/web/src/AboutCounter.tsx @@ -0,0 +1,20 @@ +'use client' + +import React from 'react' + +// @ts-expect-error no types +import styles from './Counter.module.css' +import './Counter.css' + +export const AboutCounter = () => { + const [count, setCount] = React.useState(0) + + return ( +
+

Count: {count}

+ +

This is a client component.

+

RSC on client: {globalThis.RWJS_EXP_RSC ? 'enabled' : 'disabled'}

+
+ ) +} diff --git a/__fixtures__/test-project-rsc-external-packages/web/src/AboutPage.tsx b/__fixtures__/test-project-rsc-external-packages/web/src/AboutPage.tsx index f35708e1fa20..0caf6a955090 100644 --- a/__fixtures__/test-project-rsc-external-packages/web/src/AboutPage.tsx +++ b/__fixtures__/test-project-rsc-external-packages/web/src/AboutPage.tsx @@ -1,7 +1,7 @@ import { Assets } from '@redwoodjs/vite/assets' import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' -import { Counter } from './Counter' +import { AboutCounter } from './AboutCounter' import './AboutPage.css' @@ -17,7 +17,8 @@ const AboutPage = () => {

About Redwood

- + +

RSC on server: {globalThis.RWJS_EXP_RSC ? 'enabled' : 'disabled'}

) diff --git a/__fixtures__/test-project-rsc-external-packages/web/src/entry.client.tsx b/__fixtures__/test-project-rsc-external-packages/web/src/entry.client.tsx index a020c7fa31eb..f6d17f5aed82 100644 --- a/__fixtures__/test-project-rsc-external-packages/web/src/entry.client.tsx +++ b/__fixtures__/test-project-rsc-external-packages/web/src/entry.client.tsx @@ -2,8 +2,10 @@ import { createRoot } from 'react-dom/client' import { Route, Router, Set } from '@redwoodjs/router' import { serve } from '@redwoodjs/vite/client' +import { FatalErrorBoundary } from '@redwoodjs/web' import NavigationLayout from './layouts/NavigationLayout/NavigationLayout' +import FatalErrorPage from './pages/FatalErrorPage/FatalErrorPage' import NotFoundPage from './pages/NotFoundPage/NotFoundPage' const redwoodAppElement = document.getElementById('redwood-app') @@ -15,13 +17,15 @@ const root = createRoot(redwoodAppElement) const App = () => { return ( - - - - - - - + + + + + + + + + ) } diff --git a/packages/cli/src/commands/experimental/setupRscHandler.js b/packages/cli/src/commands/experimental/setupRscHandler.js index 58b5847263da..8ecf0e1b3ffe 100644 --- a/packages/cli/src/commands/experimental/setupRscHandler.js +++ b/packages/cli/src/commands/experimental/setupRscHandler.js @@ -148,6 +148,25 @@ export const handler = async ({ force, verbose }) => { }) }, }, + { + title: 'Adding AboutCounter.tsx...', + task: async () => { + const counterTemplate = fs.readFileSync( + path.resolve( + __dirname, + 'templates', + 'rsc', + 'AboutCounter.tsx.template' + ), + 'utf-8' + ) + const counterPath = path.join(rwPaths.web.src, 'AboutCounter.tsx') + + writeFile(counterPath, counterTemplate, { + overwriteExisting: force, + }) + }, + }, { title: 'Adding CSS files...', task: async () => { diff --git a/packages/cli/src/commands/experimental/templates/rsc/AboutCounter.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/AboutCounter.tsx.template new file mode 100644 index 000000000000..c86915e87f8b --- /dev/null +++ b/packages/cli/src/commands/experimental/templates/rsc/AboutCounter.tsx.template @@ -0,0 +1,20 @@ +'use client' + +import React from 'react' + +// @ts-expect-error no types +import styles from './Counter.module.css' +import './Counter.css' + +export const AboutCounter = () => { + const [count, setCount] = React.useState(0) + + return ( +
+

Count: {count}

+ +

This is a client component.

+

RSC on client: {globalThis.RWJS_EXP_RSC ? 'enabled' : 'disabled'}

+
+ ) +} diff --git a/packages/cli/src/commands/experimental/templates/rsc/AboutPage.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/AboutPage.tsx.template index f35708e1fa20..0caf6a955090 100644 --- a/packages/cli/src/commands/experimental/templates/rsc/AboutPage.tsx.template +++ b/packages/cli/src/commands/experimental/templates/rsc/AboutPage.tsx.template @@ -1,7 +1,7 @@ import { Assets } from '@redwoodjs/vite/assets' import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' -import { Counter } from './Counter' +import { AboutCounter } from './AboutCounter' import './AboutPage.css' @@ -17,7 +17,8 @@ const AboutPage = () => {

About Redwood

- + +

RSC on server: {globalThis.RWJS_EXP_RSC ? 'enabled' : 'disabled'}

) diff --git a/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template index a020c7fa31eb..f6d17f5aed82 100644 --- a/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template +++ b/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template @@ -2,8 +2,10 @@ import { createRoot } from 'react-dom/client' import { Route, Router, Set } from '@redwoodjs/router' import { serve } from '@redwoodjs/vite/client' +import { FatalErrorBoundary } from '@redwoodjs/web' import NavigationLayout from './layouts/NavigationLayout/NavigationLayout' +import FatalErrorPage from './pages/FatalErrorPage/FatalErrorPage' import NotFoundPage from './pages/NotFoundPage/NotFoundPage' const redwoodAppElement = document.getElementById('redwood-app') @@ -15,13 +17,15 @@ const root = createRoot(redwoodAppElement) const App = () => { return ( - - - - - - - + + + + + + + + + ) } diff --git a/packages/vite/src/buildFeServer.ts b/packages/vite/src/buildFeServer.ts index bfa139469e53..f5d01e254268 100644 --- a/packages/vite/src/buildFeServer.ts +++ b/packages/vite/src/buildFeServer.ts @@ -48,7 +48,6 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => { await buildRscFeServer({ viteConfigPath, - webSrc: rwPaths.web.src, webHtml: rwPaths.web.html, entries: rwPaths.web.entries, webDist: rwPaths.web.dist, diff --git a/packages/vite/src/buildRscFeServer.ts b/packages/vite/src/buildRscFeServer.ts index f07fa98385e3..c989b92aacca 100644 --- a/packages/vite/src/buildRscFeServer.ts +++ b/packages/vite/src/buildRscFeServer.ts @@ -2,11 +2,11 @@ import { rscBuildAnalyze } from './rsc/rscBuildAnalyze' import { rscBuildClient } from './rsc/rscBuildClient' import { rscBuildClientEntriesMappings } from './rsc/rscBuildClientEntriesFile' import { rscBuildCopyCssAssets } from './rsc/rscBuildCopyCssAssets' +import { rscBuildRwEnvVars } from './rsc/rscBuildRwEnvVars' import { rscBuildServer } from './rsc/rscBuildServer' interface Args { viteConfigPath: string - webSrc: string webHtml: string entries: string webDist: string @@ -16,7 +16,6 @@ interface Args { export const buildRscFeServer = async ({ viteConfigPath, - webSrc, webHtml, entries, webDist, @@ -30,7 +29,6 @@ export const buildRscFeServer = async ({ // Generate the client bundle const clientBuildOutput = await rscBuildClient( - webSrc, webHtml, webDist, clientEntryFiles @@ -54,4 +52,7 @@ export const buildRscFeServer = async ({ clientEntryFiles, webDistServerEntries ) + + // Mappings from server to client asset file names + await rscBuildRwEnvVars(webDistServerEntries) } diff --git a/packages/vite/src/react-server-dom-webpack/node-loader.ts b/packages/vite/src/react-server-dom-webpack/node-loader.ts index 565d126e8f0c..0e390c8ec8d6 100644 --- a/packages/vite/src/react-server-dom-webpack/node-loader.ts +++ b/packages/vite/src/react-server-dom-webpack/node-loader.ts @@ -570,6 +570,7 @@ export async function load( url, defaultLoad ) + return { format: 'module', source: newSrc, diff --git a/packages/vite/src/rsc/rscBuildClient.ts b/packages/vite/src/rsc/rscBuildClient.ts index 650273c3dbd4..415906ec178c 100644 --- a/packages/vite/src/rsc/rscBuildClient.ts +++ b/packages/vite/src/rsc/rscBuildClient.ts @@ -1,6 +1,10 @@ +import path from 'node:path' + import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' +import { getConfig, getPaths } from '@redwoodjs/project-config' + import { onWarn } from '../lib/onWarn' import { rscIndexPlugin } from '../waku-lib/vite-plugin-rsc' @@ -10,14 +14,64 @@ import { rscIndexPlugin } from '../waku-lib/vite-plugin-rsc' * Generate the client bundle */ export async function rscBuildClient( - webSrc: string, webHtml: string, webDist: string, clientEntryFiles: Record ) { + const rwPaths = getPaths() + const rwConfig = getConfig() + + const graphQlUrl = + rwConfig.web.apiGraphQLUrl ?? rwConfig.web.apiUrl + '/graphql' + const clientBuildOutput = await viteBuild({ // configFile: viteConfigPath, - root: webSrc, + root: rwPaths.web.src, + envPrefix: 'REDWOOD_ENV_', + publicDir: path.join(rwPaths.web.base, 'public'), + define: { + RWJS_ENV: { + __REDWOOD__APP_TITLE: rwConfig.web.title || path.basename(rwPaths.base), + RWJS_API_GRAPHQL_URL: graphQlUrl, + RWJS_API_URL: rwConfig.web.apiUrl, + RWJS_EXP_STREAMING_SSR: rwConfig.experimental?.streamingSsr?.enabled, + RWJS_EXP_RSC: rwConfig.experimental?.rsc?.enabled, + }, + RWJS_DEBUG_ENV: { + RWJS_SRC_ROOT: rwPaths.web.src, + REDWOOD_ENV_EDITOR: JSON.stringify(process.env.REDWOOD_ENV_EDITOR), + }, + // Vite can automatically expose environment variables, but we + // disable that in `buildFeServer.ts` by setting `envFile: false` + // because we want to use our own logic for loading .env, + // .env.defaults, etc + // The two object spreads below will expose all environment + // variables listed in redwood.toml and all environment variables + // prefixed with REDWOOD_ENV_ + ...Object.fromEntries( + rwConfig.web.includeEnvironmentVariables.flatMap((envName) => [ + // TODO (RSC): Figure out if/why we need to disable eslint here. + // Re-enable if possible + // eslint-disable-next-line + [`import.meta.env.${envName}`, JSON.stringify(process.env[envName])], + // TODO (RSC): Figure out if/why we need to disable eslint here + // Re-enable if possible + // eslint-disable-next-line + [`process.env.${envName}`, JSON.stringify(process.env[envName])], + ]) + ), + ...Object.entries(process.env).reduce>( + (acc, [key, value]) => { + if (key.startsWith('REDWOOD_ENV_')) { + acc[`import.meta.env.${key}`] = JSON.stringify(value) + acc[`process.env.${key}`] = JSON.stringify(value) + } + + return acc + }, + {} + ), + }, plugins: [react(), rscIndexPlugin()], build: { outDir: webDist, diff --git a/packages/vite/src/rsc/rscBuildRwEnvVars.ts b/packages/vite/src/rsc/rscBuildRwEnvVars.ts new file mode 100644 index 000000000000..d0c767eacea2 --- /dev/null +++ b/packages/vite/src/rsc/rscBuildRwEnvVars.ts @@ -0,0 +1,38 @@ +import fs from 'fs/promises' + +/** + * RSC build. Step 6. + * Make RW specific env vars available to server components. + * For client components this is done as a side-effect of importing from + * @redwoodjs/web (see packages/web/src/config.ts). + * The import of entries.js that we're adding this to is handled by the + * RSC worker we've got set up + */ +export async function rscBuildRwEnvVars(webDistServerEntries: string) { + await fs.appendFile( + webDistServerEntries, + ` + +globalThis.RWJS_API_GRAPHQL_URL = RWJS_ENV.RWJS_API_GRAPHQL_URL +globalThis.RWJS_API_URL = RWJS_ENV.RWJS_API_URL +globalThis.__REDWOOD__APP_TITLE = RWJS_ENV.__REDWOOD__APP_TITLE +globalThis.RWJS_EXP_STREAMING_SSR = RWJS_ENV.RWJS_EXP_STREAMING_SSR +globalThis.RWJS_EXP_RSC = RWJS_ENV.RWJS_EXP_RSC +` + ) + + // TODO (RSC): See if we can just import that config.ts file from + // @redwoodjs/web/dist/config here + // Or find some other way to not duplicate the definitions + // Want to look at `noExternal` in our worker to do RWJS_ENV transforms. + // And/or possibly optimizeDeps. I'm not sure. Also, right now we're getting + // "`require` is not defined" errors. Probably some ESM/CJS issue + // Also seems like when using noExternal we have to use just @redwoodjs/web + // instead of @redwoodjs/web/dist/config which I think would be better + // + // console.log('adding rwjs/web import to entries.js') + // return fs.appendFile( + // webDistServerEntries, + // `\nimport '@redwoodjs/web/dist/config'` + // ) +} diff --git a/packages/vite/src/rsc/rscBuildServer.ts b/packages/vite/src/rsc/rscBuildServer.ts index 1848f7b1ed37..f4ea94b1a75f 100644 --- a/packages/vite/src/rsc/rscBuildServer.ts +++ b/packages/vite/src/rsc/rscBuildServer.ts @@ -3,7 +3,7 @@ import path from 'node:path' import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' -import { getPaths } from '@redwoodjs/project-config' +import { getConfig, getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn' @@ -28,10 +28,63 @@ export async function rscBuildServer( console.log('input', input) const rwPaths = getPaths() + const rwConfig = getConfig() + + console.log( + 'rscBuildServer.ts RWJS_EXP_RSC', + rwConfig.experimental?.rsc?.enabled + ) const serverBuildOutput = await viteBuild({ // ...configFileConfig, root: rwPaths.web.base, + envPrefix: 'REDWOOD_ENV_', + publicDir: path.join(rwPaths.web.base, 'public'), + define: { + RWJS_ENV: { + // @NOTE we're avoiding process.env here, unlike webpack + RWJS_API_GRAPHQL_URL: + rwConfig.web.apiGraphQLUrl ?? rwConfig.web.apiUrl + '/graphql', + RWJS_API_URL: rwConfig.web.apiUrl, + __REDWOOD__APP_TITLE: rwConfig.web.title || path.basename(rwPaths.base), + RWJS_EXP_STREAMING_SSR: rwConfig.experimental?.streamingSsr?.enabled, + RWJS_EXP_RSC: rwConfig.experimental?.rsc?.enabled, + }, + RWJS_DEBUG_ENV: { + RWJS_SRC_ROOT: rwPaths.web.src, + REDWOOD_ENV_EDITOR: JSON.stringify(process.env.REDWOOD_ENV_EDITOR), + }, + // Vite can automatically expose environment variables, but we + // disable that in `buildFeServer.ts` by setting `envFile: false` + // because we want to use our own logic for loading .env, + // .env.defaults, etc + // The two object spreads below will expose all environment + // variables listed in redwood.toml and all environment variables + // prefixed with REDWOOD_ENV_ + ...Object.fromEntries( + rwConfig.web.includeEnvironmentVariables.flatMap((envName) => [ + // TODO (RSC): Figure out if/why we need to disable eslint here. + // Re-enable if possible + // eslint-disable-next-line + [`import.meta.env.${envName}`, JSON.stringify(process.env[envName])], + // TODO (RSC): Figure out if/why we need to disable eslint here + // Re-enable if possible + // eslint-disable-next-line + [`process.env.${envName}`, JSON.stringify(process.env[envName])], + ]) + ), + ...Object.entries(process.env).reduce>( + (acc, [key, value]) => { + if (key.startsWith('REDWOOD_ENV_')) { + acc[`import.meta.env.${key}`] = JSON.stringify(value) + acc[`process.env.${key}`] = JSON.stringify(value) + } + + return acc + }, + {} + ), + }, ssr: { // Externalize everything except packages with files that have // 'use client' in them (which are the files in `clientEntryFiles`) @@ -41,32 +94,37 @@ export async function rscBuildServer( // The map function below will return '..' for local files. That's not // very pretty, but it works. It just won't match anything. noExternal: Object.values(clientEntryFiles).map((fullPath) => { - // On Windows `fullPath` will be something like - // D:/a/redwood/test-project-rsc-external-packages/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js - const relativePath = path.relative( - path.join(rwPaths.base, 'node_modules'), - fullPath - ) - // On Windows `relativePath` will be something like - // @tobbe.dev\rsc-test\dist\rsc-test.es.js - // So `splitPath` will in this case become - // ['@tobbe.dev', 'rsc-test', 'dist', 'rsc-test.es.js'] - const splitPath = relativePath.split(path.sep) - - // Packages without scope. Full package name looks like: package_name - let packageName = splitPath[0] - - // Handle scoped packages. Full package name looks like: - // @org_name/package_name - if (splitPath[0].startsWith('@')) { - // join @org_name with package_name - packageName = path.join(splitPath[0], splitPath[1]) - } - - console.log('noExternal packageName', packageName) - - return packageName - }), + // On Windows `fullPath` will be something like + // D:/a/redwood/test-project-rsc-external-packages/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js + const relativePath = path.relative( + path.join(rwPaths.base, 'node_modules'), + fullPath + ) + // On Windows `relativePath` will be something like + // @tobbe.dev\rsc-test\dist\rsc-test.es.js + // So `splitPath` will in this case become + // ['@tobbe.dev', 'rsc-test', 'dist', 'rsc-test.es.js'] + const splitPath = relativePath.split(path.sep) + + // Packages without scope. Full package name looks like: package_name + let packageName = splitPath[0] + + // Handle scoped packages. Full package name looks like: + // @org_name/package_name + if (splitPath[0].startsWith('@')) { + // join @org_name with package_name + packageName = path.join(splitPath[0], splitPath[1]) + } + + console.log( + 'noExternal fullPath', + fullPath, + 'packageName', + packageName + ) + + return packageName + }), resolve: { externalConditions: ['react-server'], }, diff --git a/packages/vite/src/waku-lib/vite-plugin-rsc.ts b/packages/vite/src/waku-lib/vite-plugin-rsc.ts index f9c54c98df32..8ecf79c3561a 100644 --- a/packages/vite/src/waku-lib/vite-plugin-rsc.ts +++ b/packages/vite/src/waku-lib/vite-plugin-rsc.ts @@ -93,7 +93,9 @@ export function rscTransformPlugin(): Plugin { resolve ) - return (await RSDWNodeLoader.load(id, null, load)).source + const source = (await RSDWNodeLoader.load(id, null, load)).source + + return source }, } } diff --git a/packages/web/src/config.ts b/packages/web/src/config.ts index 9dded9c11a06..59bd013f1fc1 100644 --- a/packages/web/src/config.ts +++ b/packages/web/src/config.ts @@ -1,6 +1,10 @@ // RWJS_ENV and RWJS_DEBUG_ENV // are defined in Webpack.common.js and Vite.config.js +console.log('config.ts') +console.log('config.ts', RWJS_ENV) +console.log('config.ts') + // @NOTE: do not use globalThis on the right side, because webpack cannot access these vars then globalThis.RWJS_API_GRAPHQL_URL = RWJS_ENV.RWJS_API_GRAPHQL_URL as string globalThis.RWJS_API_URL = RWJS_ENV.RWJS_API_URL as string diff --git a/tasks/smoke-tests/rsc/tests/rsc.spec.ts b/tasks/smoke-tests/rsc/tests/rsc.spec.ts index 5866a974276a..a72ca9251734 100644 --- a/tasks/smoke-tests/rsc/tests/rsc.spec.ts +++ b/tasks/smoke-tests/rsc/tests/rsc.spec.ts @@ -16,3 +16,12 @@ test('Setting up RSC should give you a test project with a client side counter c page.close() }) + +test('RWJS_* env vars', async ({ page }) => { + await page.goto('/about') + + await expect(page.getByText('RSC on client: enabled')).toBeVisible() + await expect(page.getByText('RSC on server: enabled')).toBeVisible() + + page.close() +}) From e9f5f015b7184a3aba103863330b6813125ab15c Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Tue, 5 Dec 2023 14:33:08 +0100 Subject: [PATCH 02/12] RSC: Use Routes.tsx for (client-side) routing (#9630) --- __fixtures__/test-project-rsa/web/src/Routes.tsx | 15 ++++++++++++--- .../test-project-rsa/web/src/entry.client.tsx | 16 ++-------------- .../web/src/Routes.tsx | 15 ++++++++++++--- .../web/src/entry.client.tsx | 16 ++-------------- .../templates/rsc/Routes.tsx.template | 15 ++++++++++++--- .../templates/rsc/entry.client.tsx.template | 16 ++-------------- 6 files changed, 42 insertions(+), 51 deletions(-) diff --git a/__fixtures__/test-project-rsa/web/src/Routes.tsx b/__fixtures__/test-project-rsa/web/src/Routes.tsx index 3ed60721de24..89a1df33eef0 100644 --- a/__fixtures__/test-project-rsa/web/src/Routes.tsx +++ b/__fixtures__/test-project-rsa/web/src/Routes.tsx @@ -7,13 +7,22 @@ // 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage -import { Router, Route } from '@redwoodjs/router' +import { Router, Route, Set } from '@redwoodjs/router' +import { serve } from '@redwoodjs/vite/client' + +import NavigationLayout from './layouts/NavigationLayout/NavigationLayout' +import NotFoundPage from './pages/NotFoundPage/NotFoundPage' + +const AboutPage = serve('AboutPage') +const HomePage = serve('HomePage') const Routes = () => { return ( - - + + + + ) diff --git a/__fixtures__/test-project-rsa/web/src/entry.client.tsx b/__fixtures__/test-project-rsa/web/src/entry.client.tsx index f6d17f5aed82..b7c0b5061610 100644 --- a/__fixtures__/test-project-rsa/web/src/entry.client.tsx +++ b/__fixtures__/test-project-rsa/web/src/entry.client.tsx @@ -1,30 +1,18 @@ import { createRoot } from 'react-dom/client' -import { Route, Router, Set } from '@redwoodjs/router' -import { serve } from '@redwoodjs/vite/client' import { FatalErrorBoundary } from '@redwoodjs/web' -import NavigationLayout from './layouts/NavigationLayout/NavigationLayout' import FatalErrorPage from './pages/FatalErrorPage/FatalErrorPage' -import NotFoundPage from './pages/NotFoundPage/NotFoundPage' +import Routes from './Routes' const redwoodAppElement = document.getElementById('redwood-app') -const AboutPage = serve('AboutPage') -const HomePage = serve('HomePage') - const root = createRoot(redwoodAppElement) const App = () => { return ( - - - - - - - + ) } diff --git a/__fixtures__/test-project-rsc-external-packages/web/src/Routes.tsx b/__fixtures__/test-project-rsc-external-packages/web/src/Routes.tsx index 3ed60721de24..89a1df33eef0 100644 --- a/__fixtures__/test-project-rsc-external-packages/web/src/Routes.tsx +++ b/__fixtures__/test-project-rsc-external-packages/web/src/Routes.tsx @@ -7,13 +7,22 @@ // 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage -import { Router, Route } from '@redwoodjs/router' +import { Router, Route, Set } from '@redwoodjs/router' +import { serve } from '@redwoodjs/vite/client' + +import NavigationLayout from './layouts/NavigationLayout/NavigationLayout' +import NotFoundPage from './pages/NotFoundPage/NotFoundPage' + +const AboutPage = serve('AboutPage') +const HomePage = serve('HomePage') const Routes = () => { return ( - - + + + + ) diff --git a/__fixtures__/test-project-rsc-external-packages/web/src/entry.client.tsx b/__fixtures__/test-project-rsc-external-packages/web/src/entry.client.tsx index f6d17f5aed82..b7c0b5061610 100644 --- a/__fixtures__/test-project-rsc-external-packages/web/src/entry.client.tsx +++ b/__fixtures__/test-project-rsc-external-packages/web/src/entry.client.tsx @@ -1,30 +1,18 @@ import { createRoot } from 'react-dom/client' -import { Route, Router, Set } from '@redwoodjs/router' -import { serve } from '@redwoodjs/vite/client' import { FatalErrorBoundary } from '@redwoodjs/web' -import NavigationLayout from './layouts/NavigationLayout/NavigationLayout' import FatalErrorPage from './pages/FatalErrorPage/FatalErrorPage' -import NotFoundPage from './pages/NotFoundPage/NotFoundPage' +import Routes from './Routes' const redwoodAppElement = document.getElementById('redwood-app') -const AboutPage = serve('AboutPage') -const HomePage = serve('HomePage') - const root = createRoot(redwoodAppElement) const App = () => { return ( - - - - - - - + ) } diff --git a/packages/cli/src/commands/experimental/templates/rsc/Routes.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/Routes.tsx.template index 3ed60721de24..89a1df33eef0 100644 --- a/packages/cli/src/commands/experimental/templates/rsc/Routes.tsx.template +++ b/packages/cli/src/commands/experimental/templates/rsc/Routes.tsx.template @@ -7,13 +7,22 @@ // 'src/pages/HomePage/HomePage.js' -> HomePage // 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage -import { Router, Route } from '@redwoodjs/router' +import { Router, Route, Set } from '@redwoodjs/router' +import { serve } from '@redwoodjs/vite/client' + +import NavigationLayout from './layouts/NavigationLayout/NavigationLayout' +import NotFoundPage from './pages/NotFoundPage/NotFoundPage' + +const AboutPage = serve('AboutPage') +const HomePage = serve('HomePage') const Routes = () => { return ( - - + + + + ) diff --git a/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template index f6d17f5aed82..b7c0b5061610 100644 --- a/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template +++ b/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template @@ -1,30 +1,18 @@ import { createRoot } from 'react-dom/client' -import { Route, Router, Set } from '@redwoodjs/router' -import { serve } from '@redwoodjs/vite/client' import { FatalErrorBoundary } from '@redwoodjs/web' -import NavigationLayout from './layouts/NavigationLayout/NavigationLayout' import FatalErrorPage from './pages/FatalErrorPage/FatalErrorPage' -import NotFoundPage from './pages/NotFoundPage/NotFoundPage' +import Routes from './Routes' const redwoodAppElement = document.getElementById('redwood-app') -const AboutPage = serve('AboutPage') -const HomePage = serve('HomePage') - const root = createRoot(redwoodAppElement) const App = () => { return ( - - - - - - - + ) } From cc33ebc66fe9c3f4b27cd1de535bdc0be945d136 Mon Sep 17 00:00:00 2001 From: xmaxcooking Date: Tue, 5 Dec 2023 21:49:06 +0100 Subject: [PATCH 03/12] Add vscode web debugger and compound (#9567) Adds 2 debug configurations to vscode/launch.json - Web Debugger (launches the built-in chrome web debugger) - Compound of Dev + Api + Web (launches a fully debuggable redwood with a single configuration) It makes sense to disable the browser open in the redwood.toml if you want to use the web debugger alone or in compound. ``` [browser] open = false ``` It'd also be possible to add a --fwd="--open=false" but that is currently being discussed as an issue in #9209 --------- Co-authored-by: Tobbe Lundberg --- __fixtures__/test-project/.vscode/launch.json | 24 +++++++++- __fixtures__/test-project/.vscode/tasks.json | 43 +++++++++++++++++ .../project-configuration-dev-test-build.mdx | 47 ++++++++++--------- .../templates/js/.vscode/launch.json | 24 +++++++++- .../templates/js/.vscode/tasks.json | 29 ++++++++++++ .../templates/ts/.vscode/launch.json | 24 +++++++++- .../templates/ts/.vscode/tasks.json | 29 ++++++++++++ .../create-redwood-app/tests/template.test.js | 2 + 8 files changed, 194 insertions(+), 28 deletions(-) create mode 100644 __fixtures__/test-project/.vscode/tasks.json create mode 100644 packages/create-redwood-app/templates/js/.vscode/tasks.json create mode 100644 packages/create-redwood-app/templates/ts/.vscode/tasks.json diff --git a/__fixtures__/test-project/.vscode/launch.json b/__fixtures__/test-project/.vscode/launch.json index ea5956966fff..340be43c34da 100644 --- a/__fixtures__/test-project/.vscode/launch.json +++ b/__fixtures__/test-project/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.3.0", "configurations": [ { - "command": "yarn redwood dev --apiDebugPort 18911", + "command": "yarn redwood dev --apiDebugPort 18911", // you can add --fwd='--open=false' to prevent the browser from opening "name": "Run Dev Server", "request": "launch", "type": "node-terminal" @@ -18,7 +18,16 @@ "localRoot": "${workspaceFolder}/node_modules/@redwoodjs/api-server/dist", "remoteRoot": "${workspaceFolder}/node_modules/@redwoodjs/api-server/dist", "sourceMaps": true, - "restart": true + "restart": true, + "preLaunchTask": "WaitForDevServer", + }, + { + "name": "Launch Web debugger", + "type": "chrome", + "request": "launch", + "url": "http://localhost:8910", + "webRoot": "${workspaceRoot}/web/src", + "preLaunchTask": "WaitForDevServer", }, { "command": "yarn redwood test api", @@ -32,5 +41,16 @@ "request": "launch", "type": "node-terminal" }, + ], + "compounds": [ + { + "name": "Start Debug", + "configurations": [ + "Run Dev Server", + "Attach API debugger", + "Launch Web debugger" + ], + "stopAll": true + } ] } diff --git a/__fixtures__/test-project/.vscode/tasks.json b/__fixtures__/test-project/.vscode/tasks.json new file mode 100644 index 000000000000..8b486bdf6596 --- /dev/null +++ b/__fixtures__/test-project/.vscode/tasks.json @@ -0,0 +1,43 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "WaitForDevServer", + "group": "none", + "type": "shell", + "windows": { + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", "Bypass", + "$port = $env:PORT; while (-not (Test-NetConnection -ComputerName localhost -Port $port)) { Start-Sleep -Seconds 1 };" + ] + }, + "linux": { + "command": "bash", + "args": [ + "-c", + "port=$PORT; while ! nc -z localhost $port; do sleep 1; done;" + ] + }, + "osx": { + "command": "bash", + "args": [ + "-c", + "port=$PORT; while ! nc -z localhost $port; do sleep 1; done;" + ] + }, + "options": { + "env": { + "port": "18911" + } + }, + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "panel": "shared", + "close": true + } + }, + ] +} diff --git a/docs/docs/project-configuration-dev-test-build.mdx b/docs/docs/project-configuration-dev-test-build.mdx index 3f5833966bff..061a08cb28c2 100644 --- a/docs/docs/project-configuration-dev-test-build.mdx +++ b/docs/docs/project-configuration-dev-test-build.mdx @@ -104,40 +104,28 @@ You can find all the details in the [source](https://github.com/redwoodjs/redwoo You can customize the types that Redwood generates from your project too! This is documented in a bit more detail in the [Generated Types](typescript/generated-types#customising-codegen-config) doc. -## Debugger configuration -The `yarn rw dev` command is configured by default to launch a debugger on the port `18911`, your Redwood app also ships with default configuration to attach a debugger from VSCode. - -Simply run your dev server, then attach the debugger from the "run and debug" panel. Quick demo below: - - +## Debug configurations +### Dev Server +The `yarn rw dev` command is configured by default to open a browser and a debugger on the port `18911` and your redwood app ships with several default configurations to debug with VSCode. -
- -> **ℹī¸ Tip: Can't see the "Attach debugger" configuration?** In VSCode -> -> You can grab the latest launch.json from the Redwood template [here](https://github.com/redwoodjs/redwood/blob/main/packages/create-redwood-app/templates/ts/.vscode/launch.json). Copy the contents into your project's `.vscode/launch.json` - - -#### Customizing the debug port -You can choose to use a different debug port in one of two ways: - +#### Customizing the configuration **a) Using the redwood.toml** -Add/change the `debugPort` under your api settings +Add/change the `debugPort` or `open` under your api settings ```toml title="redwood.toml" [web] # . - # . [api] - port = 8911 + # . // highlight-next-line - debugPort = 18911 # 👈 change me! + debugPort = 18911 # change me! +[browser] + // highlight-next-line + open = true # change me! ``` -If you set it to `false`, no debug port will be exposed. The `debugPort` is only ever used during development when running `yarn rw dev` - **b) Pass a flag to `rw dev` command** You can also pass a flag when you launch your dev servers, for example: @@ -149,6 +137,21 @@ The flag passed in the CLI will always take precedence over your setting in the Just remember to also change the port you are attaching to in your `./vscode/launch.json` +### API and Web Debuggers +Simply run your dev server, then attach the debugger from the "run and debug" panel. Quick demo below: + + + +### Compound Debugger +The compound configuration is a combination of the dev, api and web configurations. +It allows you to start all debugging configurations at once, facilitating simultaneous debugging of server and client-side code. + +
+ +> **ℹī¸ Tip: Can't see the debug configurations?** In VSCode +> +> You can grab the latest launch.json from the Redwood template [here](https://github.com/redwoodjs/redwood/blob/main/packages/create-redwood-app/templates/ts/.vscode/launch.json). Copy the contents into your project's `.vscode/launch.json` + ## Ignoring the `.yarn` folder The `.yarn` folder contains the most recent Yarn executable that Redwood supports diff --git a/packages/create-redwood-app/templates/js/.vscode/launch.json b/packages/create-redwood-app/templates/js/.vscode/launch.json index ea5956966fff..340be43c34da 100644 --- a/packages/create-redwood-app/templates/js/.vscode/launch.json +++ b/packages/create-redwood-app/templates/js/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.3.0", "configurations": [ { - "command": "yarn redwood dev --apiDebugPort 18911", + "command": "yarn redwood dev --apiDebugPort 18911", // you can add --fwd='--open=false' to prevent the browser from opening "name": "Run Dev Server", "request": "launch", "type": "node-terminal" @@ -18,7 +18,16 @@ "localRoot": "${workspaceFolder}/node_modules/@redwoodjs/api-server/dist", "remoteRoot": "${workspaceFolder}/node_modules/@redwoodjs/api-server/dist", "sourceMaps": true, - "restart": true + "restart": true, + "preLaunchTask": "WaitForDevServer", + }, + { + "name": "Launch Web debugger", + "type": "chrome", + "request": "launch", + "url": "http://localhost:8910", + "webRoot": "${workspaceRoot}/web/src", + "preLaunchTask": "WaitForDevServer", }, { "command": "yarn redwood test api", @@ -32,5 +41,16 @@ "request": "launch", "type": "node-terminal" }, + ], + "compounds": [ + { + "name": "Start Debug", + "configurations": [ + "Run Dev Server", + "Attach API debugger", + "Launch Web debugger" + ], + "stopAll": true + } ] } diff --git a/packages/create-redwood-app/templates/js/.vscode/tasks.json b/packages/create-redwood-app/templates/js/.vscode/tasks.json new file mode 100644 index 000000000000..549249ec6324 --- /dev/null +++ b/packages/create-redwood-app/templates/js/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "WaitForDevServer", + "group": "none", + "type": "shell", + "command": "bash", + "args": [ + "-c", + "while ! echo -n > /dev/tcp/localhost/18911; do sleep 1; done;" + ], + "windows": { + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", "Bypass", + "while (-not (Test-NetConnection -ComputerName localhost -Port 18911)) { Start-Sleep -Seconds 1 };" + ] + }, + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "panel": "shared", + "close": true + } + }, + ] +} diff --git a/packages/create-redwood-app/templates/ts/.vscode/launch.json b/packages/create-redwood-app/templates/ts/.vscode/launch.json index ea5956966fff..340be43c34da 100644 --- a/packages/create-redwood-app/templates/ts/.vscode/launch.json +++ b/packages/create-redwood-app/templates/ts/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.3.0", "configurations": [ { - "command": "yarn redwood dev --apiDebugPort 18911", + "command": "yarn redwood dev --apiDebugPort 18911", // you can add --fwd='--open=false' to prevent the browser from opening "name": "Run Dev Server", "request": "launch", "type": "node-terminal" @@ -18,7 +18,16 @@ "localRoot": "${workspaceFolder}/node_modules/@redwoodjs/api-server/dist", "remoteRoot": "${workspaceFolder}/node_modules/@redwoodjs/api-server/dist", "sourceMaps": true, - "restart": true + "restart": true, + "preLaunchTask": "WaitForDevServer", + }, + { + "name": "Launch Web debugger", + "type": "chrome", + "request": "launch", + "url": "http://localhost:8910", + "webRoot": "${workspaceRoot}/web/src", + "preLaunchTask": "WaitForDevServer", }, { "command": "yarn redwood test api", @@ -32,5 +41,16 @@ "request": "launch", "type": "node-terminal" }, + ], + "compounds": [ + { + "name": "Start Debug", + "configurations": [ + "Run Dev Server", + "Attach API debugger", + "Launch Web debugger" + ], + "stopAll": true + } ] } diff --git a/packages/create-redwood-app/templates/ts/.vscode/tasks.json b/packages/create-redwood-app/templates/ts/.vscode/tasks.json new file mode 100644 index 000000000000..549249ec6324 --- /dev/null +++ b/packages/create-redwood-app/templates/ts/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "WaitForDevServer", + "group": "none", + "type": "shell", + "command": "bash", + "args": [ + "-c", + "while ! echo -n > /dev/tcp/localhost/18911; do sleep 1; done;" + ], + "windows": { + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", "Bypass", + "while (-not (Test-NetConnection -ComputerName localhost -Port 18911)) { Start-Sleep -Seconds 1 };" + ] + }, + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "panel": "shared", + "close": true + } + }, + ] +} diff --git a/packages/create-redwood-app/tests/template.test.js b/packages/create-redwood-app/tests/template.test.js index c8afd900e4b9..8c7bb38a3ad8 100644 --- a/packages/create-redwood-app/tests/template.test.js +++ b/packages/create-redwood-app/tests/template.test.js @@ -17,6 +17,7 @@ describe('template', () => { "/.vscode/extensions.json", "/.vscode/launch.json", "/.vscode/settings.json", + "/.vscode/tasks.json", "/.yarn", "/.yarn/releases", "/.yarn/releases/yarn-3.7.0.cjs", @@ -103,6 +104,7 @@ describe('JS template', () => { "/.vscode/extensions.json", "/.vscode/launch.json", "/.vscode/settings.json", + "/.vscode/tasks.json", "/.yarn", "/.yarn/releases", "/.yarn/releases/yarn-3.7.0.cjs", From 94ccd23b79f3e7f52e1b239279dbf220ed69aa18 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 5 Dec 2023 13:12:34 -0800 Subject: [PATCH 04/12] chore(release): fix open answer --- tasks/release/prMilestoneCache.json | 11 ++++++++++- tasks/release/releaseLib.mjs | 8 ++++++-- tasks/release/triage/main_next.commitTriageData.json | 11 +++++++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/tasks/release/prMilestoneCache.json b/tasks/release/prMilestoneCache.json index 98ab7fd81c94..3acc0e90168f 100644 --- a/tasks/release/prMilestoneCache.json +++ b/tasks/release/prMilestoneCache.json @@ -259,5 +259,14 @@ "https://github.com/redwoodjs/redwood/pull/9616": "next-release", "https://github.com/redwoodjs/redwood/pull/9617": "next-release", "https://github.com/redwoodjs/redwood/pull/9618": "RSC", - "https://github.com/redwoodjs/redwood/pull/9614": "v6.4.2" + "https://github.com/redwoodjs/redwood/pull/9614": "v6.4.2", + "https://github.com/redwoodjs/redwood/pull/9629": "v6.5.0", + "https://github.com/redwoodjs/redwood/pull/9628": "v6.5.0", + "https://github.com/redwoodjs/redwood/pull/9627": "v6.5.0", + "https://github.com/redwoodjs/redwood/pull/9626": "v6.5.0", + "https://github.com/redwoodjs/redwood/pull/9625": "v6.5.0", + "https://github.com/redwoodjs/redwood/pull/9622": "RSC", + "https://github.com/redwoodjs/redwood/pull/9630": "RSC", + "https://github.com/redwoodjs/redwood/pull/9567": "next-release", + "https://github.com/redwoodjs/redwood/pull/9624": "next-release-patch" } diff --git a/tasks/release/releaseLib.mjs b/tasks/release/releaseLib.mjs index 37f4832431c2..5b8c86eb0cbc 100644 --- a/tasks/release/releaseLib.mjs +++ b/tasks/release/releaseLib.mjs @@ -824,7 +824,7 @@ export async function triageCommits({ commits, commitTriageData, range }) { comment = commentRes.comment } - if (['open', 'o'].includes(answer)) { + if (answer === 'open') { if (commit.url) { await $`open ${commit.url}` } else { @@ -848,7 +848,7 @@ export async function triageCommits({ commits, commitTriageData, range }) { /** * * @param {string} answer - * @returns {'yes'|'no'|'skip'} + * @returns {'yes'|'no'|'skip'|'open'} */ function getLongAnswer(answer) { answer = answer.toLowerCase() @@ -864,6 +864,10 @@ function getLongAnswer(answer) { if (['s', 'skip'].includes(answer)) { return 'skip' } + + if (['o', 'open'].includes(answer)) { + return 'open' + } } export let prMilestoneCache diff --git a/tasks/release/triage/main_next.commitTriageData.json b/tasks/release/triage/main_next.commitTriageData.json index e7d3bdf71a34..a8fcea367b85 100644 --- a/tasks/release/triage/main_next.commitTriageData.json +++ b/tasks/release/triage/main_next.commitTriageData.json @@ -333,9 +333,12 @@ "needsCherryPick": "skip", "comment": "Needs another PR to complete the feature" }, - "8759c0cf0209ded8c49f0071dd032ac5f4162e9b": { - "message": "fix(cli): avoid calling rw-vite-build via yarn (#9624)", - "needsCherryPick": "skip", - "comment": "We need to vet this in an RC before releasing it" + "69a17708e868cb49a13d88085e36c1dca34e2940": { + "message": "RSC: Add RW env var definitions to Vite config and include FatalErrorBoundary (#9622)", + "needsCherryPick": "no" + }, + "e9f5f015b7184a3aba103863330b6813125ab15c": { + "message": "RSC: Use Routes.tsx for (client-side) routing (#9630)", + "needsCherryPick": "no" } } From 1dcac65bcbf53aa77f4f8f924433363e5d2cfde3 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Tue, 5 Dec 2023 16:47:12 -0500 Subject: [PATCH 05/12] chore: Update Testing documentation to link to How to Test Email/Mailer (#9634) The testing docs include info on how to test service, directives, cells, and more ... but did not link to the "how to test email" with the recent Redwood Mailer package. This PR adds some short comy introducing the topic and a link to the main Mailer testing section. --- docs/docs/testing.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/docs/testing.md b/docs/docs/testing.md index 23b4a2afc340..12ee3579ad08 100644 --- a/docs/docs/testing.md +++ b/docs/docs/testing.md @@ -1975,6 +1975,16 @@ console.log(testCacheClient.storage) This is mainly helpful when you are testing for a very specific value, or have edgecases in how the serialization/deserialization works in the cache. +## Testing Mailer + +If your project uses [RedwoodJS Mailer](./mailer.md) to send emails, you can [also write tests](./mailer.md#testing) to make sure that email: + +* is sent to an sandbox inbox +* renders properly +* sets the expected to, from, cc, bcc, subject attributes based on the email sending logic +* checks that the html and text content is set correctly + +Since these tests send mail to a sandbox inbox, you can be confident that no emails accidentally get sent into the wild as part of your test or CI runs. ## Wrapping Up From b1adcc7ee8dd7a4fc4974bf822945a01a80c5d2c Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Tue, 5 Dec 2023 22:30:31 +0000 Subject: [PATCH 06/12] chore: Linting and disable some console logs (#9635) --- packages/vite/src/rsc/rscBuildServer.ts | 57 +++++++++++-------------- packages/web/src/config.ts | 7 +-- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/packages/vite/src/rsc/rscBuildServer.ts b/packages/vite/src/rsc/rscBuildServer.ts index f4ea94b1a75f..29667a8e02da 100644 --- a/packages/vite/src/rsc/rscBuildServer.ts +++ b/packages/vite/src/rsc/rscBuildServer.ts @@ -94,37 +94,32 @@ export async function rscBuildServer( // The map function below will return '..' for local files. That's not // very pretty, but it works. It just won't match anything. noExternal: Object.values(clientEntryFiles).map((fullPath) => { - // On Windows `fullPath` will be something like - // D:/a/redwood/test-project-rsc-external-packages/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js - const relativePath = path.relative( - path.join(rwPaths.base, 'node_modules'), - fullPath - ) - // On Windows `relativePath` will be something like - // @tobbe.dev\rsc-test\dist\rsc-test.es.js - // So `splitPath` will in this case become - // ['@tobbe.dev', 'rsc-test', 'dist', 'rsc-test.es.js'] - const splitPath = relativePath.split(path.sep) - - // Packages without scope. Full package name looks like: package_name - let packageName = splitPath[0] - - // Handle scoped packages. Full package name looks like: - // @org_name/package_name - if (splitPath[0].startsWith('@')) { - // join @org_name with package_name - packageName = path.join(splitPath[0], splitPath[1]) - } - - console.log( - 'noExternal fullPath', - fullPath, - 'packageName', - packageName - ) - - return packageName - }), + // On Windows `fullPath` will be something like + // D:/a/redwood/test-project-rsc-external-packages/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js + const relativePath = path.relative( + path.join(rwPaths.base, 'node_modules'), + fullPath + ) + // On Windows `relativePath` will be something like + // @tobbe.dev\rsc-test\dist\rsc-test.es.js + // So `splitPath` will in this case become + // ['@tobbe.dev', 'rsc-test', 'dist', 'rsc-test.es.js'] + const splitPath = relativePath.split(path.sep) + + // Packages without scope. Full package name looks like: package_name + let packageName = splitPath[0] + + // Handle scoped packages. Full package name looks like: + // @org_name/package_name + if (splitPath[0].startsWith('@')) { + // join @org_name with package_name + packageName = path.join(splitPath[0], splitPath[1]) + } + + console.log('noExternal fullPath', fullPath, 'packageName', packageName) + + return packageName + }), resolve: { externalConditions: ['react-server'], }, diff --git a/packages/web/src/config.ts b/packages/web/src/config.ts index 59bd013f1fc1..59f3fb10493f 100644 --- a/packages/web/src/config.ts +++ b/packages/web/src/config.ts @@ -1,9 +1,10 @@ // RWJS_ENV and RWJS_DEBUG_ENV // are defined in Webpack.common.js and Vite.config.js -console.log('config.ts') -console.log('config.ts', RWJS_ENV) -console.log('config.ts') +// Note: These lines are useful during RSC/SSR development but will execute for all projects, even those without RSC/SSR +// console.log('config.ts') +// console.log('config.ts', RWJS_ENV) +// console.log('config.ts') // @NOTE: do not use globalThis on the right side, because webpack cannot access these vars then globalThis.RWJS_API_GRAPHQL_URL = RWJS_ENV.RWJS_API_GRAPHQL_URL as string From 05192858f5aeca715a5b4975e99094253c2d3825 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 5 Dec 2023 22:23:36 -0800 Subject: [PATCH 07/12] chore(release): improve tooling - remove unused files - reversion docs on patch - try-catch all publishes - check node.js version --- tasks/release/prMilestoneCache.json | 3 +- tasks/release/release.mjs | 148 +++++++++++++++-- tasks/release/releaseLib.mjs | 31 +++- .../triage/main_next.commitTriageData.json | 4 + tasks/unpublishPackageVersions.mjs | 153 ------------------ tasks/update-package-versions | 92 ----------- 6 files changed, 164 insertions(+), 267 deletions(-) delete mode 100644 tasks/unpublishPackageVersions.mjs delete mode 100755 tasks/update-package-versions diff --git a/tasks/release/prMilestoneCache.json b/tasks/release/prMilestoneCache.json index 3acc0e90168f..664555d6dad4 100644 --- a/tasks/release/prMilestoneCache.json +++ b/tasks/release/prMilestoneCache.json @@ -268,5 +268,6 @@ "https://github.com/redwoodjs/redwood/pull/9622": "RSC", "https://github.com/redwoodjs/redwood/pull/9630": "RSC", "https://github.com/redwoodjs/redwood/pull/9567": "next-release", - "https://github.com/redwoodjs/redwood/pull/9624": "next-release-patch" + "https://github.com/redwoodjs/redwood/pull/9624": "next-release-patch", + "https://github.com/redwoodjs/redwood/pull/9635": "chore" } diff --git a/tasks/release/release.mjs b/tasks/release/release.mjs index c7964539c4b1..cde15b06a471 100644 --- a/tasks/release/release.mjs +++ b/tasks/release/release.mjs @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url' import { parseArgs as _parseArgs } from 'node:util' import semverPackage from 'semver' -import { cd, chalk, fs, question, within, $ } from 'zx' +import { cd, chalk, fs, path, question, within, $ } from 'zx' import { branchExists, @@ -12,6 +12,7 @@ import { consoleBoxen, getOctokit, getLatestRelease, + getMilestones, getPRsWithMilestone, getRedwoodRemote, getSpinner, @@ -19,6 +20,7 @@ import { prompts, unwrap, setVerbosity, + findUp, } from './releaseLib.mjs' let octokit @@ -37,6 +39,14 @@ export async function main() { const { verbose } = options setVerbosity(verbose) + try { + await doChecks() + } catch (e) { + consoleBoxen('👷 Heads up', e.message) + process.exitCode = 1 + return + } + try { // We'll be making requests to GitHub for PRs. While this data isn't private, we could get rate-limited without a token. octokit = await getOctokit() @@ -121,6 +131,29 @@ main() // ─── Helpers ───────────────────────────────────────────────────────────────── +async function doChecks() { + // Check Node.js version. Right now, v18.19 breaks one of our tests. + const nodeVersion = unwrap(await $`node -v`) + + if (nodeVersion.startsWith('v20')) { + throw new Error( + [ + 'The framework is currently built for v18; running QA with v20 may cause issues.', + 'Switch to v18.18.2.', + ].join('\n') + ) + } + + if (nodeVersion.startsWith('v18.19')) { + throw new Error( + [ + 'Node.js v18.19 currently has a breaking change that makes one of our tests fail.', + 'Switch to v18.18.2.', + ].join('\n') + ) + } +} + function parseArgs() { const { values } = _parseArgs({ options: { @@ -262,6 +295,9 @@ async function resolveMilestones() { ) ) ) { + const milestones = await getMilestones() + milestone = milestones.find(({ title }) => title === nextRelease) + if (!milestone) { milestone = await createMilestone(nextRelease) } @@ -373,10 +409,18 @@ async function releaseMajorOrMinor() { await $`git reset --hard HEAD~1` await updateCreateRedwoodAppTemplates() console.log() - await within(async () => { - $.verbose = true - await $`yarn lerna publish from-package` - }) + try { + await within(async () => { + $.verbose = true + await $`yarn lerna publish from-package` + }) + } catch { + exitIfNo( + await question( + 'Publishing failed. You can usually recover from this by running `yarn lerna publish from-package` again. Continue? [Y/n] > ' + ) + ) + } console.log() // Clean up commits and push. This combines the update package versions commit and update CRWA commit into one. @@ -419,6 +463,14 @@ async function versionDocs() { const spinner = getSpinner('Versioning docs') await cd('./docs') + + if (fs.existsSync(`./versioned_docs/version-${nextDocsVersion}`)) { + await $`rm -rf ./versioned_docs/version-${nextDocsVersion}` + + const versions = await fs.readJSON('./versions.json') + await fs.writeJSON('./versions.json', versions.slice(1)) + } + await $`yarn` await $`yarn clear` await $`yarn docusaurus docs:version ${nextDocsVersion}` @@ -442,18 +494,45 @@ async function cleanInstallUpdate() { await $`yarn install` spinner.text = 'Updating package versions' - await $`./tasks/update-package-versions ${nextRelease}` + + const lernaVersion = nextRelease.replace('v', '') + await $`yarn lerna version ${lernaVersion} --force-publish --no-push --no-git-tag-version --exact --yes` + + const cwd = path.dirname(findUp('lerna.json')) + + spinner.text = 'Updating CRWA templates...' + + const tsTemplatePath = path.join( + cwd, + 'packages/create-redwood-app/templates/ts' + ) + updateRWJSPkgsVersion(tsTemplatePath, lernaVersion) + updateRWJSPkgsVersion(path.join(tsTemplatePath, 'api'), lernaVersion) + updateRWJSPkgsVersion(path.join(tsTemplatePath, 'web'), lernaVersion) + $.verbose && console.log() + + const jsTemplatePath = path.join( + cwd, + 'packages/create-redwood-app/templates/js' + ) + updateRWJSPkgsVersion(jsTemplatePath, lernaVersion) + updateRWJSPkgsVersion(path.join(jsTemplatePath, 'api'), lernaVersion) + updateRWJSPkgsVersion(path.join(jsTemplatePath, 'web'), lernaVersion) + $.verbose && console.log() + + spinner.text = 'Updating test-project fixture...' + + const fixturePath = path.join(cwd, '__fixtures__/test-project') + updateRWJSPkgsVersion(fixturePath, lernaVersion) + updateRWJSPkgsVersion(path.join(fixturePath, 'api'), lernaVersion) + updateRWJSPkgsVersion(path.join(fixturePath, 'web'), lernaVersion) + $.verbose && console.log() spinner.text = 'Installing' await $`yarn install` spinner.stop() $.verbose && console.log() - exitIfNo( - await question( - `The package versions have been updated. Everything look ok? [Y/n] > ` - ) - ) await $`git commit -am "chore: update package versions to ${nextRelease}"` } @@ -575,6 +654,8 @@ async function releasePatch() { $.verbose && console.log() await runQA() $.verbose && console.log() + await versionDocs() + $.verbose && console.log() exitIfNo( await question(`Everything passed local QA. Ok to publish to NPM? [Y/n] > `) @@ -604,10 +685,18 @@ async function releasePatch() { await $`git reset --hard HEAD~1` await updateCreateRedwoodAppTemplates() console.log() - await within(async () => { - $.verbose = true - await $`yarn lerna publish from-package` - }) + try { + await within(async () => { + $.verbose = true + await $`yarn lerna publish from-package` + }) + } catch { + exitIfNo( + await question( + 'Publishing failed. You can usually recover from this by running `yarn lerna publish from-package` again. Continue? [Y/n] > ' + ) + ) + } console.log() // Clean up commits and push. This combines the update package versions commit and update CRWA commit into one. @@ -655,3 +744,32 @@ async function exitIfNo(answer, { code } = { code: 1 }) { process.exit(code) } + +/** + * Iterates over `@redwoodjs/*` dependencies in a package.json and updates their version. + * + * @param {string} pkgPath + * @param {string} version + */ +function updateRWJSPkgsVersion(pkgPath, version) { + const pkg = fs.readJSONSync(path.join(pkgPath, 'package.json'), 'utf-8') + + for (const dep of Object.keys(pkg.dependencies ?? {}).filter(isRWJSPkg)) { + console.log(` - ${dep}: ${pkg.dependencies[dep]} => ${version}`) + pkg.dependencies[dep] = `${version}` + } + + for (const dep of Object.keys(pkg.devDependencies ?? {}).filter(isRWJSPkg)) { + console.log(` - ${dep}: ${pkg.devDependencies[dep]} => ${version}`) + pkg.devDependencies[dep] = `${version}` + } + + for (const dep of Object.keys(pkg.peerDependencies ?? {}).filter(isRWJSPkg)) { + console.log(` - ${dep}: ${pkg.devDependencies[dep]} => ${version}`) + pkg.devDependencies[dep] = `${version}` + } + + fs.writeJSONSync(path.join(pkgPath, 'package.json'), pkg, { spaces: 2 }) +} + +const isRWJSPkg = (pkg) => pkg.startsWith('@redwoodjs/') diff --git a/tasks/release/releaseLib.mjs b/tasks/release/releaseLib.mjs index 5b8c86eb0cbc..cc6e9fe8b11c 100644 --- a/tasks/release/releaseLib.mjs +++ b/tasks/release/releaseLib.mjs @@ -6,7 +6,7 @@ import { Octokit } from 'octokit' import ora from 'ora' import _prompts from 'prompts' import semver from 'semver' -import { chalk, fs, question, $ } from 'zx' +import { chalk, fs, path, question, $ } from 'zx' import 'dotenv/config' // ─── Types ─────────────────────────────────────────────────────────────────── @@ -1264,9 +1264,28 @@ export async function openCherryPickPRs() { await $`open https://github.com/redwoodjs/redwood/pulls?q=is%3Apr+is%3Aopen+label%3Acherry-pick` } -// ─── Wip ───────────────────────────────────────────────────────────────────── +// ─── Misc ──────────────────────────────────────────────────────────────────── -// Troublesome lines to test... -// Here, there's two PR syntaxes. We want the last one -// < | | f5d1a1a1f77afafb252031c07f5405b998004f20 feature(#8676): added usernameMatch criteria to login methods to match signup (#8686) -// Find one with square brackets ([]) +/** + * Find a file by walking up parent directories. + * + * @param {string} file + * @param {string} [startingDirectory=process.cwd()] + * @returns {string | null} + */ +export function findUp(file, startingDirectory = process.cwd()) { + const possibleFilepath = path.join(startingDirectory, file) + + if (fs.existsSync(possibleFilepath)) { + return possibleFilepath + } + + const parentDirectory = path.dirname(startingDirectory) + + // If we've reached the root directory, there's no file to be found. + if (parentDirectory === startingDirectory) { + return null + } + + return findUp(file, parentDirectory) +} diff --git a/tasks/release/triage/main_next.commitTriageData.json b/tasks/release/triage/main_next.commitTriageData.json index a8fcea367b85..97cd7489f00e 100644 --- a/tasks/release/triage/main_next.commitTriageData.json +++ b/tasks/release/triage/main_next.commitTriageData.json @@ -340,5 +340,9 @@ "e9f5f015b7184a3aba103863330b6813125ab15c": { "message": "RSC: Use Routes.tsx for (client-side) routing (#9630)", "needsCherryPick": "no" + }, + "b1adcc7ee8dd7a4fc4974bf822945a01a80c5d2c": { + "message": "chore: Linting and disable some console logs (#9635)", + "needsCherryPick": "no" } } diff --git a/tasks/unpublishPackageVersions.mjs b/tasks/unpublishPackageVersions.mjs deleted file mode 100644 index 7a2a14efb62f..000000000000 --- a/tasks/unpublishPackageVersions.mjs +++ /dev/null @@ -1,153 +0,0 @@ -/* eslint-env node */ - -// HOW TO UNPUBLISH -// npm only allows unpublishig if the following conditions are met: -// 1. no other packages in the npm Public Registry depend on -// 2. had less than 300 downloads over the last week -// 3. has a single owner/maintainer -// -// At this time, I cannot approval to unpublish any packages < 1.0.0 -// The script is working fine -// - -import { execSync } from 'child_process' -import { basename } from 'node:path' -import readline from 'readline' - -async function main() { - const [_nodeBinPath, scriptPath, ...argOptions] = process.argv - - if (process.argv.includes('help', '-h', '--help')) { - console.log( - [ - '', - `yarn node tasks/${basename( - scriptPath - )} [packageName] [targetTag] [targetVersion] --iKnowWhatImDoing`, - '', - ' STATUS: Options only work if you pass ALL or NONE', - '', - ' This script uses "npm unpublish" and passes "--dry-run" by default because safety.', - ' Read on if you want to run it for realz...', - '', - `packageName [string]`, - 'name of package to unpublish', - '', - `targetTag [string]`, - 'valid npm tag, e.g. "canary"', - '', - `targetVersion [string]`, - 'semver to target using "startsWith" to match; e.g. "0.1" or "2.1" or "0"', - '', - `--iKnowWhatImDoing`, - 'if you really want to unpublish, you MUST pass this arg', - '', - '', - ].join('\n') - ) - - return - } - - const prompt = (argument) => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }) - - return new Promise((resolve) => { - rl.question( - `Please enter the ${argument} of the package to unpublish: `, - (answer) => { - rl.close() - resolve(answer) - } - ) - }) - } - - try { - // ONLY works if you pass all or none - const packageName = - argOptions.length === 0 - ? await prompt('Name') - : argOptions[0] !== '--iKnowWhatImDoing' - ? argOptions[0] - : await prompt('Name') - const targetTag = - argOptions.length === 0 - ? await prompt('NPM Tag') - : argOptions[1] !== '--iKnowWhatImDoing' - ? argOptions[1] - : await prompt('NPM Tag') - const targetVersion = - argOptions.length === 0 - ? await prompt('Semver "startsWith" string') - : argOptions[2] !== '--iKnowWhatImDoing' - ? argOptions[2] - : await prompt('Semver "startsWith" string') - - const stdout = execSync(`npm view ${packageName} --json`).toString() - - const packageData = JSON.parse(stdout) - const canaryVersions = - packageData.versions && packageData.versions.length > 0 - ? packageData.versions - .filter((version) => version.includes(targetTag)) - .filter((version) => version.startsWith(targetVersion)) - : [] - - if (canaryVersions.length === 0) { - console.log( - `No "${targetTag}" packages found starting with semver "${targetVersion}" for package '${packageName}'.` - ) - return - } - - console.log( - `The following versions of package '${packageName}' will be unpublished:` - ) - console.log(canaryVersions.join(`\n`)) - console.log( - '', - `Total number of packages that will be unpublished: ${canaryVersions.length}`, - '' - ) - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }) - - rl.question('Do you want to proceed? Type YES to confirm: ', (answer) => { - rl.close() - - if (answer !== 'YES') { - console.log('EJECTED! (phewf)') - return - } - - const dryRunOption = process.argv.includes('--iKnowWhatImDoing') - ? '' - : '--dry-run' - - for (const version of canaryVersions) { - console.log(`Unpublishing ${packageName}@${version}...`) - try { - execSync( - `npm unpublish ${packageName}@${version} ${dryRunOption} --force` - ) - } catch (error) { - console.error( - `Unpublish Error ${packageName}@${canaryVersions}:`, - `${error}` - ) - } - } - }) - } catch (error) { - console.error(`Error: ${error}`) - } -} - -main() diff --git a/tasks/update-package-versions b/tasks/update-package-versions deleted file mode 100755 index 3ea10c4f183c..000000000000 --- a/tasks/update-package-versions +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env node -/* eslint-env node */ - -const child = require('child_process') -const path = require('path') - -const fs = require('fs-extra') - -async function run() { - const version = process.argv[2].replace(/v/, '') - - if (!version) { - console.error( - 'You have to provide a version.\nUsage ./update-package-versions ' - ) - process.exitCode = 1 - return - } - - const cwd = path.join(__dirname, '../') - - const cmd = [ - 'yarn lerna version', - version, - '--force-publish', - '--no-push', - '--no-git-tag-version', - '--exact', - '--yes', - ].join(' ') - - console.log(`Running "${cmd}"`) - console.log() - child.execSync(cmd, { - cwd, - }) - console.log() - - // Updates create-redwood-app template - console.log('Updating CRWA template...') - const tsTemplatePath = path.join( - cwd, - 'packages/create-redwood-app/templates/ts' - ) - updateRWJSPkgsVersion(tsTemplatePath, version) - updateRWJSPkgsVersion(path.join(tsTemplatePath, 'api'), version) - updateRWJSPkgsVersion(path.join(tsTemplatePath, 'web'), version) - console.log() - - const jsTemplatePath = path.join( - cwd, - 'packages/create-redwood-app/templates/js' - ) - updateRWJSPkgsVersion(jsTemplatePath, version) - updateRWJSPkgsVersion(path.join(jsTemplatePath, 'api'), version) - updateRWJSPkgsVersion(path.join(jsTemplatePath, 'web'), version) - console.log() - - // Updates __fixtures__/test-project packages - console.log('Updating test-project fixture...') - const fixturePath = path.join(cwd, '__fixtures__/test-project') - updateRWJSPkgsVersion(fixturePath, version) - updateRWJSPkgsVersion(path.join(fixturePath, 'api'), version) - updateRWJSPkgsVersion(path.join(fixturePath, 'web'), version) - console.log() -} - -/** - * Iterates over `@redwoodjs/*` dependencies in a package.json and updates their version. - * - * @param {string} pkgPath - * @param {string} version - */ -function updateRWJSPkgsVersion(pkgPath, version) { - const pkg = fs.readJSONSync(path.join(pkgPath, 'package.json'), 'utf-8') - - for (const dep of Object.keys(pkg.dependencies ?? {}).filter(isRWJSPkg)) { - console.log(` - ${dep}: ${pkg.dependencies[dep]} => ${version}`) - pkg.dependencies[dep] = `${version}` - } - - for (const dep of Object.keys(pkg.devDependencies ?? {}).filter(isRWJSPkg)) { - console.log(` - ${dep}: ${pkg.devDependencies[dep]} => ${version}`) - pkg.devDependencies[dep] = `${version}` - } - - fs.writeJSONSync(path.join(pkgPath, 'package.json'), pkg, { spaces: 2 }) -} - -const isRWJSPkg = (pkg) => pkg.startsWith('@redwoodjs/') - -run() From 49379294ce7b7332619bda886785435b8c08e400 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Wed, 6 Dec 2023 08:22:47 +0100 Subject: [PATCH 08/12] RSC: Remove unused code. Improve code organization (#9631) --- packages/vite/package.json | 4 +- packages/vite/src/rsc/rscBuildAnalyze.ts | 3 +- packages/vite/src/rsc/rscBuildClient.ts | 3 +- .../node-loader.ts => rsc/rscNodeLoader.ts} | 6 +- .../rscVitePlugins.ts} | 14 +- packages/vite/src/rsc/rscWorker.ts | 193 ++++-------------- packages/vite/src/waku-lib/builder.ts | 191 ----------------- packages/vite/src/waku-lib/config.ts | 41 ---- packages/vite/src/waku-lib/rsc-utils.ts | 84 -------- 9 files changed, 64 insertions(+), 475 deletions(-) rename packages/vite/src/{waku-lib/node-loader.ts => rsc/rscNodeLoader.ts} (90%) rename packages/vite/src/{waku-lib/vite-plugin-rsc.ts => rsc/rscVitePlugins.ts} (93%) delete mode 100644 packages/vite/src/waku-lib/builder.ts delete mode 100644 packages/vite/src/waku-lib/config.ts delete mode 100644 packages/vite/src/waku-lib/rsc-utils.ts diff --git a/packages/vite/package.json b/packages/vite/package.json index 6b192e1448a8..8d7427be94e3 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -39,8 +39,8 @@ "default": "./dist/buildFeServer.js" }, "./node-loader": { - "types": "./dist/waku-lib/node-loader.d.ts", - "default": "./dist/waku-lib/node-loader.js" + "types": "./dist/rsc/rscNodeLoader.d.ts", + "default": "./dist/rsc/rscNodeLoader.js" }, "./react-node-loader": { "types": "./dist/react-server-dom-webpack/node-loader.d.ts", diff --git a/packages/vite/src/rsc/rscBuildAnalyze.ts b/packages/vite/src/rsc/rscBuildAnalyze.ts index bda200a0ff0e..7bc4b364ee40 100644 --- a/packages/vite/src/rsc/rscBuildAnalyze.ts +++ b/packages/vite/src/rsc/rscBuildAnalyze.ts @@ -4,7 +4,8 @@ import { build as viteBuild } from 'vite' import { getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn' -import { rscAnalyzePlugin } from '../waku-lib/vite-plugin-rsc' + +import { rscAnalyzePlugin } from './rscVitePlugins' /** * RSC build. Step 1. diff --git a/packages/vite/src/rsc/rscBuildClient.ts b/packages/vite/src/rsc/rscBuildClient.ts index 415906ec178c..e119cc790edc 100644 --- a/packages/vite/src/rsc/rscBuildClient.ts +++ b/packages/vite/src/rsc/rscBuildClient.ts @@ -6,7 +6,8 @@ import { build as viteBuild } from 'vite' import { getConfig, getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn' -import { rscIndexPlugin } from '../waku-lib/vite-plugin-rsc' + +import { rscIndexPlugin } from './rscVitePlugins' /** * RSC build. Step 2. diff --git a/packages/vite/src/waku-lib/node-loader.ts b/packages/vite/src/rsc/rscNodeLoader.ts similarity index 90% rename from packages/vite/src/waku-lib/node-loader.ts rename to packages/vite/src/rsc/rscNodeLoader.ts index 9be77626fc64..4f4655c0e3bf 100644 --- a/packages/vite/src/waku-lib/node-loader.ts +++ b/packages/vite/src/rsc/rscNodeLoader.ts @@ -14,10 +14,12 @@ // Error: Expected source to have been loaded into a string. // at load (/Users/tobbe/tmp/rw-rsc-esm/node_modules/@redwoodjs/vite/dist/react-server-dom-webpack/node-loader.js:357:13) // at async nextLoad (node:internal/modules/esm/loader:163:22) +// +// TODO (RSC): Verify that the above is still true +// +// This loader is exported in package.json and used with the rsc worker export async function load(url: string, context: any, nextLoad: any) { - // console.log('waku-lib/node-loader: load', context.format, url) - const result = await nextLoad(url, context, nextLoad) if (result.format === 'module') { diff --git a/packages/vite/src/waku-lib/vite-plugin-rsc.ts b/packages/vite/src/rsc/rscVitePlugins.ts similarity index 93% rename from packages/vite/src/waku-lib/vite-plugin-rsc.ts rename to packages/vite/src/rsc/rscVitePlugins.ts index 8ecf79c3561a..b2bdd5bf920c 100644 --- a/packages/vite/src/waku-lib/vite-plugin-rsc.ts +++ b/packages/vite/src/rsc/rscVitePlugins.ts @@ -1,4 +1,3 @@ -// TODO (RSC) Take ownership of this file and move it out ouf the waku-lib folder import path from 'node:path' import * as swc from '@swc/core' @@ -7,10 +6,19 @@ import type { Plugin } from 'vite' import * as RSDWNodeLoader from '../react-server-dom-webpack/node-loader' import type { ResolveFunction } from '../react-server-dom-webpack/node-loader' -import { codeToInject } from './rsc-utils.js' - // Used in Step 2 of the build process, for the client bundle export function rscIndexPlugin(): Plugin { + const codeToInject = ` + globalThis.__rw_module_cache__ = new Map(); + + globalThis.__webpack_chunk_load__ = (id) => { + return import(id).then((m) => globalThis.__rw_module_cache__.set(id, m)) + }; + + globalThis.__webpack_require__ = (id) => { + return globalThis.__rw_module_cache__.get(id) + };\n ` + return { name: 'rsc-index-plugin', async transformIndexHtml() { diff --git a/packages/vite/src/rsc/rscWorker.ts b/packages/vite/src/rsc/rscWorker.ts index b6c5e32b301b..ddd5a9f6dce0 100644 --- a/packages/vite/src/rsc/rscWorker.ts +++ b/packages/vite/src/rsc/rscWorker.ts @@ -3,32 +3,31 @@ // `--condition react-server`. If we did try to do that the main process // couldn't do SSR because it would be missing client-side React functions // like `useState` and `createContext`. + +import { Buffer } from 'node:buffer' import path from 'node:path' -import { Writable } from 'node:stream' +import { Transform, Writable } from 'node:stream' import { parentPort } from 'node:worker_threads' import { createElement } from 'react' import RSDWServer from 'react-server-dom-webpack/server' -import { createServer } from 'vite' +import type { ResolvedConfig } from 'vite' +import { createServer, resolveConfig } from 'vite' import { getPaths } from '@redwoodjs/project-config' import type { defineEntries } from '../entries' import { StatusError } from '../lib/StatusError' -import { configFileConfig, resolveConfig } from '../waku-lib/config' -import { transformRsfId } from '../waku-lib/rsc-utils' -import { - rscTransformPlugin, - rscReloadPlugin, -} from '../waku-lib/vite-plugin-rsc' -// import type { unstable_GetCustomModules } from '../waku-server' +import { rscTransformPlugin, rscReloadPlugin } from './rscVitePlugins' import type { RenderInput, MessageRes, MessageReq, } from './rscWorkerCommunication' + +// import type { unstable_GetCustomModules } from '../waku-server' // import type { RenderInput, MessageReq, MessageRes } from './rsc-handler' // import { transformRsfId, generatePrefetchCode } from './rsc-utils' @@ -153,7 +152,6 @@ const handleRender = async ({ id, input }: MessageReq & { type: 'render' }) => { // } const vitePromise = createServer({ - ...configFileConfig, plugins: [ rscTransformPlugin(), rscReloadPlugin((type) => { @@ -208,7 +206,9 @@ parentPort.on('message', (message: MessageReq) => { } }) -const configPromise = resolveConfig('serve') +// Let me re-assign root +type ConfigType = Omit & { root: string } +const configPromise: Promise = resolveConfig({}, 'serve') const getEntriesFile = async ( config: Awaited>, @@ -217,11 +217,9 @@ const getEntriesFile = async ( const rwPaths = getPaths() if (isBuild) { - return path.join( - config.root, - config.build.outDir, - config.framework.entriesJs - ) + // TODO (RSC): Should we make this path configurable? Or at least read + // from getPaths()? + return path.join(config.root, config.build.outDir, 'entries.js') } return rwPaths.web.distServerEntries @@ -386,137 +384,32 @@ async function renderRsc(input: RenderInput): Promise { throw new Error('Unexpected input') } -// async function getCustomModulesRSC(): Promise<{ [name: string]: string }> { -// const config = await configPromise -// const entriesFile = await getEntriesFile(config, false) -// const { -// default: { unstable_getCustomModules: getCustomModules }, -// } = await (loadServerFile(entriesFile) as Promise<{ -// default: Entries['default'] & { -// unstable_getCustomModules?: unstable_GetCustomModules -// } -// }>) -// if (!getCustomModules) { -// return {} -// } -// const modules = await getCustomModules() -// return modules -// } - -// // FIXME this may take too much responsibility -// async function buildRSC(): Promise { -// const config = await resolveConfig('build') -// const basePath = config.base + config.framework.rscPrefix -// const distEntriesFile = await getEntriesFile(config, true) -// const { -// default: { getBuilder }, -// } = await (loadServerFile(distEntriesFile) as Promise) -// if (!getBuilder) { -// console.warn( -// "getBuilder is undefined. It's recommended for optimization and sometimes required." -// ) -// return -// } - -// // FIXME this doesn't seem an ideal solution -// const decodeId = (encodedId: string): [id: string, name: string] => { -// const [filePath, name] = encodedId.split('#') as [string, string] -// const id = resolveClientEntry(config, filePath) -// return [id, name] -// } - -// const pathMap = await getBuilder(decodeId) -// const clientModuleMap = new Map>() -// const addClientModule = (pathStr: string, id: string) => { -// let idSet = clientModuleMap.get(pathStr) -// if (!idSet) { -// idSet = new Set() -// clientModuleMap.set(pathStr, idSet) -// } -// idSet.add(id) -// } -// await Promise.all( -// Object.entries(pathMap).map(async ([pathStr, { elements }]) => { -// for (const [rscId, props] of elements || []) { -// // FIXME we blindly expect JSON.stringify usage is deterministic -// const serializedProps = JSON.stringify(props) -// const searchParams = new URLSearchParams() -// searchParams.set('props', serializedProps) -// const destFile = path.join( -// config.root, -// config.build.outDir, -// config.framework.outPublic, -// config.framework.rscPrefix, -// decodeURIComponent(rscId), -// decodeURIComponent(`${searchParams}`) -// ) -// fs.mkdirSync(path.dirname(destFile), { recursive: true }) -// const bundlerConfig = new Proxy( -// {}, -// { -// get(_target, encodedId: string) { -// const [id, name] = decodeId(encodedId) -// addClientModule(pathStr, id) -// return { id, chunks: [id], name, async: true } -// }, -// } -// ) -// const component = await getFunctionComponent(rscId, config, true) -// const pipeable = renderToPipeableStream( -// createElement(component, props as any), -// bundlerConfig -// ).pipe(transformRsfId(path.join(config.root, config.build.outDir))) -// await new Promise((resolve, reject) => { -// const stream = fs.createWriteStream(destFile) -// stream.on('finish', resolve) -// stream.on('error', reject) -// pipeable.pipe(stream) -// }) -// } -// }) -// ) - -// const publicIndexHtmlFile = path.join( -// config.root, -// config.build.outDir, -// config.framework.outPublic, -// config.framework.indexHtml -// ) -// const publicIndexHtml = fs.readFileSync(publicIndexHtmlFile, { -// encoding: 'utf8', -// }) -// await Promise.all( -// Object.entries(pathMap).map(async ([pathStr, { elements, customCode }]) => { -// const destFile = path.join( -// config.root, -// config.build.outDir, -// config.framework.outPublic, -// pathStr, -// pathStr.endsWith('/') ? 'index.html' : '' -// ) -// let data = '' -// if (fs.existsSync(destFile)) { -// data = fs.readFileSync(destFile, { encoding: 'utf8' }) -// } else { -// fs.mkdirSync(path.dirname(destFile), { recursive: true }) -// data = publicIndexHtml -// } -// const code = -// generatePrefetchCode( -// basePath, -// Array.from(elements || []).flatMap(([rscId, props, skipPrefetch]) => { -// if (skipPrefetch) { -// return [] -// } -// return [[rscId, props]] -// }), -// clientModuleMap.get(pathStr) || [] -// ) + (customCode || '') -// if (code) { -// // HACK is this too naive to inject script code? -// data = data.replace(/<\/body>/, ``) -// } -// fs.writeFileSync(destFile, data, { encoding: 'utf8' }) -// }) -// ) -// } +// HACK Patching stream is very fragile. +// TODO (RSC): Sanitize prefixToRemove to make sure it's safe to use in a +// RegExp (CodeQL is complaining on GitHub) +function transformRsfId(prefixToRemove: string) { + // Should be something like /home/runner/work/redwood/test-project-rsa + console.log('prefixToRemove', prefixToRemove) + + return new Transform({ + transform(chunk, encoding, callback) { + if (encoding !== ('buffer' as any)) { + throw new Error('Unknown encoding') + } + const data = chunk.toString() + const lines = data.split('\n') + console.log('lines', lines) + let changed = false + for (let i = 0; i < lines.length; ++i) { + const match = lines[i].match( + new RegExp(`^([0-9]+):{"id":"${prefixToRemove}(.*?)"(.*)$`) + ) + if (match) { + lines[i] = `${match[1]}:{"id":"${match[2]}"${match[3]}` + changed = true + } + } + callback(null, changed ? Buffer.from(lines.join('\n')) : chunk) + }, + }) +} diff --git a/packages/vite/src/waku-lib/builder.ts b/packages/vite/src/waku-lib/builder.ts deleted file mode 100644 index 1ab154b604ae..000000000000 --- a/packages/vite/src/waku-lib/builder.ts +++ /dev/null @@ -1,191 +0,0 @@ -// TODO (RSC) Take ownership of this file and move it out ouf the waku-lib folder -import fs from 'node:fs' -import { createRequire } from 'node:module' -import path from 'node:path' - -import react from '@vitejs/plugin-react' -import { build as viteBuild } from 'vite' - -import { onWarn } from '../lib/onWarn' -import { - shutdown, - setClientEntries, - getCustomModulesRSC, - buildRSC, -} from '../rsc/rscWorkerCommunication' - -import { configFileConfig, resolveConfig } from './config' -import { rscIndexPlugin, rscAnalyzePlugin } from './vite-plugin-rsc' - -export async function build() { - const config = await resolveConfig('build') - const indexHtmlFile = path.join(config.root, config.framework.indexHtml) - const distEntriesFile = path.join( - config.root, - config.build.outDir, - config.framework.entriesJs - ) - let entriesFile = path.join(config.root, config.framework.entriesJs) - if (entriesFile.endsWith('.js')) { - for (const ext of ['.js', '.ts', '.tsx', '.jsx']) { - const tmp = entriesFile.slice(0, -3) + ext - if (fs.existsSync(tmp)) { - entriesFile = tmp - break - } - } - } - const require = createRequire(import.meta.url) - - const customModules = await getCustomModulesRSC() - const clientEntryFileSet = new Set() - const serverEntryFileSet = new Set() - await viteBuild({ - ...configFileConfig, - plugins: [ - rscAnalyzePlugin( - (id) => clientEntryFileSet.add(id), - (id) => serverEntryFileSet.add(id) - ), - ], - ssr: { - // TODO (RSC): Is this still relevant? - // FIXME Without this, waku/router isn't considered to have client - // entries, and "No client entry" error occurs. - // Unless we fix this, RSC-capable packages aren't supported. - // This also seems to cause problems with pnpm. - noExternal: ['waku'], - }, - build: { - write: false, - ssr: true, - rollupOptions: { - onwarn: onWarn, - input: { - entries: entriesFile, - ...customModules, - }, - }, - }, - }) - const clientEntryFiles = Object.fromEntries( - Array.from(clientEntryFileSet).map((fname, i) => [`rsc${i}`, fname]) - ) - const serverEntryFiles = Object.fromEntries( - Array.from(serverEntryFileSet).map((fname, i) => [`rsf${i}`, fname]) - ) - - const serverBuildOutput = await viteBuild({ - ...configFileConfig, - ssr: { - noExternal: Array.from(clientEntryFileSet).map( - // FIXME this might not work with pnpm - (fname) => - path - .relative(path.join(config.root, 'node_modules'), fname) - .split('/')[0] - ), - }, - build: { - ssr: true, - rollupOptions: { - onwarn: onWarn, - input: { - entries: entriesFile, - ...clientEntryFiles, - ...serverEntryFiles, - ...customModules, - }, - output: { - banner: (chunk) => { - // HACK to bring directives to the front - let code = '' - if (chunk.moduleIds.some((id) => clientEntryFileSet.has(id))) { - code += '"use client";' - } - if (chunk.moduleIds.some((id) => serverEntryFileSet.has(id))) { - code += '"use server";' - } - return code - }, - entryFileNames: (chunkInfo) => { - if (chunkInfo.name === 'entries' || customModules[chunkInfo.name]) { - return '[name].js' - } - return 'assets/[name].js' - }, - }, - }, - }, - }) - if (!('output' in serverBuildOutput)) { - throw new Error('Unexpected vite server build output') - } - - const clientBuildOutput = await viteBuild({ - ...configFileConfig, - plugins: [react(), rscIndexPlugin()], - build: { - outDir: path.join(config.build.outDir, config.framework.outPublic), - rollupOptions: { - onwarn: onWarn, - input: { - main: indexHtmlFile, - ...clientEntryFiles, - }, - preserveEntrySignatures: 'exports-only', - }, - }, - }) - if (!('output' in clientBuildOutput)) { - throw new Error('Unexpected vite client build output') - } - - const clientEntries: Record = {} - for (const item of clientBuildOutput.output) { - const { name, fileName } = item - const entryFile = - name && - serverBuildOutput.output.find( - (item) => - 'moduleIds' in item && - item.moduleIds.includes(clientEntryFiles[name] as string) - )?.fileName - if (entryFile) { - clientEntries[entryFile] = fileName - } - } - console.log('clientEntries', clientEntries) - fs.appendFileSync( - distEntriesFile, - `export const clientEntries=${JSON.stringify(clientEntries)};` - ) - - const absoluteClientEntries = Object.fromEntries( - Object.entries(clientEntries).map(([key, val]) => [ - path.join(path.dirname(entriesFile), config.build.outDir, key), - config.base + val, - ]) - ) - await setClientEntries(absoluteClientEntries) - - await buildRSC() - - const origPackageJson = require(path.join(config.root, 'package.json')) - const packageJson = { - name: origPackageJson.name, - version: origPackageJson.version, - private: true, - type: 'module', - scripts: { - start: 'waku start', - }, - dependencies: origPackageJson.dependencies, - } - fs.writeFileSync( - path.join(config.root, config.build.outDir, 'package.json'), - JSON.stringify(packageJson, null, 2) - ) - - await shutdown() -} diff --git a/packages/vite/src/waku-lib/config.ts b/packages/vite/src/waku-lib/config.ts deleted file mode 100644 index 386ee6b1e91d..000000000000 --- a/packages/vite/src/waku-lib/config.ts +++ /dev/null @@ -1,41 +0,0 @@ -// TODO (RSC) Take ownership of this file and move it out ouf the waku-lib folder -import type { ConfigEnv, UserConfig } from 'vite' -import { resolveConfig as viteResolveConfig } from 'vite' - -export interface FrameworkConfig { - indexHtml?: string // relative to root - entriesJs?: string // relative to root - outPublic?: string // relative to build.outDir - rscPrefix?: string // defaults to "RSC/" -} - -export interface ExtendedUserConfig extends UserConfig { - framework?: FrameworkConfig -} - -export function defineConfig( - config: - | ExtendedUserConfig - | Promise - | ((env: ConfigEnv) => ExtendedUserConfig) - | ((env: ConfigEnv) => Promise) -) { - return config -} - -export const configFileConfig = process.env.CONFIG_FILE - ? { configFile: process.env.CONFIG_FILE } - : {} - -export async function resolveConfig(command: 'build' | 'serve') { - const origConfig = await viteResolveConfig(configFileConfig, command) - const framework: Required = { - indexHtml: 'index.html', - entriesJs: 'entries.js', - outPublic: 'public', - rscPrefix: 'RSC/', - ...(origConfig as { framework?: FrameworkConfig }).framework, - } - const config = { ...origConfig, framework } - return config -} diff --git a/packages/vite/src/waku-lib/rsc-utils.ts b/packages/vite/src/waku-lib/rsc-utils.ts deleted file mode 100644 index 9dfc1f98fb1a..000000000000 --- a/packages/vite/src/waku-lib/rsc-utils.ts +++ /dev/null @@ -1,84 +0,0 @@ -// TODO (RSC) Take ownership of this file and move it out ouf the waku-lib folder -import { Buffer } from 'node:buffer' -import { Transform } from 'node:stream' - -export const codeToInject = ` - globalThis.__rw_module_cache__ = new Map(); - - globalThis.__webpack_chunk_load__ = (id) => { - return import(id).then((m) => globalThis.__rw_module_cache__.set(id, m)) - }; - - globalThis.__webpack_require__ = (id) => { - return globalThis.__rw_module_cache__.get(id) - };\n ` - -export const generatePrefetchCode = ( - basePath: string, - entryItemsIterable: Iterable, - moduleIds: Iterable -) => { - const entryItems = Array.from(entryItemsIterable) - let code = '' - if (entryItems.length) { - const rscIds = Array.from(new Set(entryItems.map(([rscId]) => rscId))) - code += ` -globalThis.__WAKU_PREFETCHED__ = { -${rscIds - .map((rscId) => { - const value = - '{' + - entryItems - .flatMap(([id, props]) => { - if (id !== rscId) { - return [] - } - // FIXME we blindly expect JSON.stringify usage is deterministic - const serializedProps = JSON.stringify(props) - const searchParams = new URLSearchParams() - searchParams.set('props', serializedProps) - return [ - `'${serializedProps}': fetch('${basePath}${rscId}/${searchParams}')`, - ] - }) - .join(',') + - '}' - return ` '${rscId}': ${value}` - }) - .join(',\n')} -};` - } - for (const moduleId of moduleIds) { - code += ` -import('${moduleId}');` - } - return code -} - -// HACK Patching stream is very fragile. -export const transformRsfId = (prefixToRemove: string) => { - // Should be something like /home/runner/work/redwood/test-project-rsa - console.log('prefixToRemove', prefixToRemove) - - return new Transform({ - transform(chunk, encoding, callback) { - if (encoding !== ('buffer' as any)) { - throw new Error('Unknown encoding') - } - const data = chunk.toString() - const lines = data.split('\n') - console.log('lines', lines) - let changed = false - for (let i = 0; i < lines.length; ++i) { - const match = lines[i].match( - new RegExp(`^([0-9]+):{"id":"${prefixToRemove}(.*?)"(.*)$`) - ) - if (match) { - lines[i] = `${match[1]}:{"id":"${match[2]}"${match[3]}` - changed = true - } - } - callback(null, changed ? Buffer.from(lines.join('\n')) : chunk) - }, - }) -} From 2fdb496e70ba963590a9fbedeb32e1047279634b Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Wed, 6 Dec 2023 12:15:41 +0100 Subject: [PATCH 09/12] RSC: No need to patch Vite anymore (#9636) --- .../commands/experimental/setupRscHandler.js | 41 ------------------- .../vite-npm-4.4.9-e845c1bbf8.patch.template | 19 --------- 2 files changed, 60 deletions(-) delete mode 100644 packages/cli/src/commands/experimental/templates/rsc/vite-npm-4.4.9-e845c1bbf8.patch.template diff --git a/packages/cli/src/commands/experimental/setupRscHandler.js b/packages/cli/src/commands/experimental/setupRscHandler.js index 8ecf0e1b3ffe..bea2f5f431c6 100644 --- a/packages/cli/src/commands/experimental/setupRscHandler.js +++ b/packages/cli/src/commands/experimental/setupRscHandler.js @@ -1,7 +1,6 @@ import fs from 'fs' import path from 'path' -import execa from 'execa' import { Listr } from 'listr2' import { prettify } from '@redwoodjs/cli-helpers' @@ -350,46 +349,6 @@ export const handler = async ({ force, verbose }) => { }) }, }, - { - title: 'Patch vite', - task: async () => { - const vitePatchTemplate = fs.readFileSync( - path.resolve( - __dirname, - 'templates', - 'rsc', - 'vite-npm-4.4.9-e845c1bbf8.patch.template' - ), - 'utf-8' - ) - - const yarnPatchDir = path.join(rwPaths.base, '.yarn', 'patches') - const vitePatchPath = path.join( - yarnPatchDir, - 'vite-npm-4.4.9-e845c1bbf8.patch' - ) - writeFile(vitePatchPath, vitePatchTemplate, { - overwriteExisting: force, - }) - - const packageJsonPath = path.join(rwPaths.base, 'package.json') - const packageJson = JSON.parse( - fs.readFileSync(packageJsonPath, 'utf-8') - ) - packageJson.resolutions = packageJson.resolutions || {} - packageJson.resolutions['vite@4.4.9'] = - 'patch:vite@npm%3A4.4.9#./.yarn/patches/vite-npm-4.4.9-e845c1bbf8.patch' - writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), { - overwriteExisting: true, - }) - - await execa('yarn install', { - stdio: 'ignore', - shell: true, - cwd: rwPaths.base, - }) - }, - }, { task: () => { printTaskEpilogue(command, description, EXPERIMENTAL_TOPIC_ID) diff --git a/packages/cli/src/commands/experimental/templates/rsc/vite-npm-4.4.9-e845c1bbf8.patch.template b/packages/cli/src/commands/experimental/templates/rsc/vite-npm-4.4.9-e845c1bbf8.patch.template deleted file mode 100644 index eb67d7906284..000000000000 --- a/packages/cli/src/commands/experimental/templates/rsc/vite-npm-4.4.9-e845c1bbf8.patch.template +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/dist/node/chunks/dep-df561101.js b/dist/node/chunks/dep-df561101.js -index 1bc8674177fe73120171b22436e6104713c5d764..f0fee7b385868cb01c6d47b80d7f64a7368c0412 100644 ---- a/dist/node/chunks/dep-df561101.js -+++ b/dist/node/chunks/dep-df561101.js -@@ -55890,12 +55890,12 @@ async function instantiateModule(url, server, context = { global }, urlStack = [ - }; - urlStack = urlStack.concat(url); - const isCircular = (url) => urlStack.includes(url); -- const { isProduction, resolve: { dedupe, preserveSymlinks }, root, } = server.config; -+ const { isProduction, resolve: { dedupe, preserveSymlinks, conditions }, root, } = server.config; - const resolveOptions = { - mainFields: ['main'], - browserField: true, - conditions: [], -- overrideConditions: ['production', 'development'], -+ overrideConditions: [...conditions, 'production', 'development'], - extensions: ['.js', '.cjs', '.json'], - dedupe, - preserveSymlinks, From 199f9533caac66d5388223c8f0df2043797a062c Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 6 Dec 2023 11:51:15 -0800 Subject: [PATCH 10/12] chore(release): handle OTP for lerna publish --- docs/.node-version | 2 +- package.json | 1 + tasks/release/release.mjs | 23 +++++++---------------- yarn.lock | 1 + 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/docs/.node-version b/docs/.node-version index 3c032078a4a2..87ec8842b158 100644 --- a/docs/.node-version +++ b/docs/.node-version @@ -1 +1 @@ -18 +18.18.2 diff --git a/package.json b/package.json index cc312fab45de..bb251f4c538a 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "dependency-cruiser": "13.1.5", "dotenv": "16.3.1", "eslint": "8.55.0", + "execa": "5.1.1", "fast-glob": "3.3.2", "fs-extra": "11.2.0", "jest": "29.7.0", diff --git a/tasks/release/release.mjs b/tasks/release/release.mjs index cde15b06a471..030565038400 100644 --- a/tasks/release/release.mjs +++ b/tasks/release/release.mjs @@ -3,8 +3,9 @@ import { fileURLToPath } from 'node:url' import { parseArgs as _parseArgs } from 'node:util' +import execa from 'execa' import semverPackage from 'semver' -import { cd, chalk, fs, path, question, within, $ } from 'zx' +import { cd, chalk, fs, path, question, $ } from 'zx' import { branchExists, @@ -392,10 +393,7 @@ async function releaseMajorOrMinor() { // Publish. try { - await within(async () => { - $.verbose = true - await $`yarn lerna publish from-package` - }) + await execa.command('yarn lerna publish from-package', { stdio: 'inherit' }) } catch { exitIfNo( await question( @@ -410,10 +408,7 @@ async function releaseMajorOrMinor() { await updateCreateRedwoodAppTemplates() console.log() try { - await within(async () => { - $.verbose = true - await $`yarn lerna publish from-package` - }) + await execa.command('yarn lerna publish from-package', { stdio: 'inherit' }) } catch { exitIfNo( await question( @@ -668,9 +663,8 @@ async function releasePatch() { // Publish. try { - await within(async () => { - $.verbose = true - await $`yarn lerna publish from-package` + await execa.command('yarn lerna publish from-package', { + stdio: 'inherit', }) } catch { exitIfNo( @@ -686,10 +680,7 @@ async function releasePatch() { await updateCreateRedwoodAppTemplates() console.log() try { - await within(async () => { - $.verbose = true - await $`yarn lerna publish from-package` - }) + await execa.command('yarn lerna publish from-package', { stdio: 'inherit' }) } catch { exitIfNo( await question( diff --git a/yarn.lock b/yarn.lock index 734d016e8714..3388ba398c3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31402,6 +31402,7 @@ __metadata: dependency-cruiser: 13.1.5 dotenv: 16.3.1 eslint: 8.55.0 + execa: 5.1.1 fast-glob: 3.3.2 fs-extra: 11.2.0 jest: 29.7.0 From ab335d93459f791036de523227ee929b0ee8d097 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 6 Dec 2023 12:37:34 -0800 Subject: [PATCH 11/12] chore(release): configure aloglia to index docs --- docs/netlify.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/netlify.toml b/docs/netlify.toml index d452601fd085..63dafd244adf 100644 --- a/docs/netlify.toml +++ b/docs/netlify.toml @@ -279,3 +279,9 @@ from = "/docs/3.2/*" to = "/docs/3.x/:splat" status = 301 + +[[plugins]] + package = "@algolia/netlify-plugin-crawler" + + [plugins.inputs] + branches = ['next'] From 3ca1f03a5437c41aaea1d0b71711215eb3e5bc91 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 6 Dec 2023 23:57:03 -0800 Subject: [PATCH 12/12] fix(CLI): merge NODE_OPTIONS in `yarn rw dev` (#9585) --- packages/cli/src/commands/devHandler.js | 24 ++++++++++- .../lib/__tests__/getDevNodeOptions.test.js | 27 +++++++++++++ tasks/release/release.mjs | 40 +++++++++---------- 3 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 packages/cli/src/lib/__tests__/getDevNodeOptions.test.js diff --git a/packages/cli/src/commands/devHandler.js b/packages/cli/src/commands/devHandler.js index 964143794423..6ba3e6858659 100644 --- a/packages/cli/src/commands/devHandler.js +++ b/packages/cli/src/commands/devHandler.js @@ -178,7 +178,7 @@ export const handler = async ({ const jobs = { api: { name: 'api', - command: `yarn cross-env NODE_ENV=development NODE_OPTIONS=--enable-source-maps yarn nodemon --quiet --watch "${redwoodConfigPath}" --exec "yarn rw-api-server-watch --port ${apiAvailablePort} ${getApiDebugFlag()} | rw-log-formatter"`, + command: `yarn cross-env NODE_ENV=development ${getDevNodeOptions()} yarn nodemon --quiet --watch "${redwoodConfigPath}" --exec "yarn rw-api-server-watch --port ${apiAvailablePort} ${getApiDebugFlag()} | rw-log-formatter"`, prefixColor: 'cyan', runWhen: () => fs.existsSync(rwjsPaths.api.src), }, @@ -222,3 +222,25 @@ export const handler = async ({ } }) } + +/** + * Gets the NODE_OPTIONS environment variable from `process.env`, appending `--enable-source-maps` if it's not already there. + * See https://nodejs.org/api/cli.html#node_optionsoptions. + * + * @returns {string} + */ +export function getDevNodeOptions() { + const { NODE_OPTIONS } = process.env + + const enableSourceMapsOption = '--enable-source-maps' + + if (!NODE_OPTIONS) { + return `NODE_OPTIONS=${enableSourceMapsOption}` + } + + if (NODE_OPTIONS.includes(enableSourceMapsOption)) { + return NODE_OPTIONS + } + + return `${NODE_OPTIONS} ${enableSourceMapsOption}` +} diff --git a/packages/cli/src/lib/__tests__/getDevNodeOptions.test.js b/packages/cli/src/lib/__tests__/getDevNodeOptions.test.js new file mode 100644 index 000000000000..cc34d471b9d7 --- /dev/null +++ b/packages/cli/src/lib/__tests__/getDevNodeOptions.test.js @@ -0,0 +1,27 @@ +import { getDevNodeOptions } from '../../commands/devHandler' + +describe('getNodeOptions', () => { + const enableSourceMapsOption = '--enable-source-maps' + + it('defaults to enable-source-maps', () => { + const nodeOptions = getDevNodeOptions() + expect(nodeOptions).toEqual(`NODE_OPTIONS=${enableSourceMapsOption}`) + }) + + it("doesn't specify `--enable-source-maps` twice", () => { + process.env.NODE_OPTIONS = `NODE_OPTIONS=${enableSourceMapsOption}` + + const nodeOptions = getDevNodeOptions() + expect(nodeOptions).toEqual(`NODE_OPTIONS=${enableSourceMapsOption}`) + }) + + it('merges existing options with `--enable-source-maps`', () => { + const existingOptions = '--inspect --no-experimental-fetch' + process.env.NODE_OPTIONS = `NODE_OPTIONS=${existingOptions}` + + const nodeOptions = getDevNodeOptions() + expect(nodeOptions).toEqual( + `NODE_OPTIONS=${existingOptions} ${enableSourceMapsOption}` + ) + }) +}) diff --git a/tasks/release/release.mjs b/tasks/release/release.mjs index 030565038400..3be51a42d06a 100644 --- a/tasks/release/release.mjs +++ b/tasks/release/release.mjs @@ -212,12 +212,28 @@ async function resolveMilestones() { } // Depending on if we're releasing a patch or not, there's a few things we need to check. + const { + search: { nodes: prs }, + } = await octokit.graphql(` + { + search( + query: "repo:redwoodjs/redwood is:pr is:merged milestone:next-release-patch" + first: 5 + type: ISSUE + ) { + nodes { + ... on PullRequest { + id + } + } + } + } + `) + if (semver === 'patch') { console.log() console.log( - `Since we're releasing a ${chalk.magenta( - 'patch' - )}, we'll be releasing all the PRs that have the ${chalk.magenta( + `There's ${prs.length} PR(s) that have the ${chalk.magenta( 'next-release-patch' )} milestone.` ) @@ -238,24 +254,6 @@ async function resolveMilestones() { ) } } else { - const { - search: { nodes: prs }, - } = await octokit.graphql(` - { - search( - query: "repo:redwoodjs/redwood is:pr is:merged milestone:next-release-patch" - first: 5 - type: ISSUE - ) { - nodes { - ... on PullRequest { - id - } - } - } - } - `) - if (prs.length) { console.log() console.log(