Skip to content

Commit

Permalink
Pages Route Module (vercel#50070)
Browse files Browse the repository at this point in the history
This exports the named exports from the user's page route file and
exports a new `routeModule` stub. This stub will be the new rendering
mechanism used for pages within `pages/` and will serve to allow us to
deprecate large chunks of the server code to instead live within the
pages entrypoints.

TLDR;

- Route Modules now accept a `definition` so that they have their full
route definition
- Exports are now hoisted from the userland module by name to prevent
name collisions
- A new `PagesRouteModule` stub was added that will in the future
contain all the pages rendering logic

---------

Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
  • Loading branch information
2 people authored and hydRAnger committed Jun 12, 2023
1 parent 836ca8f commit 610f9a5
Show file tree
Hide file tree
Showing 18 changed files with 355 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import { EdgeRouteModuleWrapper } from 'next/dist/server/web/edge-route-module-w

import RouteModule from 'ROUTE_MODULE'
import * as userland from 'ENTRY'
import { PAGE, PATHNAME } from 'BOOTSTRAP_CONFIG'
import { PAGE, PATHNAME, KIND } from 'BOOTSTRAP_CONFIG'

// TODO: (wyattjoh) - perform the option construction in Rust to allow other modules to accept different options
const routeModule = new RouteModule({
userland,
pathname: PATHNAME,
definition: {
page: PAGE,
kind: KIND,
pathname: PATHNAME,
// The following aren't used in production.
filename: '',
bundlePath: '',
},
resolvedPagePath: `app/${PAGE}`,
nextConfigOutput: undefined,
})
Expand Down
11 changes: 9 additions & 2 deletions packages/next-swc/crates/next-core/js/src/entry/app/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ import { attachRequestMeta } from '../../internal/next-request-helpers'

import RouteModule from 'ROUTE_MODULE'
import * as userland from 'ENTRY'
import { PAGE, PATHNAME } from 'BOOTSTRAP_CONFIG'
import { PAGE, PATHNAME, KIND } from 'BOOTSTRAP_CONFIG'

const routeModule = new RouteModule({
userland,
pathname: PATHNAME,
definition: {
page: PAGE,
kind: KIND,
pathname: PATHNAME,
// The following aren't used in production.
filename: '',
bundlePath: '',
},
resolvedPagePath: `app/${PAGE}`,
nextConfigOutput: undefined,
})
Expand Down
3 changes: 3 additions & 0 deletions packages/next-swc/crates/next-core/js/types/rust.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,12 @@ declare module 'ROUTE_MODULE' {
}

declare module 'BOOTSTRAP_CONFIG' {
import type { RouteKind } from 'next/dist/server/future/route-kind'

export const NAME: string
export const PAGE: string
export const PATHNAME: string
export const KIND: RouteKind
}

declare module 'CLIENT_MODULE' {
Expand Down
1 change: 1 addition & 0 deletions packages/next-swc/crates/next-core/src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ pub async fn bootstrap(
let mut config = config.await?.clone_value();
config.insert("PAGE".to_string(), path.to_string());
config.insert("PATHNAME".to_string(), pathname);
config.insert("KIND".to_string(), "APP_ROUTE".to_string());

let config_asset = as_es_module_asset(
VirtualAssetVc::new(
Expand Down
12 changes: 8 additions & 4 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,16 +1193,20 @@ export default async function build(
? [
'hasCustomGetInitialProps',
'isPageStatic',
'getNamedExports',
'getDefinedNamedExports',
'exportPage',
]
: ['hasCustomGetInitialProps', 'isPageStatic', 'getNamedExports'],
: [
'hasCustomGetInitialProps',
'isPageStatic',
'getDefinedNamedExports',
],
}) as Worker &
Pick<
typeof import('./worker'),
| 'hasCustomGetInitialProps'
| 'isPageStatic'
| 'getNamedExports'
| 'getDefinedNamedExports'
| 'exportPage'
>
}
Expand Down Expand Up @@ -1275,7 +1279,7 @@ export default async function build(
true
)

const namedExportsPromise = pagesStaticWorkers.getNamedExports(
const namedExportsPromise = pagesStaticWorkers.getDefinedNamedExports(
appPageToCheck,
distDir,
runtimeEnvConfig
Expand Down
9 changes: 5 additions & 4 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1677,21 +1677,22 @@ export async function hasCustomGetInitialProps(
return mod.getInitialProps !== mod.origGetInitialProps
}

export async function getNamedExports(
export async function getDefinedNamedExports(
page: string,
distDir: string,
runtimeEnvConfig: any
): Promise<Array<string>> {
): Promise<ReadonlyArray<string>> {
require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig)
const components = await loadComponents({
distDir,
pathname: page,
hasServerComponents: false,
isAppPath: false,
})
let mod = components.ComponentMod

return Object.keys(mod)
return Object.keys(components.ComponentMod).filter((key) => {
return typeof components.ComponentMod[key] !== 'undefined'
})
}

export function detectConflictingPaths(
Expand Down
30 changes: 20 additions & 10 deletions packages/next/src/build/webpack/loaders/next-app-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { isAppRouteRoute } from '../../../lib/is-app-route-route'
import { isMetadataRoute } from '../../../lib/metadata/is-metadata-route'
import { NextConfig } from '../../../server/config-shared'
import { AppPathnameNormalizer } from '../../../server/future/normalizers/built/app/app-pathname-normalizer'
import { RouteKind } from '../../../server/future/route-kind'
import { AppRouteRouteModuleOptions } from '../../../server/future/route-modules/app-route/module'
import { AppBundlePathNormalizer } from '../../../server/future/normalizers/built/app/app-bundle-path-normalizer'

export type AppLoaderOptions = {
name: string
Expand Down Expand Up @@ -107,21 +110,24 @@ async function createAppRouteCode({
// References the route handler file to load found in `./routes/${kind}.ts`.
// TODO: allow switching to the different kinds of routes
const kind = 'app-route'
const normalizer = new AppPathnameNormalizer()
const pathname = normalizer.normalize(page)
const pathname = new AppPathnameNormalizer().normalize(page)
const bundlePath = new AppBundlePathNormalizer().normalize(page)

// This is providing the options defined by the route options type found at
// ./routes/${kind}.ts. This is stringified here so that the literal for
// `userland` can reference the variable for `userland` that's in scope for
// the loader code.
const options = `{
userland,
pathname: ${JSON.stringify(pathname)},
resolvedPagePath: ${JSON.stringify(resolvedPagePath)},
nextConfigOutput: ${
nextConfigOutput ? JSON.stringify(nextConfigOutput) : 'undefined'
const options: Omit<AppRouteRouteModuleOptions, 'userland'> = {
definition: {
kind: RouteKind.APP_ROUTE,
page,
pathname,
filename,
bundlePath,
},
}`
resolvedPagePath,
nextConfigOutput,
}

return `
import 'next/dist/server/node-polyfill-headers'
Expand All @@ -130,7 +136,11 @@ async function createAppRouteCode({
import * as userland from ${JSON.stringify(resolvedPagePath)}
const routeModule = new RouteModule(${options})
const options = ${JSON.stringify(options)}
const routeModule = new RouteModule({
...options,
userland,
})
// Pull out the exports that we need to expose from the module. This should
// be eliminated when we've moved the other routes to the new format. These
Expand Down
64 changes: 0 additions & 64 deletions packages/next/src/build/webpack/loaders/next-route-loader.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Hoists a name from a module or promised module.
*
* @param module the module to hoist the name from
* @param name the name to hoist
* @returns the value on the module (or promised module)
*/
export function hoist(module: any, name: string) {
// If the name is available in the module, return it.
if (name in module) {
return module[name]
}

// If a property called `then` exists, assume it's a promise and
// return a promise that resolves to the name.
if ('then' in module && typeof module.then === 'function') {
return module.then((mod: any) => hoist(mod, name))
}

// If we're trying to hoise the default export, and the module is a function,
// return the module itself.
if (typeof module === 'function' && name === 'default') {
return module
}

// Otherwise, return undefined.
return undefined
}
104 changes: 104 additions & 0 deletions packages/next/src/build/webpack/loaders/next-route-loader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { webpack } from 'next/dist/compiled/webpack/webpack'

import { stringify } from 'querystring'
import { getModuleBuildInfo } from '../get-module-build-info'
import { PagesRouteModuleOptions } from '../../../../server/future/route-modules/pages/module'
import { RouteKind } from '../../../../server/future/route-kind'
import { normalizePagePath } from '../../../../shared/lib/page-path/normalize-page-path'

/**
* The options for the route loader.
*/
type RouteLoaderOptions = {
/**
* The page name for this particular route.
*/
page: string

/**
* The preferred region for this route.
*/
preferredRegion: string | string[] | undefined

/**
* The absolute path to the userland page file.
*/
absolutePagePath: string
}

/**
* Returns the loader entry for a given page.
*
* @param query the options to create the loader entry
* @returns the encoded loader entry
*/
export function getRouteLoaderEntry(query: RouteLoaderOptions): string {
return `next-route-loader?${stringify(query)}!`
}

/**
* Handles the `next-route-loader` options.
* @returns the loader definition function
*/
const loader: webpack.LoaderDefinitionFunction<RouteLoaderOptions> =
function () {
const { page, preferredRegion, absolutePagePath } = this.getOptions()

// Ensure we only run this loader for as a module.
if (!this._module) {
throw new Error('Invariant: expected this to reference a module')
}

// Attach build info to the module.
const buildInfo = getModuleBuildInfo(this._module)
buildInfo.route = {
page,
absolutePagePath,
preferredRegion,
}

const options: Omit<PagesRouteModuleOptions, 'userland'> = {
definition: {
kind: RouteKind.PAGES,
page: normalizePagePath(page),
pathname: page,
// The following aren't used in production.
bundlePath: '',
filename: '',
},
}

return `
// Next.js Route Loader
import RouteModule from "next/dist/server/future/route-modules/pages/module"
import { hoist } from "next/dist/build/webpack/loaders/next-route-loader/helpers"
// Import the userland code.
import * as userland from ${JSON.stringify(absolutePagePath)}
// Re-export the component (should be the default export).
export default hoist(userland, "default")
// Re-export methods.
export const getStaticProps = hoist(userland, "getStaticProps")
export const getStaticPaths = hoist(userland, "getStaticPaths")
export const getServerSideProps = hoist(userland, "getServerSideProps")
export const config = hoist(userland, "config")
export const reportWebVitals = hoist(userland, "reportWebVitals")
// Re-export legacy methods.
export const unstable_getStaticProps = hoist(userland, "unstable_getStaticProps")
export const unstable_getStaticPaths = hoist(userland, "unstable_getStaticPaths")
export const unstable_getStaticParams = hoist(userland, "unstable_getStaticParams")
export const unstable_getServerProps = hoist(userland, "unstable_getServerProps")
export const unstable_getServerSideProps = hoist(userland, "unstable_getServerSideProps")
// Create and export the route module that will be consumed.
const options = ${JSON.stringify(options)}
const routeModule = new RouteModule({ ...options, userland })
export { routeModule }
`
}

export default loader
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export async function getNotFoundError(
!/next-(app|middleware|client-pages|route|flight-(client|server|client-entry))-loader\.js/.test(
name
) &&
!/next-route-loader\/index\.js/.test(name) &&
!/css-loader.+\.js/.test(name)
)
if (moduleTrace.length === 0) return ''
Expand Down

0 comments on commit 610f9a5

Please sign in to comment.