Skip to content

Commit

Permalink
Add route loader (#36712)
Browse files Browse the repository at this point in the history
* Add route loader

* Update to leverage new view-loader

* fix lint

Co-authored-by: JJ Kasper <jj@jjsweb.site>
  • Loading branch information
timneutkens and ijjk committed May 5, 2022
1 parent 4fb0beb commit 6f90d19
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 50 deletions.
21 changes: 20 additions & 1 deletion packages/next/build/entries.ts
Expand Up @@ -285,6 +285,15 @@ export function getEdgeServerEntry(opts: {
return `next-middleware-ssr-loader?${stringify(loaderParams)}!`
}

export function getViewsEntry(opts: { pagePath: string; viewsDir: string }) {
const loaderParams = {
pagePath: opts.pagePath,
viewsDir: opts.viewsDir,
}

return `next-view-loader?${stringify(loaderParams)}!`
}

export function getServerlessEntry(opts: {
absolutePagePath: string
buildId: string
Expand Down Expand Up @@ -357,6 +366,11 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
const getEntryHandler =
(mappings: Record<string, string>, isViews: boolean) =>
async (page: string) => {
// TODO: @timneutkens do not pass layouts to entry here
if (isViews && page.endsWith('/layout')) {
return
}

const bundleFile = normalizePagePath(page)
const clientBundlePath = posix.join('pages', bundleFile)
const serverBundlePath = posix.join(
Expand Down Expand Up @@ -388,7 +402,12 @@ export async function createEntrypoints(params: CreateEntrypointsParams) {
})
},
onServer: () => {
if (isTargetLikeServerless(target)) {
if (isViews && viewsDir) {
server[serverBundlePath] = getViewsEntry({
pagePath: mappings[page],
viewsDir,
})
} else if (isTargetLikeServerless(target)) {
if (page !== '/_app' && page !== '/_document') {
server[serverBundlePath] = getServerlessEntry({
...params,
Expand Down
102 changes: 95 additions & 7 deletions packages/next/build/webpack/loaders/next-view-loader.ts
@@ -1,17 +1,105 @@
import path from 'path'
import type webpack from 'webpack5'
import { NODE_RESOLVE_OPTIONS } from '../../webpack-config'

function pathToUrlPath(pathname: string) {
let urlPath = pathname.replace(/^private-next-views-dir/, '')

// For `views/layout.js`
if (urlPath === '') {
urlPath = '/'
}

return urlPath
}

async function resolveLayoutPathsByPage({
pagePath,
resolve,
}: {
pagePath: string
resolve: (pathname: string) => Promise<string | undefined>
}) {
const layoutPaths = new Map<string, string | undefined>()
const parts = pagePath.split('/')

for (let i = 1; i < parts.length; i++) {
const pathWithoutSlashLayout = parts.slice(0, i).join('/')
const layoutPath = `${pathWithoutSlashLayout}/layout`

const resolvedLayoutPath = await resolve(layoutPath)

let urlPath = pathToUrlPath(pathWithoutSlashLayout)

layoutPaths.set(urlPath, resolvedLayoutPath)
}

return layoutPaths
}

const extensions = [
...NODE_RESOLVE_OPTIONS.extensions,
...NODE_RESOLVE_OPTIONS.extensions.map((ext) => `.server${ext}`),
...NODE_RESOLVE_OPTIONS.extensions.map((ext) => `.client${ext}`),
]
const resolveOptions: any = {
...NODE_RESOLVE_OPTIONS,
extensions,
}

const nextViewLoader: webpack.LoaderDefinitionFunction<{
components: string[]
}> = function nextViewLoader() {
pagePath: string
viewsDir: string
}> = async function nextViewLoader() {
const loaderOptions = this.getOptions() || {}
const resolve = this.getResolve(resolveOptions)
const viewsDir = loaderOptions.viewsDir

return `
export const components = {
${loaderOptions.components
.map((component) => `'${component}': () => import('${component}')`)
.join(',\n')}
const layoutPaths = await resolveLayoutPathsByPage({
pagePath: loaderOptions.pagePath,
resolve: async (pathname) => {
try {
return await resolve(this.rootContext, pathname)
} catch (err: any) {
if (err.message.includes("Can't resolve")) {
return undefined
}
throw err
}
},
})

const componentsCode = []
for (const [layoutPath, resolvedLayoutPath] of layoutPaths) {
if (resolvedLayoutPath) {
this.addDependency(resolvedLayoutPath)
// use require so that we can bust the require cache
const codeLine = `'${layoutPath}': () => require('${resolvedLayoutPath}')`
componentsCode.push(codeLine)
} else {
for (const ext of extensions) {
this.addMissingDependency(
path.join(viewsDir, layoutPath, `layout${ext}`)
)
}
}
}

// Add page itself to the list of components
componentsCode.push(
`'${pathToUrlPath(loaderOptions.pagePath).replace(
new RegExp(`/page\\.+(${extensions.join('|')})$`),
''
// use require so that we can bust the require cache
)}': () => require('${loaderOptions.pagePath}')`
)

const result = `
export const components = {
${componentsCode.join(',\n')}
};
`
return result
}

export default nextViewLoader
24 changes: 0 additions & 24 deletions packages/next/server/base-server.ts
Expand Up @@ -1748,24 +1748,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
return null
}

const gatherViewLayouts = async (
viewPath: string,
result: FindComponentsResult
): Promise<void> => {
const layoutPaths = this.getViewPathLayouts(viewPath)
result.components.viewLayouts = await Promise.all(
layoutPaths.map(async (path) => {
const layoutRes = await this.findPageComponents(path)
return {
isRootLayout: path === '/layout',
Component: layoutRes?.components.Component!,
getStaticProps: layoutRes?.components.getStaticProps,
getServerSideProps: layoutRes?.components.getServerSideProps,
}
})
)
}

try {
// Ensure a request to the URL /accounts/[id] will be treated as a dynamic
// route correctly and not loaded immediately without parsing params.
Expand All @@ -1778,9 +1760,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
const result = await this.findPageComponents(page, query)
if (result) {
try {
if (result.components.isViewPath) {
await gatherViewLayouts(page, result)
}
return await this.renderToResponseWithComponents(ctx, result)
} catch (err) {
const isNoFallbackError = err instanceof NoFallbackError
Expand Down Expand Up @@ -1812,9 +1791,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
)
if (dynamicRouteResult) {
try {
if (dynamicRouteResult.components.isViewPath) {
await gatherViewLayouts(page, dynamicRouteResult)
}
return await this.renderToResponseWithComponents(
{
...ctx,
Expand Down
18 changes: 16 additions & 2 deletions packages/next/server/dev/hot-reloader.ts
Expand Up @@ -11,11 +11,16 @@ import {
finalizeEntrypoint,
getClientEntry,
getEdgeServerEntry,
getViewsEntry,
runDependingOnPageType,
} from '../../build/entries'
import { watchCompilers } from '../../build/output'
import getBaseWebpackConfig from '../../build/webpack-config'
import { API_ROUTE, MIDDLEWARE_ROUTE } from '../../lib/constants'
import {
API_ROUTE,
MIDDLEWARE_ROUTE,
VIEWS_DIR_ALIAS,
} from '../../lib/constants'
import { recursiveDelete } from '../../lib/recursive-delete'
import { BLOCKED_PAGES } from '../../shared/lib/constants'
import { __ApiPreviewProps } from '../api-utils'
Expand Down Expand Up @@ -592,7 +597,16 @@ export default class HotReloader {
entrypoints[bundlePath] = finalizeEntrypoint({
compilerType: 'server',
name: bundlePath,
value: request,
value:
this.viewsDir && bundlePath.startsWith('views/')
? getViewsEntry({
pagePath: join(
VIEWS_DIR_ALIAS,
relative(this.viewsDir!, absolutePagePath)
),
viewsDir: this.viewsDir!,
})
: request,
})
}
},
Expand Down
6 changes: 0 additions & 6 deletions packages/next/server/load-components.ts
Expand Up @@ -41,12 +41,6 @@ export type LoadComponentsReturnType = {
AppMod: any
AppServerMod: any
isViewPath?: boolean
viewLayouts?: Array<{
isRootLayout?: boolean
Component: NextComponentType
getStaticProps?: GetStaticProps
getServerSideProps?: GetServerSideProps
}>
}

export async function loadDefaultErrorComponents(distDir: string) {
Expand Down
22 changes: 12 additions & 10 deletions packages/next/server/view-render.tsx
Expand Up @@ -216,13 +216,13 @@ export async function renderToHTML(

const hasConcurrentFeatures = !!runtime
const pageIsDynamic = isDynamicRoute(pathname)
const layouts = renderOpts.viewLayouts || []

layouts.push({
Component: renderOpts.Component,
getStaticProps: renderOpts.getStaticProps,
getServerSideProps: renderOpts.getServerSideProps,
})
const components = Object.keys(ComponentMod.components)
.sort()
.map((key) => {
const mod = ComponentMod.components[key]()
mod.Component = mod.default || mod
return mod
})

// Reads of this are cached on the `req` object, so this should resolve
// instantly. There's no need to pass this data down from a previous
Expand All @@ -239,11 +239,13 @@ export async function renderToHTML(

const dataCache = new Map<string, Record>()

for (let i = layouts.length - 1; i >= 0; i--) {
for (let i = components.length - 1; i >= 0; i--) {
const dataCacheKey = i.toString()
const layout = layouts[i]
const layout = components[i]

if (layout.isRootLayout) {
if (i === 0) {
// top-most layout is the root layout that renders
// the html/body tags
RootLayout = layout.Component
continue
}
Expand Down

0 comments on commit 6f90d19

Please sign in to comment.