diff --git a/packages/next/build/webpack/loaders/next-app-loader.ts b/packages/next/build/webpack/loaders/next-app-loader.ts index 3d21024d84a33..83ec9ecb73cb2 100644 --- a/packages/next/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/build/webpack/loaders/next-app-loader.ts @@ -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}')` @@ -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 diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index c9624b7a9e73a..2e958cb510ef2 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -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]) { diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx index c0b958017bc3b..21df8aeb919e4 100644 --- a/packages/next/client/app-index.tsx +++ b/packages/next/client/app-index.tsx @@ -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' /// @@ -151,76 +148,11 @@ const RSCComponent = () => { return } -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 ( - - {root ? root : children} - - ) -} - export function hydrate() { renderReactElement(appElement!, () => ( - - - + )) diff --git a/packages/next/client/components/app-router.client.tsx b/packages/next/client/components/app-router.client.tsx new file mode 100644 index 0000000000000..44a2f25142478 --- /dev/null +++ b/packages/next/client/components/app-router.client.tsx @@ -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() +} +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 ( + + {root ? root : children} + + ) +} diff --git a/packages/next/client/link.tsx b/packages/next/client/link.tsx index 86ca88c684347..0f0123ebbf518 100644 --- a/packages/next/client/link.tsx +++ b/packages/next/client/link.tsx @@ -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 @@ -269,7 +270,12 @@ const Link = React.forwardRef( } 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) diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 07481eaf50d88..2122155c4cb1c 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -355,6 +355,15 @@ export async function renderToHTML( // } } + const AppRouter = ComponentMod.AppRouter + const WrappedComponentWithRouter = () => { + return ( + + + + ) + } + const bootstrapScripts = !isSubtreeRender ? buildManifest.rootMainFiles.map((src) => '/_next/' + src) : undefined @@ -368,7 +377,7 @@ export async function renderToHTML( const search = stringifyQuery(query) const Component = createServerComponentRenderer( - WrappedComponent, + WrappedComponentWithRouter, ComponentMod, { cachePrefix: pathname + (search ? `?${search}` : ''), @@ -393,7 +402,7 @@ export async function renderToHTML( if (renderServerComponentData) { return new RenderResult( renderToReadableStream( - , + , serverComponentManifest ).pipeThrough(createBufferedTransformStream()) ) diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 17dae9483817a..5ccf84b21afde 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1733,13 +1733,10 @@ export default abstract class Server { 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 diff --git a/packages/next/shared/lib/app-router-context.ts b/packages/next/shared/lib/app-router-context.ts new file mode 100644 index 0000000000000..2d01ff67dcd68 --- /dev/null +++ b/packages/next/shared/lib/app-router-context.ts @@ -0,0 +1,7 @@ +import React from 'react' + +export const AppRouterContext = React.createContext(null as any) + +if (process.env.NODE_ENV !== 'production') { + AppRouterContext.displayName = 'AppRouterContext' +} diff --git a/test/e2e/app-dir/app-rendering/app/getserversideprops-only/slow/index/page.server.js b/test/e2e/app-dir/app-rendering/app/getserversideprops-only/slow/page.server.js similarity index 100% rename from test/e2e/app-dir/app-rendering/app/getserversideprops-only/slow/index/page.server.js rename to test/e2e/app-dir/app-rendering/app/getserversideprops-only/slow/page.server.js diff --git a/test/e2e/app-dir/app-rendering/app/getstaticprops-getserversideprops-combined/nested/index/page.server.js b/test/e2e/app-dir/app-rendering/app/getstaticprops-getserversideprops-combined/nested/page.server.js similarity index 100% rename from test/e2e/app-dir/app-rendering/app/getstaticprops-getserversideprops-combined/nested/index/page.server.js rename to test/e2e/app-dir/app-rendering/app/getstaticprops-getserversideprops-combined/nested/page.server.js diff --git a/test/e2e/app-dir/app-rendering/app/getstaticprops-getserversideprops-combined/slow/index/page.server.js b/test/e2e/app-dir/app-rendering/app/getstaticprops-getserversideprops-combined/slow/page.server.js similarity index 100% rename from test/e2e/app-dir/app-rendering/app/getstaticprops-getserversideprops-combined/slow/index/page.server.js rename to test/e2e/app-dir/app-rendering/app/getstaticprops-getserversideprops-combined/slow/page.server.js diff --git a/test/e2e/app-dir/app-rendering/app/getstaticprops-only/slow/index/page.server.js b/test/e2e/app-dir/app-rendering/app/getstaticprops-only/slow/page.server.js similarity index 100% rename from test/e2e/app-dir/app-rendering/app/getstaticprops-only/slow/index/page.server.js rename to test/e2e/app-dir/app-rendering/app/getstaticprops-only/slow/page.server.js diff --git a/test/e2e/app-dir/app/app/client-nested/index/page.server.js b/test/e2e/app-dir/app/app/client-nested/page.server.js similarity index 100% rename from test/e2e/app-dir/app/app/client-nested/index/page.server.js rename to test/e2e/app-dir/app/app/client-nested/page.server.js diff --git a/test/e2e/app-dir/app/app/dashboard/index/page.server.js b/test/e2e/app-dir/app/app/dashboard/index/page.server.js index f80ed6fe91206..7eea5b3463b5a 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/page.server.js +++ b/test/e2e/app-dir/app/app/dashboard/index/page.server.js @@ -1,7 +1,7 @@ -export default function DashboardPage(props) { +export default function DashboardIndexPage() { return ( <> -

hello from root/dashboard

+

hello from root/dashboard/index

) } diff --git a/test/e2e/app-dir/app/app/dashboard/integrations/index/page.server.js b/test/e2e/app-dir/app/app/dashboard/integrations/page.server.js similarity index 100% rename from test/e2e/app-dir/app/app/dashboard/integrations/index/page.server.js rename to test/e2e/app-dir/app/app/dashboard/integrations/page.server.js diff --git a/test/e2e/app-dir/app/app/dashboard/page.server.js b/test/e2e/app-dir/app/app/dashboard/page.server.js new file mode 100644 index 0000000000000..f80ed6fe91206 --- /dev/null +++ b/test/e2e/app-dir/app/app/dashboard/page.server.js @@ -0,0 +1,7 @@ +export default function DashboardPage(props) { + return ( + <> +

hello from root/dashboard

+ + ) +} diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index ea3c668d11bf3..aee586482a0af 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -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)