Skip to content

Commit

Permalink
Refactor client component out of client runtime (#37238)
Browse files Browse the repository at this point in the history
Co-authored-by: Shu Ding <g@shud.in>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored May 29, 2022
1 parent bbcfc0d commit 31500ba
Show file tree
Hide file tree
Showing 17 changed files with 153 additions and 89 deletions.
4 changes: 3 additions & 1 deletion packages/next/build/webpack/loaders/next-app-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{
// Add page itself to the list of components
componentsCode.push(
`'${pathToUrlPath(pagePath).replace(
new RegExp(`/page+(${extensions.join('|')})$`),
new RegExp(`(${extensions.join('|')})$`),
''
// use require so that we can bust the require cache
)}': () => require('${pagePath}')`
Expand All @@ -117,6 +117,8 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{
${componentsCode.join(',\n')}
};
export const AppRouter = require('next/dist/client/components/app-router.client.js').default
export const __next_app_webpack_require__ = __webpack_require__
`
return result
Expand Down
37 changes: 31 additions & 6 deletions packages/next/build/webpack/plugins/flight-manifest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,41 @@ export class FlightManifestPlugin {
: `./${ssrNamedModuleId}`

const exportsInfo = compilation.moduleGraph.getExportsInfo(mod)
const moduleExportedKeys = ['', '*'].concat(
[...exportsInfo.exports]
.map((exportInfo) => {
const cjsExports = [
...new Set(
[].concat(
mod.dependencies.map((dep: any) => {
// Match CommonJsSelfReferenceDependency
if (dep.type === 'cjs self exports reference') {
// `module.exports = ...`
if (dep.base === 'module.exports') {
return 'default'
}

// `exports.foo = ...`, `exports.default = ...`
if (dep.base === 'exports') {
return dep.names.filter(
(name: any) => name !== '__esModule'
)
}
}
return null
})
)
),
]

const moduleExportedKeys = ['', '*']
.concat(
[...exportsInfo.exports].map((exportInfo) => {
if (exportInfo.provided) {
return exportInfo.name
}
return null
})
.filter(Boolean)
)
}),
...cjsExports
)
.filter((name) => name !== null)

moduleExportedKeys.forEach((name) => {
if (!moduleExports[name]) {
Expand Down
72 changes: 2 additions & 70 deletions packages/next/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import '../build/polyfills/polyfill-module'
import ReactDOMClient from 'react-dom/client'
// @ts-ignore startTransition exists when using React 18
import React from 'react'
import {
createFromFetch,
createFromReadableStream,
} from 'next/dist/compiled/react-server-dom-webpack'
import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack'

/// <reference types="react-dom/experimental" />

Expand Down Expand Up @@ -151,76 +148,11 @@ const RSCComponent = () => {
return <ServerRoot cacheKey={cacheKey} />
}

function fetchFlight(href: string) {
const url = new URL(href, location.origin)
const searchParams = url.searchParams
searchParams.append('__flight__', '1')

return fetch(url.toString())
}

function useServerResponse(cacheKey: string) {
let response = rscCache.get(cacheKey)
if (response) return response

response = createFromFetch(fetchFlight(getCacheKey()))

rscCache.set(cacheKey, response)
return response
}

const AppRouterContext = React.createContext({})

// TODO: move to client component when handling is implemented
function AppRouter({ initialUrl, children }: any) {
const initialState = {
url: initialUrl,
}
const previousUrlRef = React.useRef(initialState)
const [current, setCurrent] = React.useState(initialState)

const appRouter = React.useMemo(() => {
return {
push: (url: string) => {
previousUrlRef.current = current
setCurrent({ ...current, url })
// TODO: update url eagerly or not?
window.history.pushState(current, '', url)
},
url: current.url,
}
}, [current])

// @ts-ignore TODO: for testing
window.appRouter = appRouter

console.log({
appRouter,
previous: previousUrlRef.current,
current,
})

let root
if (current.url !== previousUrlRef.current?.url) {
// eslint-disable-next-line
const data = useServerResponse(current.url)
root = data.readRoot()
}

return (
<AppRouterContext.Provider value={appRouter}>
{root ? root : children}
</AppRouterContext.Provider>
)
}

export function hydrate() {
renderReactElement(appElement!, () => (
<React.StrictMode>
<Root>
<AppRouter initialUrl={location.pathname}>
<RSCComponent />
</AppRouter>
<RSCComponent />
</Root>
</React.StrictMode>
))
Expand Down
74 changes: 74 additions & 0 deletions packages/next/client/components/app-router.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react'
import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack'
import { AppRouterContext } from '../../shared/lib/app-router-context'

function createResponseCache() {
return new Map<string, any>()
}
const rscCache = createResponseCache()

const getCacheKey = () => {
const { pathname, search } = location
return pathname + search
}

function fetchFlight(href: string) {
const url = new URL(href, location.origin)
const searchParams = url.searchParams
searchParams.append('__flight__', '1')

return fetch(url.toString())
}

function fetchServerResponse(cacheKey: string) {
let response = rscCache.get(cacheKey)
if (response) return response

response = createFromFetch(fetchFlight(getCacheKey()))

rscCache.set(cacheKey, response)
return response
}

// TODO: move to client component when handling is implemented
export default function AppRouter({ initialUrl, children }: any) {
const initialState = {
url: initialUrl,
}
const previousUrlRef = React.useRef(initialState)
const [current, setCurrent] = React.useState(initialState)
const appRouter = React.useMemo(() => {
return {
prefetch: () => {},
replace: () => {},
push: (url: string) => {
previousUrlRef.current = current
setCurrent({ ...current, url })
// TODO: update url eagerly or not?
window.history.pushState(current, '', url)
},
url: current.url,
}
}, [current])
if (typeof window !== 'undefined') {
// @ts-ignore TODO: for testing
window.appRouter = appRouter
console.log({
appRouter,
previous: previousUrlRef.current,
current,
})
}

let root
if (current.url !== previousUrlRef.current?.url) {
// eslint-disable-next-line
const data = fetchServerResponse(current.url)
root = data.readRoot()
}
return (
<AppRouterContext.Provider value={appRouter}>
{root ? root : children}
</AppRouterContext.Provider>
)
}
10 changes: 8 additions & 2 deletions packages/next/client/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
PrefetchOptions,
resolveHref,
} from '../shared/lib/router/router'
import { useRouter } from './router'
import { RouterContext } from '../shared/lib/router-context'
import { AppRouterContext } from '../shared/lib/app-router-context'
import { useIntersection } from './use-intersection'

type Url = string | UrlObject
Expand Down Expand Up @@ -269,7 +270,12 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(
}

const p = prefetchProp !== false
const router = useRouter()
let router = React.useContext(RouterContext)

const appRouter = React.useContext(AppRouterContext)
if (appRouter) {
router = appRouter
}

const { href, as } = React.useMemo(() => {
const [resolvedHref, resolvedAs] = resolveHref(router, hrefProp, true)
Expand Down
13 changes: 11 additions & 2 deletions packages/next/server/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,15 @@ export async function renderToHTML(
// }
}

const AppRouter = ComponentMod.AppRouter
const WrappedComponentWithRouter = () => {
return (
<AppRouter initialUrl={req.url}>
<WrappedComponent />
</AppRouter>
)
}

const bootstrapScripts = !isSubtreeRender
? buildManifest.rootMainFiles.map((src) => '/_next/' + src)
: undefined
Expand All @@ -368,7 +377,7 @@ export async function renderToHTML(
const search = stringifyQuery(query)

const Component = createServerComponentRenderer(
WrappedComponent,
WrappedComponentWithRouter,
ComponentMod,
{
cachePrefix: pathname + (search ? `?${search}` : ''),
Expand All @@ -393,7 +402,7 @@ export async function renderToHTML(
if (renderServerComponentData) {
return new RenderResult(
renderToReadableStream(
<WrappedComponent />,
<WrappedComponentWithRouter />,
serverComponentManifest
).pipeThrough(createBufferedTransformStream())
)
Expand Down
7 changes: 2 additions & 5 deletions packages/next/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1733,13 +1733,10 @@ export default abstract class Server<ServerOptions extends Options = Options> {
let page = pathname
const bubbleNoFallback = !!query._nextBubbleNoFallback
delete query._nextBubbleNoFallback
// map the route to the actual bundle name e.g.
// `/dashboard/rootonly/hello` -> `/dashboard+rootonly/hello`
// map the route to the actual bundle name
const getOriginalappPath = (appPath: string) => {
if (this.nextConfig.experimental.appDir) {
const originalappPath =
this.appPathRoutes?.[`${appPath}/index`] ||
this.appPathRoutes?.[`${appPath}`]
const originalappPath = this.appPathRoutes?.[appPath]

if (!originalappPath) {
return null
Expand Down
7 changes: 7 additions & 0 deletions packages/next/shared/lib/app-router-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react'

export const AppRouterContext = React.createContext<any>(null as any)

if (process.env.NODE_ENV !== 'production') {
AppRouterContext.displayName = 'AppRouterContext'
}
4 changes: 2 additions & 2 deletions test/e2e/app-dir/app/app/dashboard/index/page.server.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default function DashboardPage(props) {
export default function DashboardIndexPage() {
return (
<>
<p>hello from root/dashboard</p>
<p>hello from root/dashboard/index</p>
</>
)
}
7 changes: 7 additions & 0 deletions test/e2e/app-dir/app/app/dashboard/page.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function DashboardPage(props) {
return (
<>
<p>hello from root/dashboard</p>
</>
)
}
7 changes: 6 additions & 1 deletion test/e2e/app-dir/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,16 @@ describe('views dir', () => {
expect(html).toContain('hello world')
})

it('should serve from root', async () => {
it('should serve from app', async () => {
const html = await renderViaHTTP(next.url, '/dashboard')
expect(html).toContain('hello from root/dashboard')
})

it('should serve /index as separate page', async () => {
const html = await renderViaHTTP(next.url, '/dashboard/index')
expect(html).toContain('hello from root/dashboard/index')
})

it('should include layouts when no direct parent layout', async () => {
const html = await renderViaHTTP(next.url, '/dashboard/integrations')
const $ = cheerio.load(html)
Expand Down

0 comments on commit 31500ba

Please sign in to comment.