Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSC: Add RSC+SSR smoke test to CI #10477

Merged
merged 17 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion __fixtures__/test-project-rsa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"packageManager": "yarn@4.1.1",
"resolutions": {
"@apollo/client-react-streaming/superjson": "^1.12.2",
"@apollo/client/rehackt": "0.0.0-pr.10.0"
"@apollo/client/rehackt": "0.0.0-pr.10.0",
"react-is": "19.0.0-canary-cb151849e1-20240424"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"packageManager": "yarn@4.1.1",
"resolutions": {
"@apollo/client-react-streaming/superjson": "^1.12.2",
"@apollo/client/rehackt": "0.0.0-pr.10.0"
"@apollo/client/rehackt": "0.0.0-pr.10.0",
"react-is": "19.0.0-canary-cb151849e1-20240424"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// In this file, all Page components from 'src/pages` are auto-imported. Nested
// directories are supported, and should be uppercase. Each subdirectory will be
// prepended onto the component name.
//
// Examples:
//
// 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage

import { Route } from '@redwoodjs/router/dist/Route'
import { Router } from '@redwoodjs/router/dist/server-router'
import { Set } from '@redwoodjs/router/dist/Set'

import NavigationLayout from './layouts/NavigationLayout/NavigationLayout'
import ScaffoldLayout from './layouts/ScaffoldLayout/ScaffoldLayout'
import AboutPage from './pages/AboutPage/AboutPage'
import EmptyUserEmptyUsersPage from './pages/EmptyUser/EmptyUsersPage/EmptyUsersPage'
import EmptyUserNewEmptyUserPage from './pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage'
import HomePage from './pages/HomePage/HomePage'
import MultiCellPage from './pages/MultiCellPage/MultiCellPage'
import UserExampleNewUserExamplePage from './pages/UserExample/NewUserExamplePage/NewUserExamplePage'
import UserExampleUserExamplePage from './pages/UserExample/UserExamplePage/UserExamplePage'
import UserExampleUserExamplesPage from './pages/UserExample/UserExamplesPage/UserExamplesPage'

const NotFoundPage = () => {
return <div>Not Found</div>
}

const Routes = ({ location }) => {
return (
<Router location={location}>
<Set wrap={NavigationLayout} rnd={Math.random()}>
<Route path="/" page={HomePage} name="home" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/multi-cell" page={MultiCellPage} name="multiCell" />

<Set
wrap={ScaffoldLayout}
title="EmptyUsers"
titleTo="emptyUsers"
buttonLabel="New EmptyUser"
buttonTo="newEmptyUser"
>
<Route
path="/empty-users/new"
page={EmptyUserNewEmptyUserPage}
name="newEmptyUser"
/>
<Route
path="/empty-users"
page={EmptyUserEmptyUsersPage}
name="emptyUsers"
/>
</Set>

<Set
wrap={ScaffoldLayout}
title="UserExamples"
titleTo="userExamples"
buttonLabel="New UserExample"
buttonTo="newUserExample"
>
<Route
path="/user-examples/new"
page={UserExampleNewUserExamplePage}
name="newUserExample"
/>
<Route
path="/user-examples/{id:Int}"
page={UserExampleUserExamplePage}
name="userExample"
/>
<Route
path="/user-examples"
page={UserExampleUserExamplesPage}
name="userExamples"
/>
</Set>
</Set>
<Route notfound page={NotFoundPage} />
</Router>
)
}

export default Routes
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import type { TagDescriptor } from '@redwoodjs/web/dist/components/htmlTags'

import { Document } from './Document'
import ServerRoutes from './ServerRoutes'

interface Props {
css: string[]
meta?: TagDescriptor[]
location: {
pathname: string
hash?: string
search?: string
}
}

export const ServerEntry: React.FC<Props> = ({ css, meta }) => {
export const ServerEntry: React.FC<Props> = ({ css, meta, location }) => {
return (
<Document css={css} meta={meta}>
<div>App</div>
<ServerRoutes location={location} />
</Document>
)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Link, routes } from '@redwoodjs/router'
import { namedRoutes as routes } from '@redwoodjs/router/dist/namedRoutes'

import './NavigationLayout.css'

type NavigationLayoutProps = {
children?: React.ReactNode
rnd?: number
}

const NavigationLayout = ({ children }: NavigationLayoutProps) => {
const Link = (props: any) => {
return <a href={props.to}>{props.children}</a>
}

const NavigationLayout = ({ children, rnd }: NavigationLayoutProps) => {
return (
<div className="navigation-layout">
<nav>
Expand All @@ -28,6 +33,7 @@ const NavigationLayout = ({ children }: NavigationLayoutProps) => {
</li>
</ul>
</nav>
<div id="rnd">{Math.round(rnd * 100)}</div>
<main>{children}</main>
</div>
)
Expand Down
3 changes: 2 additions & 1 deletion __fixtures__/test-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"packageManager": "yarn@4.1.1",
"resolutions": {
"@storybook/react-dom-shim@npm:7.6.17": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz",
"@apollo/client/rehackt": "0.0.0-pr.10.0"
"@apollo/client/rehackt": "0.0.0-pr.10.0",
"react-is": "19.0.0-canary-cb151849e1-20240424"
}
}
3 changes: 2 additions & 1 deletion packages/create-redwood-app/templates/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"packageManager": "yarn@4.1.1",
"resolutions": {
"@storybook/react-dom-shim@npm:7.6.17": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz",
"@apollo/client/rehackt": "0.0.0-pr.10.0"
"@apollo/client/rehackt": "0.0.0-pr.10.0",
"react-is": "19.0.0-canary-cb151849e1-20240424"
}
}
3 changes: 2 additions & 1 deletion packages/create-redwood-app/templates/ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"packageManager": "yarn@4.1.1",
"resolutions": {
"@storybook/react-dom-shim@npm:7.6.17": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz",
"@apollo/client/rehackt": "0.0.0-pr.10.0"
"@apollo/client/rehackt": "0.0.0-pr.10.0",
"react-is": "19.0.0-canary-cb151849e1-20240424"
}
}
2 changes: 1 addition & 1 deletion packages/vite/src/clientSsr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function renderFromDist<TProps extends Record<string, any>>(

const id = resolveClientEntryForProd(filePath, clientEntries)

console.log('Proxy id', id)
console.log('clientSsr.ts::Proxy id', id)
// id /Users/tobbe/tmp/test-project-rsc-external-packages-and-cells/web/dist/client/assets/rsc-AboutCounter.tsx-1-4kTKU8GC.mjs
return { id, chunks: [id], name, async: true }
},
Expand Down
2 changes: 2 additions & 0 deletions packages/vite/src/lib/registerFwGlobalsAndShims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ function registerFwGlobals() {
* We have to call it early in the app's lifecycle, before code that depends on
* it runs and do so at the server start in (src/devFeServer.ts and
* src/runFeServer.ts).
*
* We generate the input to the shims in the `bundlerConfig` Proxies we have
*/
function registerFwShims() {
if (!getConfig().experimental?.rsc?.enabled) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/rsc/rscWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ async function renderRsc(input: RenderInput): Promise<PipeableStream> {
id = resolveClientEntryForProd(filePath, config)
}

console.log('Proxy id', id)
console.log('rscWorker proxy id', id)
// id /assets/rsc0-beb48afe.js
return { id, chunks: [id], name, async: true }
},
Expand Down
4 changes: 4 additions & 0 deletions packages/vite/src/streaming/ssrModuleMap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { makeFilePath } from '../utils'

type SSRModuleMap = null | {
[clientId: string]: {
[clientExportName: string]: ClientReferenceManifestEntry
Expand All @@ -24,6 +26,8 @@ export const moduleMap: SSRModuleMap = new Proxy(
{},
{
get(_target, name: string) {
filePath = makeFilePath(filePath)

const manifestEntry: ClientReferenceManifestEntry = {
id: filePath,
chunks: [filePath],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { test, expect } from '@playwright/test'

// UA taken from https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers
const BOT_USERAGENT =
'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36'

test('Check that homepage has content fully rendered from the server, without JS', async ({
browser,
}) => {
// Rendering as a bot here, to make sure we're getting the full page and not
// just some of it, with the rest streamed in later
const botContext = await browser.newContext({
userAgent: BOT_USERAGENT,
// Even without JS, this should be a fully rendered page
javaScriptEnabled: false,
})

const page = await botContext.newPage()

await page.goto('/')

// Appears when the navigation layout has successfully rendered
await page.waitForSelector('main')

// The NavigationLayout should have a random number in it
const rnd = await page.locator('div#rnd').innerHTML()
expect(rnd).toMatch(/\s*\d+\s*/)

// expect there to only be one h1 heading element on the page
await expect(page.locator('h1')).toHaveCount(1)
await expect(page.locator('h1').first()).toHaveText('Hello Anonymous!!')

// There should be a link to the about page
await expect(page.locator('a').getByText('About')).toBeVisible()

await botContext.close()
})

test('Make sure navigation works even without JS', async ({ browser }) => {
// Rendering as a bot here, to make sure we're getting the full page and not
// just some of it, with the rest streamed in later
const botContext = await browser.newContext({
userAgent: BOT_USERAGENT,
// Even without JS, this should be a fully rendered page
javaScriptEnabled: false,
})

const page = await botContext.newPage()

await page.goto('/')

// There should be a link to the about page
const aboutLink = page.locator('a').getByText('About')
expect(aboutLink).toBeVisible()

// Clicking on the about link should take us to the about page
await aboutLink.click()

expect(page.url()).toMatch(/\/about$/)

// expect there to only be one h1 heading element on the page
await expect(page.locator('h1')).toHaveCount(1)
await expect(page.locator('h1').first()).toHaveText('About Redwood')

await botContext.close()
})

test('The page should have a form button, but it should be non-interactive', async ({
browser,
}) => {
// Rendering as a bot here, to make sure we're getting the full page and not
// just some of it, with the rest streamed in later
const botContext = await browser.newContext({
userAgent: BOT_USERAGENT,
// Even without JS, this should be a fully rendered page
javaScriptEnabled: false,
})

const page = await botContext.newPage()

await page.goto('/about')

const paragraphs = page.locator('p')

// Expect the count to be 0 when the page is first loaded
expect(paragraphs.getByText('Count: 0')).toBeVisible()

await page.getByRole('button', { name: 'Increment' }).click()

// The count should stay at 0, because the page should not be interactive
// (This is the SSRed version of the page, with no JS)
await expect(paragraphs.getByText('Count: 0')).toBeVisible()

await expect(paragraphs.getByText('RSC on client: enabled')).toBeVisible()
await expect(paragraphs.getByText('RSC on server: enabled')).toBeVisible()

await botContext.close()
})
Loading