Add initial handling for dynamic route href resolving and rewrites on the client (#15231)
Co-authored-by: Tim Neutkens <timneutkens@me.com>
This commit is contained in:
parent
ff0571ae14
commit
8a489e24bc
28 changed files with 646 additions and 246 deletions
|
@ -286,7 +286,26 @@ export default async function build(
|
|||
}
|
||||
|
||||
const routesManifestPath = path.join(distDir, ROUTES_MANIFEST)
|
||||
const routesManifest: any = {
|
||||
const routesManifest: {
|
||||
version: number
|
||||
pages404: boolean
|
||||
basePath: string
|
||||
redirects: Array<ReturnType<typeof buildCustomRoute>>
|
||||
rewrites: Array<ReturnType<typeof buildCustomRoute>>
|
||||
headers: Array<ReturnType<typeof buildCustomRoute>>
|
||||
dynamicRoutes: Array<{
|
||||
page: string
|
||||
regex: string
|
||||
namedRegex?: string
|
||||
routeKeys?: { [key: string]: string }
|
||||
}>
|
||||
dataRoutes: Array<{
|
||||
page: string
|
||||
routeKeys?: { [key: string]: string }
|
||||
dataRouteRegex: string
|
||||
namedDataRouteRegex?: string
|
||||
}>
|
||||
} = {
|
||||
version: 3,
|
||||
pages404: true,
|
||||
basePath: config.basePath,
|
||||
|
@ -304,6 +323,7 @@ export default async function build(
|
|||
namedRegex: routeRegex.namedRegex,
|
||||
}
|
||||
}),
|
||||
dataRoutes: [],
|
||||
}
|
||||
|
||||
await promises.mkdir(distDir, { recursive: true })
|
||||
|
@ -325,6 +345,7 @@ export default async function build(
|
|||
target,
|
||||
pagesDir,
|
||||
entrypoints: entrypoints.client,
|
||||
rewrites,
|
||||
}),
|
||||
getBaseWebpackConfig(dir, {
|
||||
tracer,
|
||||
|
@ -335,6 +356,7 @@ export default async function build(
|
|||
target,
|
||||
pagesDir,
|
||||
entrypoints: entrypoints.server,
|
||||
rewrites,
|
||||
}),
|
||||
])
|
||||
|
||||
|
@ -654,6 +676,7 @@ export default async function build(
|
|||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
// Since custom _app.js can wrap the 404 page we have to opt-out of static optimization if it has getInitialProps
|
||||
// Only export the static 404 when there is no /_error present
|
||||
const useStatic404 =
|
||||
|
|
|
@ -54,6 +54,7 @@ import WebpackConformancePlugin, {
|
|||
ReactSyncScriptsConformanceCheck,
|
||||
} from './webpack/plugins/webpack-conformance-plugin'
|
||||
import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-plugin'
|
||||
import { Rewrite } from '../lib/load-custom-routes'
|
||||
type ExcludesFalse = <T>(x: T | false) => x is T
|
||||
|
||||
const isWebpack5 = parseInt(webpack.version!) === 5
|
||||
|
@ -190,6 +191,7 @@ export default async function getBaseWebpackConfig(
|
|||
target = 'server',
|
||||
reactProductionProfiling = false,
|
||||
entrypoints,
|
||||
rewrites,
|
||||
}: {
|
||||
buildId: string
|
||||
config: any
|
||||
|
@ -200,6 +202,7 @@ export default async function getBaseWebpackConfig(
|
|||
tracer?: any
|
||||
reactProductionProfiling?: boolean
|
||||
entrypoints: WebpackEntrypoints
|
||||
rewrites: Rewrite[]
|
||||
}
|
||||
): Promise<webpack.Configuration> {
|
||||
const productionBrowserSourceMaps =
|
||||
|
@ -207,6 +210,8 @@ export default async function getBaseWebpackConfig(
|
|||
let plugins: PluginMetaData[] = []
|
||||
let babelPresetPlugins: { dir: string; config: any }[] = []
|
||||
|
||||
const hasRewrites = rewrites.length > 0 || dev
|
||||
|
||||
if (config.experimental.plugins) {
|
||||
plugins = await collectPlugins(dir, config.env, config.plugins)
|
||||
pluginLoaderOptions.plugins = plugins
|
||||
|
@ -326,6 +331,10 @@ export default async function getBaseWebpackConfig(
|
|||
}
|
||||
}
|
||||
|
||||
const clientResolveRewrites = require.resolve(
|
||||
'next/dist/next-server/lib/router/utils/resolve-rewrites'
|
||||
)
|
||||
|
||||
const resolveConfig = {
|
||||
// Disable .mjs for node_modules bundling
|
||||
extensions: isServer
|
||||
|
@ -370,6 +379,9 @@ export default async function getBaseWebpackConfig(
|
|||
[DOT_NEXT_ALIAS]: distDir,
|
||||
...getOptimizedAliases(isServer),
|
||||
...getReactProfilingInProduction(),
|
||||
[clientResolveRewrites]: hasRewrites
|
||||
? clientResolveRewrites
|
||||
: require.resolve('next/dist/client/dev/noop.js'),
|
||||
},
|
||||
mainFields: isServer ? ['main', 'module'] : ['browser', 'module', 'main'],
|
||||
plugins: isWebpack5
|
||||
|
@ -938,6 +950,7 @@ export default async function getBaseWebpackConfig(
|
|||
config.experimental.scrollRestoration
|
||||
),
|
||||
'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath),
|
||||
'process.env.__NEXT_HAS_REWRITES': JSON.stringify(hasRewrites),
|
||||
...(isServer
|
||||
? {
|
||||
// Fix bad-actors in the npm ecosystem (e.g. `node-formidable`)
|
||||
|
@ -1010,6 +1023,7 @@ export default async function getBaseWebpackConfig(
|
|||
!isServer &&
|
||||
new BuildManifestPlugin({
|
||||
buildId,
|
||||
rewrites,
|
||||
modern: config.experimental.modern,
|
||||
}),
|
||||
tracer &&
|
||||
|
|
|
@ -147,7 +147,7 @@ const nextServerlessLoader: loader.Loader = function () {
|
|||
|
||||
const handleRewrites = `
|
||||
const getCustomRouteMatcher = pathMatch(true)
|
||||
const {prepareDestination} = require('next/dist/next-server/server/router')
|
||||
const prepareDestination = require('next/dist/next-server/lib/router/utils/prepare-destination').default
|
||||
|
||||
function handleRewrites(parsedUrl) {
|
||||
for (const rewrite of rewrites) {
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
import { BuildManifest } from '../../../next-server/server/get-page-files'
|
||||
import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-entrypoint'
|
||||
import { ampFirstEntryNamesMap } from './next-drop-client-page-plugin'
|
||||
import { Rewrite } from '../../../lib/load-custom-routes'
|
||||
import { getSortedRoutes } from '../../../next-server/lib/router/utils'
|
||||
|
||||
const isWebpack5 = parseInt(webpack.version!) === 5
|
||||
|
||||
|
@ -21,12 +23,19 @@ export type ClientBuildManifest = Record<string, string[]>
|
|||
// reduced version to send to the client.
|
||||
function generateClientManifest(
|
||||
assetMap: BuildManifest,
|
||||
isModern: boolean
|
||||
isModern: boolean,
|
||||
rewrites: Rewrite[]
|
||||
): string {
|
||||
const clientManifest: ClientBuildManifest = {}
|
||||
const clientManifest: ClientBuildManifest = {
|
||||
// TODO: update manifest type to include rewrites
|
||||
__rewrites: rewrites as any,
|
||||
}
|
||||
const appDependencies = new Set(assetMap.pages['/_app'])
|
||||
const sortedPageKeys = getSortedRoutes(Object.keys(assetMap.pages))
|
||||
|
||||
sortedPageKeys.forEach((page) => {
|
||||
const dependencies = assetMap.pages[page]
|
||||
|
||||
Object.entries(assetMap.pages).forEach(([page, dependencies]) => {
|
||||
if (page === '/_app') return
|
||||
// Filter out dependencies in the _app entry, because those will have already
|
||||
// been loaded by the client prior to a navigation event
|
||||
|
@ -41,6 +50,10 @@ function generateClientManifest(
|
|||
clientManifest[page] = filteredDeps
|
||||
}
|
||||
})
|
||||
// provide the sorted pages as an array so we don't rely on the object's keys
|
||||
// being in order and we don't slow down look-up time for page assets
|
||||
clientManifest.sortedPages = sortedPageKeys
|
||||
|
||||
return devalue(clientManifest)
|
||||
}
|
||||
|
||||
|
@ -65,10 +78,25 @@ function getFilesArray(files: any) {
|
|||
export default class BuildManifestPlugin {
|
||||
private buildId: string
|
||||
private modern: boolean
|
||||
private rewrites: Rewrite[]
|
||||
|
||||
constructor(options: { buildId: string; modern: boolean }) {
|
||||
constructor(options: {
|
||||
buildId: string
|
||||
modern: boolean
|
||||
rewrites: Rewrite[]
|
||||
}) {
|
||||
this.buildId = options.buildId
|
||||
this.modern = options.modern
|
||||
this.rewrites = options.rewrites.map((r) => {
|
||||
const rewrite = { ...r }
|
||||
|
||||
// omit external rewrite destinations since these aren't
|
||||
// handled client-side
|
||||
if (!rewrite.destination.startsWith('/')) {
|
||||
delete rewrite.destination
|
||||
}
|
||||
return rewrite
|
||||
})
|
||||
}
|
||||
|
||||
createAssets(compilation: any, assets: any) {
|
||||
|
@ -185,7 +213,8 @@ export default class BuildManifestPlugin {
|
|||
assets[clientManifestPath] = new RawSource(
|
||||
`self.__BUILD_MANIFEST = ${generateClientManifest(
|
||||
assetMap,
|
||||
false
|
||||
false,
|
||||
this.rewrites
|
||||
)};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
|
||||
)
|
||||
|
||||
|
@ -195,7 +224,8 @@ export default class BuildManifestPlugin {
|
|||
assets[modernClientManifestPath] = new RawSource(
|
||||
`self.__BUILD_MANIFEST = ${generateClientManifest(
|
||||
assetMap,
|
||||
true
|
||||
true,
|
||||
this.rewrites
|
||||
)};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,9 +33,11 @@ function EventSourceWrapper(options) {
|
|||
for (var i = 0; i < listeners.length; i++) {
|
||||
listeners[i](event)
|
||||
}
|
||||
if (event.data.indexOf('action') !== -1) {
|
||||
eventCallbacks.forEach((cb) => cb(event))
|
||||
}
|
||||
|
||||
eventCallbacks.forEach((cb) => {
|
||||
if (!cb.unfiltered && event.data.indexOf('action') === -1) return
|
||||
cb(event)
|
||||
})
|
||||
}
|
||||
|
||||
function handleDisconnect() {
|
||||
|
|
|
@ -6,6 +6,7 @@ import initWebpackHMR from './dev/webpack-hot-middleware-client'
|
|||
import initializeBuildWatcher from './dev/dev-build-watcher'
|
||||
import initializePrerenderIndicator from './dev/prerender-indicator'
|
||||
import { displayContent } from './dev/fouc'
|
||||
import { getEventSourceWrapper } from './dev/error-overlay/eventsource'
|
||||
|
||||
// Temporary workaround for the issue described here:
|
||||
// https://github.com/vercel/next.js/issues/3775#issuecomment-407438123
|
||||
|
@ -30,6 +31,22 @@ window.next = next
|
|||
initNext({ webpackHMR })
|
||||
.then(({ renderCtx, render }) => {
|
||||
initOnDemandEntries({ assetPrefix: prefix })
|
||||
|
||||
function devPagesManifestListener(event) {
|
||||
if (event.data.indexOf('devPagesManifest') !== -1) {
|
||||
fetch(`${prefix}/_next/static/development/_devPagesManifest.json`)
|
||||
.then((res) => res.json())
|
||||
.then((manifest) => {
|
||||
window.__DEV_PAGES_MANIFEST = manifest
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`Failed to fetch devPagesManifest`, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
devPagesManifestListener.unfiltered = true
|
||||
getEventSourceWrapper({}).addMessageListener(devPagesManifestListener)
|
||||
|
||||
if (process.env.__NEXT_BUILD_INDICATOR) initializeBuildWatcher()
|
||||
if (
|
||||
process.env.__NEXT_PRERENDER_INDICATOR &&
|
||||
|
|
|
@ -80,6 +80,7 @@ export default class PageLoader {
|
|||
private loadingRoutes: Record<string, boolean>
|
||||
private promisedBuildManifest?: Promise<ClientBuildManifest>
|
||||
private promisedSsgManifest?: Promise<ClientSsgManifest>
|
||||
private promisedDevPagesManifest?: Promise<any>
|
||||
|
||||
constructor(buildId: string, assetPrefix: string, initialPage: string) {
|
||||
this.buildId = buildId
|
||||
|
@ -97,17 +98,16 @@ export default class PageLoader {
|
|||
this.loadingRoutes[initialPage] = true
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
this.promisedBuildManifest = new Promise((resolve) => {
|
||||
if ((window as any).__BUILD_MANIFEST) {
|
||||
this.promisedBuildManifest = new Promise((resolve) => {
|
||||
if ((window as any).__BUILD_MANIFEST) {
|
||||
resolve((window as any).__BUILD_MANIFEST)
|
||||
} else {
|
||||
;(window as any).__BUILD_MANIFEST_CB = () => {
|
||||
resolve((window as any).__BUILD_MANIFEST)
|
||||
} else {
|
||||
;(window as any).__BUILD_MANIFEST_CB = () => {
|
||||
resolve((window as any).__BUILD_MANIFEST)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** @type {Promise<Set<string>>} */
|
||||
this.promisedSsgManifest = new Promise((resolve) => {
|
||||
if ((window as any).__SSG_MANIFEST) {
|
||||
|
@ -120,6 +120,33 @@ export default class PageLoader {
|
|||
})
|
||||
}
|
||||
|
||||
getPageList() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return this.promisedBuildManifest!.then(
|
||||
(buildManifest) => buildManifest.sortedPages
|
||||
)
|
||||
} else {
|
||||
if ((window as any).__DEV_PAGES_MANIFEST) {
|
||||
return (window as any).__DEV_PAGES_MANIFEST.pages
|
||||
} else {
|
||||
if (!this.promisedDevPagesManifest) {
|
||||
this.promisedDevPagesManifest = fetch(
|
||||
`${this.assetPrefix}/_next/static/development/_devPagesManifest.json`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((manifest) => {
|
||||
;(window as any).__DEV_PAGES_MANIFEST = manifest
|
||||
return manifest.pages
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`Failed to fetch devPagesManifest`, err)
|
||||
})
|
||||
}
|
||||
return this.promisedDevPagesManifest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise for the dependencies for a particular route
|
||||
getDependencies(route: string): Promise<string[]> {
|
||||
return this.promisedBuildManifest!.then((m) => {
|
||||
|
|
|
@ -8,6 +8,7 @@ export const EXPORT_MARKER = 'export-marker.json'
|
|||
export const EXPORT_DETAIL = 'export-detail.json'
|
||||
export const PRERENDER_MANIFEST = 'prerender-manifest.json'
|
||||
export const ROUTES_MANIFEST = 'routes-manifest.json'
|
||||
export const DEV_CLIENT_PAGES_MANIFEST = '_devPagesManifest.json'
|
||||
export const REACT_LOADABLE_MANIFEST = 'react-loadable-manifest.json'
|
||||
export const FONT_MANIFEST = 'font-manifest.json'
|
||||
export const SERVER_DIRECTORY = 'server'
|
||||
|
|
|
@ -23,6 +23,8 @@ import { parseRelativeUrl } from './utils/parse-relative-url'
|
|||
import { searchParamsToUrlQuery } from './utils/querystring'
|
||||
import { getRouteMatcher } from './utils/route-matcher'
|
||||
import { getRouteRegex } from './utils/route-regex'
|
||||
import { denormalizePagePath } from '../../server/denormalize-page-path'
|
||||
import resolveRewrites from './utils/resolve-rewrites'
|
||||
|
||||
interface TransitionOptions {
|
||||
shallow?: boolean
|
||||
|
@ -465,6 +467,7 @@ export default class Router implements BaseRouter {
|
|||
if (!(options as any)._h && this.onlyAHashChange(cleanedAs)) {
|
||||
this.asPath = cleanedAs
|
||||
Router.events.emit('hashChangeStart', as)
|
||||
// TODO: do we need the resolved href when only a hash change?
|
||||
this.changeState(method, url, as, options)
|
||||
this.scrollToHash(cleanedAs)
|
||||
this.notify(this.components[this.route])
|
||||
|
@ -472,11 +475,25 @@ export default class Router implements BaseRouter {
|
|||
return true
|
||||
}
|
||||
|
||||
const parsed = tryParseRelativeUrl(url)
|
||||
// The build manifest needs to be loaded before auto-static dynamic pages
|
||||
// get their query parameters to allow ensuring they can be parsed properly
|
||||
// when rewritten to
|
||||
const pages = await this.pageLoader.getPageList()
|
||||
const { __rewrites: rewrites } = await this.pageLoader.promisedBuildManifest
|
||||
|
||||
let parsed = tryParseRelativeUrl(url)
|
||||
|
||||
if (!parsed) return false
|
||||
|
||||
let { pathname, searchParams } = parsed
|
||||
|
||||
parsed = this._resolveHref(parsed, pages) as typeof parsed
|
||||
|
||||
if (parsed.pathname !== pathname) {
|
||||
pathname = parsed.pathname
|
||||
url = formatWithValidation(parsed)
|
||||
}
|
||||
|
||||
const query = searchParamsToUrlQuery(searchParams)
|
||||
|
||||
// url and as should always be prefixed with basePath by this
|
||||
|
@ -498,8 +515,17 @@ export default class Router implements BaseRouter {
|
|||
const route = removePathTrailingSlash(pathname)
|
||||
const { shallow = false } = options
|
||||
|
||||
// we need to resolve the as value using rewrites for dynamic SSG
|
||||
// pages to allow building the data URL correctly
|
||||
let resolvedAs = as
|
||||
|
||||
if (process.env.__NEXT_HAS_REWRITES) {
|
||||
resolvedAs = resolveRewrites(as, pages, basePath, rewrites, query)
|
||||
}
|
||||
resolvedAs = delBasePath(resolvedAs)
|
||||
|
||||
if (isDynamicRoute(route)) {
|
||||
const { pathname: asPathname } = parseRelativeUrl(cleanedAs)
|
||||
const { pathname: asPathname } = parseRelativeUrl(resolvedAs)
|
||||
const routeRegex = getRouteRegex(route)
|
||||
const routeMatch = getRouteMatcher(routeRegex)(asPathname)
|
||||
if (!routeMatch) {
|
||||
|
@ -795,6 +821,29 @@ export default class Router implements BaseRouter {
|
|||
return this.asPath !== asPath
|
||||
}
|
||||
|
||||
_resolveHref(parsedHref: UrlObject, pages: string[]) {
|
||||
const { pathname } = parsedHref
|
||||
const cleanPathname = denormalizePagePath(delBasePath(pathname!))
|
||||
|
||||
if (cleanPathname === '/404' || cleanPathname === '/_error') {
|
||||
return parsedHref
|
||||
}
|
||||
|
||||
// handle resolving href for dynamic routes
|
||||
if (!pages.includes(cleanPathname!)) {
|
||||
for (let page of pages) {
|
||||
if (
|
||||
isDynamicRoute(page) &&
|
||||
getRouteRegex(page).re.test(cleanPathname!)
|
||||
) {
|
||||
parsedHref.pathname = addBasePath(page)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsedHref
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch page code, you may wait for the data during page rendering.
|
||||
* This feature only works in production!
|
||||
|
@ -806,11 +855,20 @@ export default class Router implements BaseRouter {
|
|||
asPath: string = url,
|
||||
options: PrefetchOptions = {}
|
||||
): Promise<void> {
|
||||
const parsed = tryParseRelativeUrl(url)
|
||||
let parsed = tryParseRelativeUrl(url)
|
||||
|
||||
if (!parsed) return
|
||||
|
||||
const { pathname } = parsed
|
||||
let { pathname } = parsed
|
||||
|
||||
const pages = await this.pageLoader.getPageList()
|
||||
|
||||
parsed = this._resolveHref(parsed, pages) as typeof parsed
|
||||
|
||||
if (parsed.pathname !== pathname) {
|
||||
pathname = parsed.pathname
|
||||
url = formatWithValidation(parsed)
|
||||
}
|
||||
|
||||
// Prefetch is not supported in development mode because it would trigger on-demand-entries
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { searchParamsToUrlQuery } from './querystring'
|
||||
import { parseRelativeUrl } from './parse-relative-url'
|
||||
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
|
||||
|
||||
type Params = { [param: string]: any }
|
||||
|
||||
export default function prepareDestination(
|
||||
destination: string,
|
||||
params: Params,
|
||||
query: ParsedUrlQuery,
|
||||
appendParamsToQuery: boolean,
|
||||
basePath: string
|
||||
) {
|
||||
let parsedDestination: {
|
||||
query?: ParsedUrlQuery
|
||||
protocol?: string
|
||||
hostname?: string
|
||||
port?: string
|
||||
} & ReturnType<typeof parseRelativeUrl> = {} as any
|
||||
|
||||
if (destination.startsWith('/')) {
|
||||
parsedDestination = parseRelativeUrl(destination)
|
||||
} else {
|
||||
const {
|
||||
pathname,
|
||||
searchParams,
|
||||
hash,
|
||||
hostname,
|
||||
port,
|
||||
protocol,
|
||||
search,
|
||||
href,
|
||||
} = new URL(destination)
|
||||
|
||||
parsedDestination = {
|
||||
pathname,
|
||||
searchParams,
|
||||
hash,
|
||||
protocol,
|
||||
hostname,
|
||||
port,
|
||||
search,
|
||||
href,
|
||||
}
|
||||
}
|
||||
|
||||
parsedDestination.query = searchParamsToUrlQuery(
|
||||
parsedDestination.searchParams
|
||||
)
|
||||
const destQuery = parsedDestination.query
|
||||
|
||||
let destinationCompiler = compilePathToRegex(
|
||||
`${parsedDestination.pathname!}${parsedDestination.hash || ''}`,
|
||||
// we don't validate while compiling the destination since we should
|
||||
// have already validated before we got to this point and validating
|
||||
// breaks compiling destinations with named pattern params from the source
|
||||
// e.g. /something:hello(.*) -> /another/:hello is broken with validation
|
||||
// since compile validation is meant for reversing and not for inserting
|
||||
// params from a separate path-regex into another
|
||||
{ validate: false }
|
||||
)
|
||||
let newUrl
|
||||
|
||||
// update any params in query values
|
||||
for (const [key, strOrArray] of Object.entries(destQuery)) {
|
||||
let value = Array.isArray(strOrArray) ? strOrArray[0] : strOrArray
|
||||
if (value) {
|
||||
// the value needs to start with a forward-slash to be compiled
|
||||
// correctly
|
||||
value = `/${value}`
|
||||
const queryCompiler = compilePathToRegex(value, { validate: false })
|
||||
value = queryCompiler(params).substr(1)
|
||||
}
|
||||
destQuery[key] = value
|
||||
}
|
||||
|
||||
// add path params to query if it's not a redirect and not
|
||||
// already defined in destination query
|
||||
if (appendParamsToQuery) {
|
||||
for (const [name, value] of Object.entries(params)) {
|
||||
if (!(name in destQuery)) {
|
||||
destQuery[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shouldAddBasePath = destination.startsWith('/') && basePath
|
||||
|
||||
try {
|
||||
newUrl = `${shouldAddBasePath ? basePath : ''}${encodeURI(
|
||||
destinationCompiler(params)
|
||||
)}`
|
||||
|
||||
const [pathname, hash] = newUrl.split('#')
|
||||
parsedDestination.pathname = pathname
|
||||
parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}`
|
||||
delete parsedDestination.search
|
||||
delete parsedDestination.searchParams
|
||||
} catch (err) {
|
||||
if (err.message.match(/Expected .*? to not repeat, but got an array/)) {
|
||||
throw new Error(
|
||||
`To use a multi-match in the destination you must add \`*\` at the end of the param name to signify it should repeat. https://err.sh/vercel/next.js/invalid-multi-match`
|
||||
)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
// Query merge order lowest priority to highest
|
||||
// 1. initial URL query values
|
||||
// 2. path segment values
|
||||
// 3. destination specified query values
|
||||
parsedDestination.query = {
|
||||
...query,
|
||||
...parsedDestination.query,
|
||||
}
|
||||
|
||||
return {
|
||||
newUrl,
|
||||
parsedDestination,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { ParsedUrlQuery } from 'querystring'
|
||||
import pathMatch from '../../../server/lib/path-match'
|
||||
import prepareDestination from './prepare-destination'
|
||||
import { Rewrite } from '../../../../lib/load-custom-routes'
|
||||
|
||||
const customRouteMatcher = pathMatch(true)
|
||||
|
||||
export default function resolveRewrites(
|
||||
asPath: string,
|
||||
pages: string[],
|
||||
basePath: string,
|
||||
rewrites: Rewrite[],
|
||||
query: ParsedUrlQuery
|
||||
) {
|
||||
if (!pages.includes(asPath)) {
|
||||
for (const rewrite of rewrites) {
|
||||
const matcher = customRouteMatcher(rewrite.source)
|
||||
const params = matcher(asPath)
|
||||
|
||||
if (params) {
|
||||
if (!rewrite.destination) {
|
||||
// this is a proxied rewrite which isn't handled on the client
|
||||
break
|
||||
}
|
||||
const destRes = prepareDestination(
|
||||
rewrite.destination,
|
||||
params,
|
||||
query,
|
||||
true,
|
||||
rewrite.basePath === false ? '' : basePath
|
||||
)
|
||||
asPath = destRes.parsedDestination.pathname!
|
||||
Object.assign(query, destRes.parsedDestination.query)
|
||||
|
||||
if (pages.includes(asPath)) {
|
||||
// check if we now match a page as this means we are done
|
||||
// resolving the rewrites
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return asPath
|
||||
}
|
13
packages/next/next-server/server/denormalize-page-path.ts
Normal file
13
packages/next/next-server/server/denormalize-page-path.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export function normalizePathSep(path: string): string {
|
||||
return path.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
export function denormalizePagePath(page: string) {
|
||||
page = normalizePathSep(page)
|
||||
if (page.startsWith('/index/')) {
|
||||
page = page.slice(6)
|
||||
} else if (page === '/index') {
|
||||
page = '/'
|
||||
}
|
||||
return page
|
||||
}
|
|
@ -2,16 +2,25 @@ import * as pathToRegexp from 'next/dist/compiled/path-to-regexp'
|
|||
|
||||
export { pathToRegexp }
|
||||
|
||||
export const matcherOptions = {
|
||||
sensitive: false,
|
||||
delimiter: '/',
|
||||
decode: decodeParam,
|
||||
}
|
||||
|
||||
export const customRouteMatcherOptions = {
|
||||
...matcherOptions,
|
||||
strict: true,
|
||||
}
|
||||
|
||||
export default (customRoute = false) => {
|
||||
return (path: string) => {
|
||||
const keys: pathToRegexp.Key[] = []
|
||||
const matcherOptions = {
|
||||
sensitive: false,
|
||||
delimiter: '/',
|
||||
...(customRoute ? { strict: true } : undefined),
|
||||
decode: decodeParam,
|
||||
}
|
||||
const matcherRegex = pathToRegexp.pathToRegexp(path, keys, matcherOptions)
|
||||
const matcherRegex = pathToRegexp.pathToRegexp(
|
||||
path,
|
||||
keys,
|
||||
customRoute ? customRouteMatcherOptions : matcherOptions
|
||||
)
|
||||
const matcher = pathToRegexp.regexpToFunction(
|
||||
matcherRegex,
|
||||
keys,
|
||||
|
|
|
@ -48,10 +48,10 @@ import Router, {
|
|||
DynamicRoutes,
|
||||
PageChecker,
|
||||
Params,
|
||||
prepareDestination,
|
||||
route,
|
||||
Route,
|
||||
} from './router'
|
||||
import prepareDestination from '../lib/router/utils/prepare-destination'
|
||||
import { sendPayload } from './send-payload'
|
||||
import { serveStatic } from './serve-static'
|
||||
import { IncrementalCache } from './incremental-cache'
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { posix } from 'path'
|
||||
|
||||
export function normalizePathSep(path: string): string {
|
||||
return path.replace(/\\/g, '/')
|
||||
}
|
||||
export { normalizePathSep, denormalizePagePath } from './denormalize-page-path'
|
||||
|
||||
export function normalizePagePath(page: string): string {
|
||||
// If the page is `/` we need to append `/index`, otherwise the returned directory root will be bundles instead of pages
|
||||
|
@ -24,13 +22,3 @@ export function normalizePagePath(page: string): string {
|
|||
}
|
||||
return page
|
||||
}
|
||||
|
||||
export function denormalizePagePath(page: string) {
|
||||
page = normalizePathSep(page)
|
||||
if (page.startsWith('/index/')) {
|
||||
page = page.slice(6)
|
||||
} else if (page === '/index') {
|
||||
page = '/'
|
||||
}
|
||||
return page
|
||||
}
|
||||
|
|
|
@ -329,7 +329,6 @@ export async function renderToHTML(
|
|||
|
||||
const headTags = (...args: any) => callMiddleware('headTags', args)
|
||||
|
||||
const didRewrite = (req as any)._nextDidRewrite
|
||||
const isFallback = !!query.__nextFallback
|
||||
delete query.__nextFallback
|
||||
|
||||
|
@ -360,23 +359,6 @@ export async function renderToHTML(
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
(isAutoExport || isFallback) &&
|
||||
pageIsDynamic &&
|
||||
didRewrite
|
||||
) {
|
||||
// TODO: If we decide to ship rewrites to the client we could
|
||||
// solve this by running over the rewrites and getting the params.
|
||||
throw new Error(
|
||||
`Rewrites don't support${
|
||||
isFallback ? ' ' : ' auto-exported '
|
||||
}dynamic pages${isFallback ? ' with getStaticProps ' : ' '}yet.\n` +
|
||||
`Using this will cause the page to fail to parse the params on the client\n` +
|
||||
`See more info: https://err.sh/next.js/rewrite-auto-export-fallback`
|
||||
)
|
||||
}
|
||||
|
||||
if (hasPageGetInitialProps && isSSG) {
|
||||
throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT + ` ${pathname}`)
|
||||
}
|
||||
|
@ -690,6 +672,27 @@ export async function renderToHTML(
|
|||
// the response might be finished on the getInitialProps call
|
||||
if (isResSent(res) && !isSSG) return null
|
||||
|
||||
// we preload the buildManifest for auto-export dynamic pages
|
||||
// to speed up hydrating query values
|
||||
let filteredBuildManifest = buildManifest
|
||||
const additionalFiles: string[] = []
|
||||
|
||||
if (isAutoExport && pageIsDynamic) {
|
||||
filteredBuildManifest = {
|
||||
...buildManifest,
|
||||
lowPriorityFiles: buildManifest.lowPriorityFiles.filter((file) => {
|
||||
if (
|
||||
file.endsWith('_buildManifest.js') ||
|
||||
file.endsWith('_buildManifest.module.js')
|
||||
) {
|
||||
additionalFiles.push(file)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// AMP First pages do not have client-side JavaScript files
|
||||
const files = ampState.ampFirst
|
||||
? []
|
||||
|
@ -700,6 +703,7 @@ export async function renderToHTML(
|
|||
? getPageFiles(buildManifest, pathname)
|
||||
: []),
|
||||
]),
|
||||
...additionalFiles,
|
||||
]
|
||||
|
||||
const renderPage: RenderPage = (
|
||||
|
@ -766,6 +770,7 @@ export async function renderToHTML(
|
|||
|
||||
let html = renderDocument(Document, {
|
||||
...renderOpts,
|
||||
buildManifest: filteredBuildManifest,
|
||||
// Only enabled in production as development mode has features relying on HMR (style injection for example)
|
||||
unstable_runtimeJS:
|
||||
process.env.NODE_ENV === 'production'
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { parse as parseUrl, UrlWithParsedQuery } from 'url'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
|
||||
import { UrlWithParsedQuery } from 'url'
|
||||
|
||||
import pathMatch from './lib/path-match'
|
||||
|
||||
export const route = pathMatch()
|
||||
|
@ -37,86 +36,6 @@ export type PageChecker = (pathname: string) => Promise<boolean>
|
|||
|
||||
const customRouteTypes = new Set(['rewrite', 'redirect', 'header'])
|
||||
|
||||
export const prepareDestination = (
|
||||
destination: string,
|
||||
params: Params,
|
||||
query: ParsedUrlQuery,
|
||||
appendParamsToQuery: boolean,
|
||||
basePath: string
|
||||
) => {
|
||||
const parsedDestination = parseUrl(destination, true)
|
||||
const destQuery = parsedDestination.query
|
||||
let destinationCompiler = compilePathToRegex(
|
||||
`${parsedDestination.pathname!}${parsedDestination.hash || ''}`,
|
||||
// we don't validate while compiling the destination since we should
|
||||
// have already validated before we got to this point and validating
|
||||
// breaks compiling destinations with named pattern params from the source
|
||||
// e.g. /something:hello(.*) -> /another/:hello is broken with validation
|
||||
// since compile validation is meant for reversing and not for inserting
|
||||
// params from a separate path-regex into another
|
||||
{ validate: false }
|
||||
)
|
||||
let newUrl
|
||||
|
||||
// update any params in query values
|
||||
for (const [key, strOrArray] of Object.entries(destQuery)) {
|
||||
let value = Array.isArray(strOrArray) ? strOrArray[0] : strOrArray
|
||||
if (value) {
|
||||
// the value needs to start with a forward-slash to be compiled
|
||||
// correctly
|
||||
value = `/${value}`
|
||||
const queryCompiler = compilePathToRegex(value, { validate: false })
|
||||
value = queryCompiler(params).substr(1)
|
||||
}
|
||||
destQuery[key] = value
|
||||
}
|
||||
|
||||
// add path params to query if it's not a redirect and not
|
||||
// already defined in destination query
|
||||
if (appendParamsToQuery) {
|
||||
for (const [name, value] of Object.entries(params)) {
|
||||
if (!(name in destQuery)) {
|
||||
destQuery[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shouldAddBasePath = destination.startsWith('/') && basePath
|
||||
|
||||
try {
|
||||
newUrl = `${shouldAddBasePath ? basePath : ''}${encodeURI(
|
||||
destinationCompiler(params)
|
||||
)}`
|
||||
|
||||
const [pathname, hash] = newUrl.split('#')
|
||||
parsedDestination.pathname = pathname
|
||||
parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}`
|
||||
parsedDestination.path = `${pathname}${parsedDestination.search}`
|
||||
delete parsedDestination.search
|
||||
} catch (err) {
|
||||
if (err.message.match(/Expected .*? to not repeat, but got an array/)) {
|
||||
throw new Error(
|
||||
`To use a multi-match in the destination you must add \`*\` at the end of the param name to signify it should repeat. https://err.sh/vercel/next.js/invalid-multi-match`
|
||||
)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
// Query merge order lowest priority to highest
|
||||
// 1. initial URL query values
|
||||
// 2. path segment values
|
||||
// 3. destination specified query values
|
||||
parsedDestination.query = {
|
||||
...query,
|
||||
...parsedDestination.query,
|
||||
}
|
||||
|
||||
return {
|
||||
newUrl,
|
||||
parsedDestination,
|
||||
}
|
||||
}
|
||||
|
||||
function replaceBasePath(basePath: string, pathname: string) {
|
||||
return pathname!.replace(basePath, '') || '/'
|
||||
}
|
||||
|
|
|
@ -549,6 +549,7 @@ export class NextScript extends Component<OriginProps> {
|
|||
? { type: 'module' }
|
||||
: { noModule: true }
|
||||
}
|
||||
|
||||
return (
|
||||
<script
|
||||
key={file}
|
||||
|
|
|
@ -31,6 +31,7 @@ import getRouteFromEntrypoint from '../next-server/server/get-route-from-entrypo
|
|||
import { isWriteable } from '../build/is-writeable'
|
||||
import { ClientPagesLoaderOptions } from '../build/webpack/loaders/next-client-pages-loader'
|
||||
import { stringify } from 'querystring'
|
||||
import { Rewrite } from '../lib/load-custom-routes'
|
||||
|
||||
export async function renderScriptError(
|
||||
res: ServerResponse,
|
||||
|
@ -152,6 +153,7 @@ export default class HotReloader {
|
|||
private onDemandEntries: any
|
||||
private previewProps: __ApiPreviewProps
|
||||
private watcher: any
|
||||
private rewrites: Rewrite[]
|
||||
|
||||
constructor(
|
||||
dir: string,
|
||||
|
@ -160,11 +162,13 @@ export default class HotReloader {
|
|||
pagesDir,
|
||||
buildId,
|
||||
previewProps,
|
||||
rewrites,
|
||||
}: {
|
||||
config: object
|
||||
pagesDir: string
|
||||
buildId: string
|
||||
previewProps: __ApiPreviewProps
|
||||
rewrites: Rewrite[]
|
||||
}
|
||||
) {
|
||||
this.buildId = buildId
|
||||
|
@ -178,6 +182,7 @@ export default class HotReloader {
|
|||
|
||||
this.config = config
|
||||
this.previewProps = previewProps
|
||||
this.rewrites = rewrites
|
||||
}
|
||||
|
||||
public async run(
|
||||
|
@ -285,6 +290,7 @@ export default class HotReloader {
|
|||
config: this.config,
|
||||
buildId: this.buildId,
|
||||
pagesDir: this.pagesDir,
|
||||
rewrites: this.rewrites,
|
||||
entrypoints: { ...entrypoints.client, ...additionalClientEntrypoints },
|
||||
}),
|
||||
getBaseWebpackConfig(this.dir, {
|
||||
|
@ -293,6 +299,7 @@ export default class HotReloader {
|
|||
config: this.config,
|
||||
buildId: this.buildId,
|
||||
pagesDir: this.pagesDir,
|
||||
rewrites: this.rewrites,
|
||||
entrypoints: entrypoints.server,
|
||||
}),
|
||||
])
|
||||
|
@ -509,7 +516,7 @@ export default class HotReloader {
|
|||
return []
|
||||
}
|
||||
|
||||
private send(action: string, ...args: any[]): void {
|
||||
public send(action?: string, ...args: any[]): void {
|
||||
this.webpackHotMiddleware!.publish({ action, data: args })
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,11 @@ import { fileExists } from '../lib/file-exists'
|
|||
import { findPagesDir } from '../lib/find-pages-dir'
|
||||
import loadCustomRoutes, { CustomRoutes } from '../lib/load-custom-routes'
|
||||
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
|
||||
import { PHASE_DEVELOPMENT_SERVER } from '../next-server/lib/constants'
|
||||
import {
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
CLIENT_STATIC_FILES_PATH,
|
||||
DEV_CLIENT_PAGES_MANIFEST,
|
||||
} from '../next-server/lib/constants'
|
||||
import {
|
||||
getRouteMatcher,
|
||||
getRouteRegex,
|
||||
|
@ -46,6 +50,8 @@ export default class DevServer extends Server {
|
|||
private webpackWatcher?: Watchpack | null
|
||||
private hotReloader?: HotReloader
|
||||
private isCustomServer: boolean
|
||||
protected sortedRoutes?: string[]
|
||||
|
||||
protected staticPathsWorker: import('jest-worker').default & {
|
||||
loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths
|
||||
}
|
||||
|
@ -206,8 +212,22 @@ export default class DevServer extends Server {
|
|||
|
||||
routedPages.push(pageName)
|
||||
}
|
||||
|
||||
try {
|
||||
this.dynamicRoutes = getSortedRoutes(routedPages)
|
||||
// we serve a separate manifest with all pages for the client in
|
||||
// dev mode so that we can match a page after a rewrite on the client
|
||||
// before it has been built and is populated in the _buildManifest
|
||||
const sortedRoutes = getSortedRoutes(routedPages)
|
||||
|
||||
if (
|
||||
!this.sortedRoutes?.every((val, idx) => val === sortedRoutes[idx])
|
||||
) {
|
||||
// emit the change so clients fetch the update
|
||||
this.hotReloader!.send(undefined, { devPagesManifest: true })
|
||||
}
|
||||
this.sortedRoutes = sortedRoutes
|
||||
|
||||
this.dynamicRoutes = this.sortedRoutes
|
||||
.filter(isDynamicRoute)
|
||||
.map((page) => ({
|
||||
page,
|
||||
|
@ -257,6 +277,7 @@ export default class DevServer extends Server {
|
|||
config: this.nextConfig,
|
||||
previewProps: this.getPreviewProps(),
|
||||
buildId: this.buildId,
|
||||
rewrites: this.customRoutes.rewrites,
|
||||
})
|
||||
await super.prepare()
|
||||
await this.addExportPathMapRoutes()
|
||||
|
@ -411,6 +432,26 @@ export default class DevServer extends Server {
|
|||
},
|
||||
})
|
||||
|
||||
fsRoutes.unshift({
|
||||
match: route(
|
||||
`/_next/${CLIENT_STATIC_FILES_PATH}/${this.buildId}/${DEV_CLIENT_PAGES_MANIFEST}`
|
||||
),
|
||||
type: 'route',
|
||||
name: `_next/${CLIENT_STATIC_FILES_PATH}/${this.buildId}/${DEV_CLIENT_PAGES_MANIFEST}`,
|
||||
fn: async (_req, res) => {
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
pages: this.sortedRoutes,
|
||||
})
|
||||
)
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
fsRoutes.push({
|
||||
match: route('/:path*'),
|
||||
type: 'route',
|
||||
|
|
|
@ -98,13 +98,13 @@ describe('Build Output', () => {
|
|||
expect(parseFloat(indexFirstLoad) - 60).toBeLessThanOrEqual(0)
|
||||
expect(indexFirstLoad.endsWith('kB')).toBe(true)
|
||||
|
||||
expect(parseFloat(err404Size) - 3.6).toBeLessThanOrEqual(0)
|
||||
expect(parseFloat(err404Size) - 3.8).toBeLessThanOrEqual(0)
|
||||
expect(err404Size.endsWith('kB')).toBe(true)
|
||||
|
||||
expect(parseFloat(err404FirstLoad) - 63).toBeLessThanOrEqual(0)
|
||||
expect(err404FirstLoad.endsWith('kB')).toBe(true)
|
||||
|
||||
expect(parseFloat(sharedByAll) - 59.4).toBeLessThanOrEqual(0)
|
||||
expect(parseFloat(sharedByAll) - 59.6).toBeLessThanOrEqual(0)
|
||||
expect(sharedByAll.endsWith('kB')).toBe(true)
|
||||
|
||||
if (_appSize.endsWith('kB')) {
|
||||
|
|
|
@ -10,6 +10,10 @@ module.exports = {
|
|||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
source: '/rewriting-to-auto-export',
|
||||
destination: '/auto-export/hello',
|
||||
},
|
||||
{
|
||||
source: '/to-another',
|
||||
destination: '/another/one',
|
||||
|
|
|
@ -36,6 +36,12 @@ let appPort
|
|||
let app
|
||||
|
||||
const runTests = (isDev = false) => {
|
||||
it('should parse params correctly for rewrite to auto-export dynamic page', async () => {
|
||||
const browser = await webdriver(appPort, '/rewriting-to-auto-export')
|
||||
const text = await browser.eval(() => document.documentElement.innerHTML)
|
||||
expect(text).toContain('auto-export hello')
|
||||
})
|
||||
|
||||
it('should handle one-to-one rewrite successfully', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/first')
|
||||
expect(html).toMatch(/hello/)
|
||||
|
@ -512,6 +518,7 @@ const runTests = (isDev = false) => {
|
|||
version: 3,
|
||||
pages404: true,
|
||||
basePath: '',
|
||||
dataRoutes: [],
|
||||
redirects: [
|
||||
{
|
||||
destination: '/:path+',
|
||||
|
@ -804,6 +811,11 @@ const runTests = (isDev = false) => {
|
|||
},
|
||||
],
|
||||
rewrites: [
|
||||
{
|
||||
destination: '/auto-export/hello',
|
||||
regex: normalizeRegEx('^\\/rewriting-to-auto-export$'),
|
||||
source: '/rewriting-to-auto-export',
|
||||
},
|
||||
{
|
||||
destination: '/another/one',
|
||||
regex: normalizeRegEx('^\\/to-another$'),
|
||||
|
@ -996,13 +1008,6 @@ const runTests = (isDev = false) => {
|
|||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
it('should show error for dynamic auto export rewrite', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/to-another')
|
||||
expect(html).toContain(
|
||||
`Rewrites don't support auto-exported dynamic pages yet`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1033,6 +1038,16 @@ describe('Custom routes', () => {
|
|||
await fs.writeFile(nextConfigPath, nextConfigRestoreContent)
|
||||
})
|
||||
|
||||
describe('dev mode', () => {
|
||||
beforeAll(async () => {
|
||||
appPort = await findPort()
|
||||
app = await launchApp(appDir, appPort)
|
||||
buildId = 'development'
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
runTests(true)
|
||||
})
|
||||
|
||||
describe('no-op rewrite', () => {
|
||||
beforeAll(async () => {
|
||||
appPort = await findPort()
|
||||
|
@ -1044,26 +1059,13 @@ describe('Custom routes', () => {
|
|||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
it('should not show error for no-op rewrite and auto export dynamic route', async () => {
|
||||
it('should not error for no-op rewrite and auto export dynamic route', async () => {
|
||||
const browser = await webdriver(appPort, '/auto-export/my-slug')
|
||||
const html = await browser.eval(() => document.documentElement.innerHTML)
|
||||
expect(html).not.toContain(
|
||||
`Rewrites don't support auto-exported dynamic pages yet`
|
||||
)
|
||||
expect(html).toContain(`auto-export my-slug`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dev mode', () => {
|
||||
beforeAll(async () => {
|
||||
appPort = await findPort()
|
||||
app = await launchApp(appDir, appPort)
|
||||
buildId = 'development'
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
runTests(true)
|
||||
})
|
||||
|
||||
describe('server mode', () => {
|
||||
beforeAll(async () => {
|
||||
const { stdout: buildStdout } = await nextBuild(appDir, [], {
|
||||
|
|
|
@ -9,6 +9,10 @@ const Page = () => {
|
|||
<a id="view-post-1">View post 1</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/post-1">
|
||||
<a id="view-post-1-no-as">View post 1</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/[name]/comments" as="/post-1/comments">
|
||||
<a id="view-post-1-comments">View post 1 comments</a>
|
||||
</Link>
|
||||
|
@ -17,6 +21,14 @@ const Page = () => {
|
|||
<a id="view-post-1-comment-1">View comment 1 on post 1</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/post-1/comment-1">
|
||||
<a id="view-post-1-comment-1-no-as">View comment 1 on post 1</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/added-later/first">
|
||||
<a id="added-later-link">/added-later/first</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="/blog/[post]/comment/[id]" as="/blog/321/comment/123">
|
||||
<a id="view-nested-dynamic-cmnt">View comment 123 on blog post 321</a>
|
||||
</Link>
|
||||
|
@ -49,6 +61,9 @@ const Page = () => {
|
|||
<Link href="/p1/p2/all-ssg/[...rest]" as="/p1/p2/all-ssg/hello1/hello2">
|
||||
<a id="ssg-catch-all-multi">Catch-all route (multi)</a>
|
||||
</Link>
|
||||
<Link href="/p1/p2/all-ssg/hello1/hello2">
|
||||
<a id="ssg-catch-all-multi-no-as">Catch-all route (multi)</a>
|
||||
</Link>
|
||||
<Link
|
||||
href="/p1/p2/nested-all-ssg/[...rest]"
|
||||
as="/p1/p2/nested-all-ssg/hello"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
import webdriver from 'next-webdriver'
|
||||
import { join } from 'path'
|
||||
import { join, dirname } from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import {
|
||||
renderViaHTTP,
|
||||
|
@ -13,6 +13,7 @@ import {
|
|||
nextBuild,
|
||||
nextStart,
|
||||
normalizeRegEx,
|
||||
check,
|
||||
} from 'next-test-utils'
|
||||
import cheerio from 'cheerio'
|
||||
import escapeRegex from 'escape-string-regexp'
|
||||
|
@ -86,9 +87,29 @@ function runTests(dev) {
|
|||
let browser
|
||||
try {
|
||||
browser = await webdriver(appPort, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.elementByCss('#view-post-1').click()
|
||||
await browser.waitForElementByCss('#asdf')
|
||||
|
||||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||||
|
||||
const text = await browser.elementByCss('#asdf').text()
|
||||
expect(text).toMatch(/this is.*?post-1/i)
|
||||
} finally {
|
||||
if (browser) await browser.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('should navigate to a dynamic page successfully no as', async () => {
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(appPort, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.elementByCss('#view-post-1-no-as').click()
|
||||
await browser.waitForElementByCss('#asdf')
|
||||
|
||||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||||
|
||||
const text = await browser.elementByCss('#asdf').text()
|
||||
expect(text).toMatch(/this is.*?post-1/i)
|
||||
} finally {
|
||||
|
@ -137,9 +158,29 @@ function runTests(dev) {
|
|||
let browser
|
||||
try {
|
||||
browser = await webdriver(appPort, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.elementByCss('#view-post-1-comment-1').click()
|
||||
await browser.waitForElementByCss('#asdf')
|
||||
|
||||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||||
|
||||
const text = await browser.elementByCss('#asdf').text()
|
||||
expect(text).toMatch(/i am.*comment-1.*on.*post-1/i)
|
||||
} finally {
|
||||
if (browser) await browser.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('should navigate to a nested dynamic page successfully no as', async () => {
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(appPort, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.elementByCss('#view-post-1-comment-1-no-as').click()
|
||||
await browser.waitForElementByCss('#asdf')
|
||||
|
||||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||||
|
||||
const text = await browser.elementByCss('#asdf').text()
|
||||
expect(text).toMatch(/i am.*comment-1.*on.*post-1/i)
|
||||
} finally {
|
||||
|
@ -354,9 +395,29 @@ function runTests(dev) {
|
|||
let browser
|
||||
try {
|
||||
browser = await webdriver(appPort, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.elementByCss('#ssg-catch-all-multi').click()
|
||||
await browser.waitForElementByCss('#all-ssg-content')
|
||||
|
||||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||||
|
||||
const text = await browser.elementByCss('#all-ssg-content').text()
|
||||
expect(text).toBe('{"rest":["hello1","hello2"]}')
|
||||
} finally {
|
||||
if (browser) await browser.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('[ssg: catch-all] should pass params in getStaticProps during client navigation (multi) no as', async () => {
|
||||
let browser
|
||||
try {
|
||||
browser = await webdriver(appPort, '/')
|
||||
await browser.eval('window.beforeNav = 1')
|
||||
await browser.elementByCss('#ssg-catch-all-multi-no-as').click()
|
||||
await browser.waitForElementByCss('#all-ssg-content')
|
||||
|
||||
expect(await browser.eval('window.beforeNav')).toBe(1)
|
||||
|
||||
const text = await browser.elementByCss('#all-ssg-content').text()
|
||||
expect(text).toBe('{"rest":["hello1","hello2"]}')
|
||||
} finally {
|
||||
|
@ -501,6 +562,39 @@ function runTests(dev) {
|
|||
})
|
||||
|
||||
if (dev) {
|
||||
it('should resolve dynamic route href for page added later', async () => {
|
||||
const browser = await webdriver(appPort, '/')
|
||||
const addLaterPage = join(appDir, 'pages/added-later/[slug].js')
|
||||
|
||||
await fs.mkdir(dirname(addLaterPage)).catch(() => {})
|
||||
await fs.writeFile(
|
||||
addLaterPage,
|
||||
`
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function Page() {
|
||||
return <p id='added-later'>slug: {useRouter().query.slug}</p>
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
await check(async () => {
|
||||
const contents = await renderViaHTTP(
|
||||
appPort,
|
||||
'/_next/static/development/_devPagesManifest.json'
|
||||
)
|
||||
return contents.includes('added-later') ? 'success' : 'fail'
|
||||
}, 'success')
|
||||
|
||||
await browser.elementByCss('#added-later-link').click()
|
||||
await browser.waitForElementByCss('#added-later')
|
||||
|
||||
const text = await browser.elementByCss('#added-later').text()
|
||||
|
||||
await fs.remove(dirname(addLaterPage))
|
||||
expect(text).toBe('slug: first')
|
||||
})
|
||||
|
||||
it('should work with HMR correctly', async () => {
|
||||
const browser = await webdriver(appPort, '/post-1/comments')
|
||||
let text = await browser.eval(`document.documentElement.innerHTML`)
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
module.exports = {
|
||||
// target
|
||||
rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/some-rewrite/:item',
|
||||
destination: '/blog/post-:item',
|
||||
},
|
||||
{
|
||||
source: '/about',
|
||||
destination: '/lang/en/about',
|
||||
|
|
|
@ -29,15 +29,15 @@ import url from 'url'
|
|||
|
||||
jest.setTimeout(1000 * 60 * 2)
|
||||
const appDir = join(__dirname, '..')
|
||||
const nextConfig = join(appDir, 'next.config.js')
|
||||
const nextConfigPath = join(appDir, 'next.config.js')
|
||||
const indexPage = join(__dirname, '../pages/index.js')
|
||||
const nextConfig = new File(nextConfigPath)
|
||||
let app
|
||||
let appPort
|
||||
let buildId
|
||||
let distPagesDir
|
||||
let exportDir
|
||||
let stderr
|
||||
let origConfig
|
||||
|
||||
const startServer = async (optEnv = {}) => {
|
||||
const scriptPath = join(appDir, 'server.js')
|
||||
|
@ -796,17 +796,27 @@ const runTests = (dev = false, isEmulatedServerless = false) => {
|
|||
expect(await browser.eval('window.beforeNav')).toBe('hi')
|
||||
expect(await browser.elementByCss('#about').text()).toBe('About: en')
|
||||
})
|
||||
|
||||
it('should not error when rewriting to fallback dynamic SSG page', async () => {
|
||||
const item = Math.round(Math.random() * 100)
|
||||
const browser = await webdriver(appPort, `/some-rewrite/${item}`)
|
||||
|
||||
await check(
|
||||
() => browser.elementByCss('p').text(),
|
||||
new RegExp(`Post: post-${item}`)
|
||||
)
|
||||
|
||||
expect(JSON.parse(await browser.elementByCss('#params').text())).toEqual({
|
||||
post: `post-${item}`,
|
||||
})
|
||||
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
|
||||
item: item + '',
|
||||
post: `post-${item}`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (dev) {
|
||||
it('should show error when rewriting to dynamic SSG page', async () => {
|
||||
const item = Math.round(Math.random() * 100)
|
||||
const html = await renderViaHTTP(appPort, `/some-rewrite/${item}`)
|
||||
expect(html).toContain(
|
||||
`Rewrites don't support dynamic pages with getStaticProps yet`
|
||||
)
|
||||
})
|
||||
|
||||
it('should not show warning from url prop being returned', async () => {
|
||||
const urlPropPage = join(appDir, 'pages/url-prop.js')
|
||||
await fs.writeFile(
|
||||
|
@ -1707,30 +1717,6 @@ const runTests = (dev = false, isEmulatedServerless = false) => {
|
|||
describe('SSG Prerender', () => {
|
||||
describe('dev mode', () => {
|
||||
beforeAll(async () => {
|
||||
origConfig = await fs.readFile(nextConfig, 'utf8')
|
||||
await fs.writeFile(
|
||||
nextConfig,
|
||||
`
|
||||
module.exports = {
|
||||
rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/some-rewrite/:item",
|
||||
destination: "/blog/post-:item"
|
||||
},
|
||||
{
|
||||
source: '/about',
|
||||
destination: '/lang/en/about'
|
||||
},
|
||||
{
|
||||
source: '/blocked-create',
|
||||
destination: '/blocking-fallback/blocked-create',
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
appPort = await findPort()
|
||||
app = await launchApp(appDir, appPort, {
|
||||
env: { __NEXT_TEST_WITH_DEVTOOL: 1 },
|
||||
|
@ -1741,7 +1727,6 @@ describe('SSG Prerender', () => {
|
|||
buildId = 'development'
|
||||
})
|
||||
afterAll(async () => {
|
||||
await fs.writeFile(nextConfig, origConfig)
|
||||
await killApp(app)
|
||||
})
|
||||
|
||||
|
@ -1750,13 +1735,10 @@ describe('SSG Prerender', () => {
|
|||
|
||||
describe('dev mode getStaticPaths', () => {
|
||||
beforeAll(async () => {
|
||||
origConfig = await fs.readFile(nextConfig, 'utf8')
|
||||
await fs.writeFile(
|
||||
nextConfig,
|
||||
nextConfig.write(
|
||||
// we set cpus to 1 so that we make sure the requests
|
||||
// aren't being cached at the jest-worker level
|
||||
`module.exports = { experimental: { cpus: 1 } }`,
|
||||
'utf8'
|
||||
`module.exports = { experimental: { cpus: 1 } }`
|
||||
)
|
||||
await fs.remove(join(appDir, '.next'))
|
||||
appPort = await findPort()
|
||||
|
@ -1765,7 +1747,7 @@ describe('SSG Prerender', () => {
|
|||
})
|
||||
})
|
||||
afterAll(async () => {
|
||||
await fs.writeFile(nextConfig, origConfig)
|
||||
nextConfig.restore()
|
||||
await killApp(app)
|
||||
})
|
||||
|
||||
|
@ -1808,7 +1790,6 @@ describe('SSG Prerender', () => {
|
|||
beforeAll(async () => {
|
||||
// remove firebase import since it breaks in legacy serverless mode
|
||||
origBlogPageContent = await fs.readFile(blogPagePath, 'utf8')
|
||||
origConfig = await fs.readFile(nextConfig, 'utf8')
|
||||
|
||||
await fs.writeFile(
|
||||
blogPagePath,
|
||||
|
@ -1818,25 +1799,7 @@ describe('SSG Prerender', () => {
|
|||
)
|
||||
)
|
||||
|
||||
await fs.writeFile(
|
||||
nextConfig,
|
||||
`module.exports = {
|
||||
target: 'serverless',
|
||||
rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/about',
|
||||
destination: '/lang/en/about'
|
||||
},
|
||||
{
|
||||
source: '/blocked-create',
|
||||
destination: '/blocking-fallback/blocked-create',
|
||||
}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
'utf8'
|
||||
)
|
||||
nextConfig.replace('// target', `target: 'serverless',`)
|
||||
await fs.remove(join(appDir, '.next'))
|
||||
await nextBuild(appDir)
|
||||
stderr = ''
|
||||
|
@ -1850,7 +1813,7 @@ describe('SSG Prerender', () => {
|
|||
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
|
||||
})
|
||||
afterAll(async () => {
|
||||
await fs.writeFile(nextConfig, origConfig)
|
||||
nextConfig.restore()
|
||||
await fs.writeFile(blogPagePath, origBlogPageContent)
|
||||
await killApp(app)
|
||||
})
|
||||
|
@ -1931,11 +1894,9 @@ describe('SSG Prerender', () => {
|
|||
})
|
||||
}
|
||||
|
||||
origConfig = await fs.readFile(nextConfig, 'utf8')
|
||||
await fs.writeFile(
|
||||
nextConfig,
|
||||
`module.exports = { target: 'experimental-serverless-trace' }`,
|
||||
'utf8'
|
||||
await nextConfig.replace(
|
||||
'// target',
|
||||
`target: 'experimental-serverless-trace',`
|
||||
)
|
||||
await fs.writeFile(
|
||||
cstmError,
|
||||
|
@ -1963,8 +1924,8 @@ describe('SSG Prerender', () => {
|
|||
app = await startServerlessEmulator(appDir, appPort, buildId)
|
||||
})
|
||||
afterAll(async () => {
|
||||
nextConfig.restore()
|
||||
await fs.remove(cstmError)
|
||||
await fs.writeFile(nextConfig, origConfig)
|
||||
await killApp(app)
|
||||
})
|
||||
|
||||
|
@ -2024,9 +1985,7 @@ describe('SSG Prerender', () => {
|
|||
|
||||
beforeAll(async () => {
|
||||
exportDir = join(appDir, 'out')
|
||||
origConfig = await fs.readFile(nextConfig, 'utf8')
|
||||
await fs.writeFile(
|
||||
nextConfig,
|
||||
nextConfig.write(
|
||||
`module.exports = {
|
||||
exportTrailingSlash: true,
|
||||
exportPathMap: function(defaultPathMap) {
|
||||
|
@ -2075,7 +2034,7 @@ describe('SSG Prerender', () => {
|
|||
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
|
||||
})
|
||||
afterAll(async () => {
|
||||
await fs.writeFile(nextConfig, origConfig)
|
||||
nextConfig.restore()
|
||||
await stopApp(app)
|
||||
|
||||
for (const page of fallbackTruePages) {
|
||||
|
|
|
@ -80,7 +80,7 @@ describe('Production response size', () => {
|
|||
)
|
||||
|
||||
// These numbers are without gzip compression!
|
||||
const delta = responseSizesBytes - 275 * 1024
|
||||
const delta = responseSizesBytes - 277 * 1024
|
||||
expect(delta).toBeLessThanOrEqual(1024) // don't increase size more than 1kb
|
||||
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue