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

Refactor client component out of client runtime #37238

Merged
merged 9 commits into from
May 29, 2022
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
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')
ijjk marked this conversation as resolved.
Show resolved Hide resolved
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