Skip to content

Commit acbc56d

Browse files
RSC: Differentiate routes autoloading behaviour (#10241)
1 parent 8d78142 commit acbc56d

19 files changed

+637
-159
lines changed

packages/babel-config/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ export {
3434
parseTypeScriptConfigFiles,
3535
registerBabel,
3636
} from './common'
37+
38+
export { redwoodRoutesAutoLoaderRscClientPlugin } from './plugins/babel-plugin-redwood-routes-auto-loader-rsc-client'
39+
export { redwoodRoutesAutoLoaderRscServerPlugin } from './plugins/babel-plugin-redwood-routes-auto-loader-rsc-server'
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
4+
import * as babel from '@babel/core'
5+
6+
import { getPaths } from '@redwoodjs/project-config'
7+
8+
import { redwoodRoutesAutoLoaderRscClientPlugin } from '../babel-plugin-redwood-routes-auto-loader-rsc-client'
9+
10+
const transform = (filename: string) => {
11+
const code = fs.readFileSync(filename, 'utf-8')
12+
return babel.transform(code, {
13+
filename,
14+
presets: ['@babel/preset-react'],
15+
plugins: [[redwoodRoutesAutoLoaderRscClientPlugin, {}]],
16+
})
17+
}
18+
19+
describe('injects the correct loading logic', () => {
20+
const RSC_FIXTURE_PATH = path.resolve(
21+
__dirname,
22+
'../../../../../__fixtures__/test-project-rsc-external-packages-and-cells/',
23+
)
24+
let result: babel.BabelFileResult | null
25+
26+
beforeAll(() => {
27+
process.env.RWJS_CWD = RSC_FIXTURE_PATH
28+
result = transform(getPaths().web.routes)
29+
})
30+
31+
afterAll(() => {
32+
delete process.env.RWJS_CWD
33+
})
34+
35+
test('pages are loaded with renderFromRscServer', () => {
36+
const codeOutput = result?.code
37+
38+
// We shouldn't see classic lazy loading like in non-RSC redwood
39+
expect(codeOutput).not.toContain(`const HomePage = {
40+
name: "HomePage",
41+
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")),
42+
LazyComponent: lazy(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"))
43+
`)
44+
45+
// We should import the function
46+
expect(codeOutput).toContain(
47+
'import { renderFromRscServer } from "@redwoodjs/vite/client"',
48+
)
49+
50+
// Un-imported pages get added with renderFromRscServer
51+
expect(codeOutput).toContain(
52+
'const HomePage = renderFromRscServer("HomePage")',
53+
)
54+
expect(codeOutput).toContain(
55+
'const AboutPage = renderFromRscServer("AboutPage")',
56+
)
57+
expect(codeOutput).toContain(
58+
'const UserExampleNewUserExamplePage = renderFromRscServer("UserExampleNewUserExamplePage")',
59+
)
60+
})
61+
62+
test('already imported pages are left alone.', () => {
63+
expect(result?.code).toContain(
64+
`import NotFoundPage from './pages/NotFoundPage/NotFoundPage'`,
65+
)
66+
67+
expect(result?.code).not.toContain(
68+
`const NotFoundPage = renderFromRscServer("NotFoundPage")`,
69+
)
70+
})
71+
})
72+
73+
describe('mulitiple files ending in Page.{js,jsx,ts,tsx}', () => {
74+
const FAILURE_FIXTURE_PATH = path.resolve(
75+
__dirname,
76+
'./__fixtures__/route-auto-loader/failure',
77+
)
78+
79+
beforeAll(() => {
80+
process.env.RWJS_CWD = FAILURE_FIXTURE_PATH
81+
})
82+
83+
afterAll(() => {
84+
delete process.env.RWJS_CWD
85+
})
86+
87+
test('fails with appropriate message', () => {
88+
expect(() => {
89+
transform(getPaths().web.routes)
90+
}).toThrow(
91+
"Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage",
92+
)
93+
94+
expect(() => {
95+
transform(getPaths().web.routes)
96+
}).toThrow(
97+
"Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage",
98+
)
99+
})
100+
})
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
4+
import * as babel from '@babel/core'
5+
6+
import { getPaths } from '@redwoodjs/project-config'
7+
8+
import { redwoodRoutesAutoLoaderRscServerPlugin } from '../babel-plugin-redwood-routes-auto-loader-rsc-server'
9+
10+
const transform = (filename: string) => {
11+
const code = fs.readFileSync(filename, 'utf-8')
12+
return babel.transform(code, {
13+
filename,
14+
presets: ['@babel/preset-react'],
15+
plugins: [[redwoodRoutesAutoLoaderRscServerPlugin, {}]],
16+
})
17+
}
18+
19+
const RSC_FIXTURE_PATH = path.resolve(
20+
__dirname,
21+
'../../../../../__fixtures__/test-project-rsc-external-packages-and-cells/',
22+
)
23+
24+
describe('injects the correct loading logic', () => {
25+
let result: babel.BabelFileResult | null
26+
beforeAll(() => {
27+
process.env.RWJS_CWD = RSC_FIXTURE_PATH
28+
result = transform(getPaths().web.routes)
29+
})
30+
31+
afterAll(() => {
32+
delete process.env.RWJS_CWD
33+
})
34+
35+
test('Pages are loaded with renderFromDist', () => {
36+
const codeOutput = result?.code
37+
38+
// We shouldn't see classic lazy loading like in non-RSC redwood
39+
expect(codeOutput).not.toContain(`const HomePage = {
40+
name: "HomePage",
41+
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")),
42+
LazyComponent: lazy(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"))
43+
`)
44+
45+
// We should import the function
46+
expect(codeOutput).toContain(
47+
'import { renderFromDist } from "@redwoodjs/vite/clientSsr"',
48+
)
49+
50+
// Un-imported pages get added with renderFromDist
51+
expect(codeOutput).toContain('const HomePage = renderFromDist("HomePage")')
52+
expect(codeOutput).toContain(
53+
'const AboutPage = renderFromDist("AboutPage")',
54+
)
55+
expect(codeOutput).toContain(
56+
'const UserExampleNewUserExamplePage = renderFromDist("UserExampleNewUserExamplePage")',
57+
)
58+
})
59+
60+
test('already imported pages are left alone.', () => {
61+
expect(result?.code).toContain(
62+
`import NotFoundPage from './pages/NotFoundPage/NotFoundPage'`,
63+
)
64+
65+
expect(result?.code).not.toContain(
66+
`const NotFoundPage = renderFromDist("NotFoundPage")`,
67+
)
68+
})
69+
})
70+
71+
describe('mulitiple files ending in Page.{js,jsx,ts,tsx}', () => {
72+
const FAILURE_FIXTURE_PATH = path.resolve(
73+
__dirname,
74+
'./__fixtures__/route-auto-loader/failure',
75+
)
76+
77+
beforeAll(() => {
78+
process.env.RWJS_CWD = FAILURE_FIXTURE_PATH
79+
})
80+
81+
afterAll(() => {
82+
delete process.env.RWJS_CWD
83+
})
84+
85+
test('fails with appropriate message', () => {
86+
expect(() => {
87+
transform(getPaths().web.routes)
88+
}).toThrow(
89+
"Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage",
90+
)
91+
92+
expect(() => {
93+
transform(getPaths().web.routes)
94+
}).toThrow(
95+
"Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage",
96+
)
97+
})
98+
})

packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader.test.ts

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -78,52 +78,3 @@ describe('page auto loader correctly imports pages', () => {
7878
)
7979
})
8080
})
81-
82-
describe('page auto loader handles imports for RSC', () => {
83-
const FIXTURE_PATH = path.resolve(
84-
__dirname,
85-
'../../../../../__fixtures__/example-todo-main/',
86-
)
87-
88-
let result: babel.BabelFileResult | null
89-
90-
beforeAll(() => {
91-
process.env.RWJS_CWD = FIXTURE_PATH
92-
result = transform(getPaths().web.routes, { forRscClient: true })
93-
})
94-
95-
afterAll(() => {
96-
delete process.env.RWJS_CWD
97-
})
98-
99-
test('Pages are loaded with renderFromRscServer', () => {
100-
const codeOutput = result?.code
101-
expect(codeOutput).not.toContain(`const HomePage = {
102-
name: "HomePage",
103-
prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")),
104-
LazyComponent: lazy(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage"))
105-
`)
106-
107-
expect(codeOutput).toContain(
108-
'import { renderFromRscServer } from "@redwoodjs/vite/client"',
109-
)
110-
111-
expect(codeOutput).toContain(
112-
'const HomePage = renderFromRscServer("HomePage")',
113-
)
114-
115-
// Un-imported pages get added with renderFromRscServer
116-
// so it calls the RSC worker to get a flight response
117-
expect(codeOutput).toContain(
118-
'const HomePage = renderFromRscServer("HomePage")',
119-
)
120-
expect(codeOutput).toContain(
121-
'const BarPage = renderFromRscServer("BarPage")',
122-
)
123-
})
124-
125-
// TODO(RSC): Figure out what the behavior should be?
126-
test('Already imported pages are left alone.', () => {
127-
expect(result?.code).toContain(`import FooPage from 'src/pages/FooPage'`)
128-
})
129-
})
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { PluginObj, types } from '@babel/core'
2+
3+
import {
4+
ensurePosixPath,
5+
importStatementPath,
6+
processPagesDir,
7+
} from '@redwoodjs/project-config'
8+
9+
import {
10+
getPathRelativeToSrc,
11+
withRelativeImports,
12+
} from './babel-plugin-redwood-routes-auto-loader'
13+
14+
export function redwoodRoutesAutoLoaderRscClientPlugin({
15+
types: t,
16+
}: {
17+
types: typeof types
18+
}): PluginObj {
19+
// @NOTE: This var gets mutated inside the visitors
20+
let pages = processPagesDir().map(withRelativeImports)
21+
22+
// Currently processPagesDir() can return duplicate entries when there are multiple files
23+
// ending in Page in the individual page directories. This will cause an error upstream.
24+
// Here we check for duplicates and throw a more helpful error message.
25+
const duplicatePageImportNames = new Set<string>()
26+
const sortedPageImportNames = pages.map((page) => page.importName).sort()
27+
for (let i = 0; i < sortedPageImportNames.length - 1; i++) {
28+
if (sortedPageImportNames[i + 1] === sortedPageImportNames[i]) {
29+
duplicatePageImportNames.add(sortedPageImportNames[i])
30+
}
31+
}
32+
33+
if (duplicatePageImportNames.size > 0) {
34+
throw new Error(
35+
`Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: ${Array.from(
36+
duplicatePageImportNames,
37+
)
38+
.map((name) => `'${name}'`)
39+
.join(', ')}`,
40+
)
41+
}
42+
43+
return {
44+
name: 'babel-plugin-redwood-routes-auto-loader',
45+
visitor: {
46+
// Remove any pages that have been explicitly imported in the Routes file,
47+
// because when one is present, the user is requesting that the module be
48+
// included in the main bundle.
49+
ImportDeclaration(p) {
50+
if (pages.length === 0) {
51+
return
52+
}
53+
54+
const userImportRelativePath = getPathRelativeToSrc(
55+
importStatementPath(p.node.source?.value),
56+
)
57+
58+
const defaultSpecifier = p.node.specifiers.filter((specifiers) =>
59+
t.isImportDefaultSpecifier(specifiers),
60+
)[0]
61+
62+
if (userImportRelativePath && defaultSpecifier) {
63+
// Remove the page from pages list, if it is already explicitly imported, so that we don't add loaders for these pages.
64+
// We use the path & defaultSpecifier because the const name could be anything
65+
pages = pages.filter(
66+
(page) =>
67+
!(
68+
page.relativeImport === ensurePosixPath(userImportRelativePath)
69+
),
70+
)
71+
}
72+
},
73+
Program: {
74+
enter() {
75+
pages = processPagesDir().map(withRelativeImports)
76+
},
77+
exit(p) {
78+
if (pages.length === 0) {
79+
return
80+
}
81+
const nodes = []
82+
83+
// For RSC Client builds add
84+
// import { renderFromRscServer } from '@redwoodjs/vite/client'
85+
// This will perform a fetch request to the remote RSC server
86+
nodes.unshift(
87+
t.importDeclaration(
88+
[
89+
t.importSpecifier(
90+
t.identifier('renderFromRscServer'),
91+
t.identifier('renderFromRscServer'),
92+
),
93+
],
94+
t.stringLiteral('@redwoodjs/vite/client'),
95+
),
96+
)
97+
98+
// Prepend all imports to the top of the file
99+
for (const { importName } of pages) {
100+
// RSC client wants this format
101+
// const AboutPage = renderFromRscServer('AboutPage')
102+
nodes.push(
103+
t.variableDeclaration('const', [
104+
t.variableDeclarator(
105+
t.identifier(importName),
106+
t.callExpression(t.identifier('renderFromRscServer'), [
107+
t.stringLiteral(importName),
108+
]),
109+
),
110+
]),
111+
)
112+
}
113+
114+
// Insert at the top of the file
115+
p.node.body.unshift(...nodes)
116+
},
117+
},
118+
},
119+
}
120+
}

0 commit comments

Comments
 (0)