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 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,
|
version: 3,
|
||||||
pages404: true,
|
pages404: true,
|
||||||
basePath: config.basePath,
|
basePath: config.basePath,
|
||||||
|
@ -304,6 +323,7 @@ export default async function build(
|
||||||
namedRegex: routeRegex.namedRegex,
|
namedRegex: routeRegex.namedRegex,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
dataRoutes: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
await promises.mkdir(distDir, { recursive: true })
|
await promises.mkdir(distDir, { recursive: true })
|
||||||
|
@ -325,6 +345,7 @@ export default async function build(
|
||||||
target,
|
target,
|
||||||
pagesDir,
|
pagesDir,
|
||||||
entrypoints: entrypoints.client,
|
entrypoints: entrypoints.client,
|
||||||
|
rewrites,
|
||||||
}),
|
}),
|
||||||
getBaseWebpackConfig(dir, {
|
getBaseWebpackConfig(dir, {
|
||||||
tracer,
|
tracer,
|
||||||
|
@ -335,6 +356,7 @@ export default async function build(
|
||||||
target,
|
target,
|
||||||
pagesDir,
|
pagesDir,
|
||||||
entrypoints: entrypoints.server,
|
entrypoints: entrypoints.server,
|
||||||
|
rewrites,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -654,6 +676,7 @@ export default async function build(
|
||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since custom _app.js can wrap the 404 page we have to opt-out of static optimization if it has getInitialProps
|
// 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
|
// Only export the static 404 when there is no /_error present
|
||||||
const useStatic404 =
|
const useStatic404 =
|
||||||
|
|
|
@ -54,6 +54,7 @@ import WebpackConformancePlugin, {
|
||||||
ReactSyncScriptsConformanceCheck,
|
ReactSyncScriptsConformanceCheck,
|
||||||
} from './webpack/plugins/webpack-conformance-plugin'
|
} from './webpack/plugins/webpack-conformance-plugin'
|
||||||
import { WellKnownErrorsPlugin } from './webpack/plugins/wellknown-errors-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
|
type ExcludesFalse = <T>(x: T | false) => x is T
|
||||||
|
|
||||||
const isWebpack5 = parseInt(webpack.version!) === 5
|
const isWebpack5 = parseInt(webpack.version!) === 5
|
||||||
|
@ -190,6 +191,7 @@ export default async function getBaseWebpackConfig(
|
||||||
target = 'server',
|
target = 'server',
|
||||||
reactProductionProfiling = false,
|
reactProductionProfiling = false,
|
||||||
entrypoints,
|
entrypoints,
|
||||||
|
rewrites,
|
||||||
}: {
|
}: {
|
||||||
buildId: string
|
buildId: string
|
||||||
config: any
|
config: any
|
||||||
|
@ -200,6 +202,7 @@ export default async function getBaseWebpackConfig(
|
||||||
tracer?: any
|
tracer?: any
|
||||||
reactProductionProfiling?: boolean
|
reactProductionProfiling?: boolean
|
||||||
entrypoints: WebpackEntrypoints
|
entrypoints: WebpackEntrypoints
|
||||||
|
rewrites: Rewrite[]
|
||||||
}
|
}
|
||||||
): Promise<webpack.Configuration> {
|
): Promise<webpack.Configuration> {
|
||||||
const productionBrowserSourceMaps =
|
const productionBrowserSourceMaps =
|
||||||
|
@ -207,6 +210,8 @@ export default async function getBaseWebpackConfig(
|
||||||
let plugins: PluginMetaData[] = []
|
let plugins: PluginMetaData[] = []
|
||||||
let babelPresetPlugins: { dir: string; config: any }[] = []
|
let babelPresetPlugins: { dir: string; config: any }[] = []
|
||||||
|
|
||||||
|
const hasRewrites = rewrites.length > 0 || dev
|
||||||
|
|
||||||
if (config.experimental.plugins) {
|
if (config.experimental.plugins) {
|
||||||
plugins = await collectPlugins(dir, config.env, config.plugins)
|
plugins = await collectPlugins(dir, config.env, config.plugins)
|
||||||
pluginLoaderOptions.plugins = 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 = {
|
const resolveConfig = {
|
||||||
// Disable .mjs for node_modules bundling
|
// Disable .mjs for node_modules bundling
|
||||||
extensions: isServer
|
extensions: isServer
|
||||||
|
@ -370,6 +379,9 @@ export default async function getBaseWebpackConfig(
|
||||||
[DOT_NEXT_ALIAS]: distDir,
|
[DOT_NEXT_ALIAS]: distDir,
|
||||||
...getOptimizedAliases(isServer),
|
...getOptimizedAliases(isServer),
|
||||||
...getReactProfilingInProduction(),
|
...getReactProfilingInProduction(),
|
||||||
|
[clientResolveRewrites]: hasRewrites
|
||||||
|
? clientResolveRewrites
|
||||||
|
: require.resolve('next/dist/client/dev/noop.js'),
|
||||||
},
|
},
|
||||||
mainFields: isServer ? ['main', 'module'] : ['browser', 'module', 'main'],
|
mainFields: isServer ? ['main', 'module'] : ['browser', 'module', 'main'],
|
||||||
plugins: isWebpack5
|
plugins: isWebpack5
|
||||||
|
@ -938,6 +950,7 @@ export default async function getBaseWebpackConfig(
|
||||||
config.experimental.scrollRestoration
|
config.experimental.scrollRestoration
|
||||||
),
|
),
|
||||||
'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath),
|
'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath),
|
||||||
|
'process.env.__NEXT_HAS_REWRITES': JSON.stringify(hasRewrites),
|
||||||
...(isServer
|
...(isServer
|
||||||
? {
|
? {
|
||||||
// Fix bad-actors in the npm ecosystem (e.g. `node-formidable`)
|
// Fix bad-actors in the npm ecosystem (e.g. `node-formidable`)
|
||||||
|
@ -1010,6 +1023,7 @@ export default async function getBaseWebpackConfig(
|
||||||
!isServer &&
|
!isServer &&
|
||||||
new BuildManifestPlugin({
|
new BuildManifestPlugin({
|
||||||
buildId,
|
buildId,
|
||||||
|
rewrites,
|
||||||
modern: config.experimental.modern,
|
modern: config.experimental.modern,
|
||||||
}),
|
}),
|
||||||
tracer &&
|
tracer &&
|
||||||
|
|
|
@ -147,7 +147,7 @@ const nextServerlessLoader: loader.Loader = function () {
|
||||||
|
|
||||||
const handleRewrites = `
|
const handleRewrites = `
|
||||||
const getCustomRouteMatcher = pathMatch(true)
|
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) {
|
function handleRewrites(parsedUrl) {
|
||||||
for (const rewrite of rewrites) {
|
for (const rewrite of rewrites) {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {
|
||||||
import { BuildManifest } from '../../../next-server/server/get-page-files'
|
import { BuildManifest } from '../../../next-server/server/get-page-files'
|
||||||
import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-entrypoint'
|
import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-entrypoint'
|
||||||
import { ampFirstEntryNamesMap } from './next-drop-client-page-plugin'
|
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
|
const isWebpack5 = parseInt(webpack.version!) === 5
|
||||||
|
|
||||||
|
@ -21,12 +23,19 @@ export type ClientBuildManifest = Record<string, string[]>
|
||||||
// reduced version to send to the client.
|
// reduced version to send to the client.
|
||||||
function generateClientManifest(
|
function generateClientManifest(
|
||||||
assetMap: BuildManifest,
|
assetMap: BuildManifest,
|
||||||
isModern: boolean
|
isModern: boolean,
|
||||||
|
rewrites: Rewrite[]
|
||||||
): string {
|
): 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 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
|
if (page === '/_app') return
|
||||||
// Filter out dependencies in the _app entry, because those will have already
|
// Filter out dependencies in the _app entry, because those will have already
|
||||||
// been loaded by the client prior to a navigation event
|
// been loaded by the client prior to a navigation event
|
||||||
|
@ -41,6 +50,10 @@ function generateClientManifest(
|
||||||
clientManifest[page] = filteredDeps
|
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)
|
return devalue(clientManifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,10 +78,25 @@ function getFilesArray(files: any) {
|
||||||
export default class BuildManifestPlugin {
|
export default class BuildManifestPlugin {
|
||||||
private buildId: string
|
private buildId: string
|
||||||
private modern: boolean
|
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.buildId = options.buildId
|
||||||
this.modern = options.modern
|
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) {
|
createAssets(compilation: any, assets: any) {
|
||||||
|
@ -185,7 +213,8 @@ export default class BuildManifestPlugin {
|
||||||
assets[clientManifestPath] = new RawSource(
|
assets[clientManifestPath] = new RawSource(
|
||||||
`self.__BUILD_MANIFEST = ${generateClientManifest(
|
`self.__BUILD_MANIFEST = ${generateClientManifest(
|
||||||
assetMap,
|
assetMap,
|
||||||
false
|
false,
|
||||||
|
this.rewrites
|
||||||
)};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
|
)};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -195,7 +224,8 @@ export default class BuildManifestPlugin {
|
||||||
assets[modernClientManifestPath] = new RawSource(
|
assets[modernClientManifestPath] = new RawSource(
|
||||||
`self.__BUILD_MANIFEST = ${generateClientManifest(
|
`self.__BUILD_MANIFEST = ${generateClientManifest(
|
||||||
assetMap,
|
assetMap,
|
||||||
true
|
true,
|
||||||
|
this.rewrites
|
||||||
)};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
|
)};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,9 +33,11 @@ function EventSourceWrapper(options) {
|
||||||
for (var i = 0; i < listeners.length; i++) {
|
for (var i = 0; i < listeners.length; i++) {
|
||||||
listeners[i](event)
|
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() {
|
function handleDisconnect() {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import initWebpackHMR from './dev/webpack-hot-middleware-client'
|
||||||
import initializeBuildWatcher from './dev/dev-build-watcher'
|
import initializeBuildWatcher from './dev/dev-build-watcher'
|
||||||
import initializePrerenderIndicator from './dev/prerender-indicator'
|
import initializePrerenderIndicator from './dev/prerender-indicator'
|
||||||
import { displayContent } from './dev/fouc'
|
import { displayContent } from './dev/fouc'
|
||||||
|
import { getEventSourceWrapper } from './dev/error-overlay/eventsource'
|
||||||
|
|
||||||
// Temporary workaround for the issue described here:
|
// Temporary workaround for the issue described here:
|
||||||
// https://github.com/vercel/next.js/issues/3775#issuecomment-407438123
|
// https://github.com/vercel/next.js/issues/3775#issuecomment-407438123
|
||||||
|
@ -30,6 +31,22 @@ window.next = next
|
||||||
initNext({ webpackHMR })
|
initNext({ webpackHMR })
|
||||||
.then(({ renderCtx, render }) => {
|
.then(({ renderCtx, render }) => {
|
||||||
initOnDemandEntries({ assetPrefix: prefix })
|
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_BUILD_INDICATOR) initializeBuildWatcher()
|
||||||
if (
|
if (
|
||||||
process.env.__NEXT_PRERENDER_INDICATOR &&
|
process.env.__NEXT_PRERENDER_INDICATOR &&
|
||||||
|
|
|
@ -80,6 +80,7 @@ export default class PageLoader {
|
||||||
private loadingRoutes: Record<string, boolean>
|
private loadingRoutes: Record<string, boolean>
|
||||||
private promisedBuildManifest?: Promise<ClientBuildManifest>
|
private promisedBuildManifest?: Promise<ClientBuildManifest>
|
||||||
private promisedSsgManifest?: Promise<ClientSsgManifest>
|
private promisedSsgManifest?: Promise<ClientSsgManifest>
|
||||||
|
private promisedDevPagesManifest?: Promise<any>
|
||||||
|
|
||||||
constructor(buildId: string, assetPrefix: string, initialPage: string) {
|
constructor(buildId: string, assetPrefix: string, initialPage: string) {
|
||||||
this.buildId = buildId
|
this.buildId = buildId
|
||||||
|
@ -97,17 +98,16 @@ export default class PageLoader {
|
||||||
this.loadingRoutes[initialPage] = true
|
this.loadingRoutes[initialPage] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
this.promisedBuildManifest = new Promise((resolve) => {
|
||||||
this.promisedBuildManifest = new Promise((resolve) => {
|
if ((window as any).__BUILD_MANIFEST) {
|
||||||
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)
|
resolve((window as any).__BUILD_MANIFEST)
|
||||||
} else {
|
|
||||||
;(window as any).__BUILD_MANIFEST_CB = () => {
|
|
||||||
resolve((window as any).__BUILD_MANIFEST)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
/** @type {Promise<Set<string>>} */
|
/** @type {Promise<Set<string>>} */
|
||||||
this.promisedSsgManifest = new Promise((resolve) => {
|
this.promisedSsgManifest = new Promise((resolve) => {
|
||||||
if ((window as any).__SSG_MANIFEST) {
|
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
|
// Returns a promise for the dependencies for a particular route
|
||||||
getDependencies(route: string): Promise<string[]> {
|
getDependencies(route: string): Promise<string[]> {
|
||||||
return this.promisedBuildManifest!.then((m) => {
|
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 EXPORT_DETAIL = 'export-detail.json'
|
||||||
export const PRERENDER_MANIFEST = 'prerender-manifest.json'
|
export const PRERENDER_MANIFEST = 'prerender-manifest.json'
|
||||||
export const ROUTES_MANIFEST = 'routes-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 REACT_LOADABLE_MANIFEST = 'react-loadable-manifest.json'
|
||||||
export const FONT_MANIFEST = 'font-manifest.json'
|
export const FONT_MANIFEST = 'font-manifest.json'
|
||||||
export const SERVER_DIRECTORY = 'server'
|
export const SERVER_DIRECTORY = 'server'
|
||||||
|
|
|
@ -23,6 +23,8 @@ import { parseRelativeUrl } from './utils/parse-relative-url'
|
||||||
import { searchParamsToUrlQuery } from './utils/querystring'
|
import { searchParamsToUrlQuery } from './utils/querystring'
|
||||||
import { getRouteMatcher } from './utils/route-matcher'
|
import { getRouteMatcher } from './utils/route-matcher'
|
||||||
import { getRouteRegex } from './utils/route-regex'
|
import { getRouteRegex } from './utils/route-regex'
|
||||||
|
import { denormalizePagePath } from '../../server/denormalize-page-path'
|
||||||
|
import resolveRewrites from './utils/resolve-rewrites'
|
||||||
|
|
||||||
interface TransitionOptions {
|
interface TransitionOptions {
|
||||||
shallow?: boolean
|
shallow?: boolean
|
||||||
|
@ -465,6 +467,7 @@ export default class Router implements BaseRouter {
|
||||||
if (!(options as any)._h && this.onlyAHashChange(cleanedAs)) {
|
if (!(options as any)._h && this.onlyAHashChange(cleanedAs)) {
|
||||||
this.asPath = cleanedAs
|
this.asPath = cleanedAs
|
||||||
Router.events.emit('hashChangeStart', as)
|
Router.events.emit('hashChangeStart', as)
|
||||||
|
// TODO: do we need the resolved href when only a hash change?
|
||||||
this.changeState(method, url, as, options)
|
this.changeState(method, url, as, options)
|
||||||
this.scrollToHash(cleanedAs)
|
this.scrollToHash(cleanedAs)
|
||||||
this.notify(this.components[this.route])
|
this.notify(this.components[this.route])
|
||||||
|
@ -472,11 +475,25 @@ export default class Router implements BaseRouter {
|
||||||
return true
|
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
|
if (!parsed) return false
|
||||||
|
|
||||||
let { pathname, searchParams } = parsed
|
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)
|
const query = searchParamsToUrlQuery(searchParams)
|
||||||
|
|
||||||
// url and as should always be prefixed with basePath by this
|
// 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 route = removePathTrailingSlash(pathname)
|
||||||
const { shallow = false } = options
|
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)) {
|
if (isDynamicRoute(route)) {
|
||||||
const { pathname: asPathname } = parseRelativeUrl(cleanedAs)
|
const { pathname: asPathname } = parseRelativeUrl(resolvedAs)
|
||||||
const routeRegex = getRouteRegex(route)
|
const routeRegex = getRouteRegex(route)
|
||||||
const routeMatch = getRouteMatcher(routeRegex)(asPathname)
|
const routeMatch = getRouteMatcher(routeRegex)(asPathname)
|
||||||
if (!routeMatch) {
|
if (!routeMatch) {
|
||||||
|
@ -795,6 +821,29 @@ export default class Router implements BaseRouter {
|
||||||
return this.asPath !== asPath
|
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.
|
* Prefetch page code, you may wait for the data during page rendering.
|
||||||
* This feature only works in production!
|
* This feature only works in production!
|
||||||
|
@ -806,11 +855,20 @@ export default class Router implements BaseRouter {
|
||||||
asPath: string = url,
|
asPath: string = url,
|
||||||
options: PrefetchOptions = {}
|
options: PrefetchOptions = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const parsed = tryParseRelativeUrl(url)
|
let parsed = tryParseRelativeUrl(url)
|
||||||
|
|
||||||
if (!parsed) return
|
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
|
// Prefetch is not supported in development mode because it would trigger on-demand-entries
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
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 { pathToRegexp }
|
||||||
|
|
||||||
|
export const matcherOptions = {
|
||||||
|
sensitive: false,
|
||||||
|
delimiter: '/',
|
||||||
|
decode: decodeParam,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customRouteMatcherOptions = {
|
||||||
|
...matcherOptions,
|
||||||
|
strict: true,
|
||||||
|
}
|
||||||
|
|
||||||
export default (customRoute = false) => {
|
export default (customRoute = false) => {
|
||||||
return (path: string) => {
|
return (path: string) => {
|
||||||
const keys: pathToRegexp.Key[] = []
|
const keys: pathToRegexp.Key[] = []
|
||||||
const matcherOptions = {
|
const matcherRegex = pathToRegexp.pathToRegexp(
|
||||||
sensitive: false,
|
path,
|
||||||
delimiter: '/',
|
keys,
|
||||||
...(customRoute ? { strict: true } : undefined),
|
customRoute ? customRouteMatcherOptions : matcherOptions
|
||||||
decode: decodeParam,
|
)
|
||||||
}
|
|
||||||
const matcherRegex = pathToRegexp.pathToRegexp(path, keys, matcherOptions)
|
|
||||||
const matcher = pathToRegexp.regexpToFunction(
|
const matcher = pathToRegexp.regexpToFunction(
|
||||||
matcherRegex,
|
matcherRegex,
|
||||||
keys,
|
keys,
|
||||||
|
|
|
@ -48,10 +48,10 @@ import Router, {
|
||||||
DynamicRoutes,
|
DynamicRoutes,
|
||||||
PageChecker,
|
PageChecker,
|
||||||
Params,
|
Params,
|
||||||
prepareDestination,
|
|
||||||
route,
|
route,
|
||||||
Route,
|
Route,
|
||||||
} from './router'
|
} from './router'
|
||||||
|
import prepareDestination from '../lib/router/utils/prepare-destination'
|
||||||
import { sendPayload } from './send-payload'
|
import { sendPayload } from './send-payload'
|
||||||
import { serveStatic } from './serve-static'
|
import { serveStatic } from './serve-static'
|
||||||
import { IncrementalCache } from './incremental-cache'
|
import { IncrementalCache } from './incremental-cache'
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { posix } from 'path'
|
import { posix } from 'path'
|
||||||
|
|
||||||
export function normalizePathSep(path: string): string {
|
export { normalizePathSep, denormalizePagePath } from './denormalize-page-path'
|
||||||
return path.replace(/\\/g, '/')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizePagePath(page: string): string {
|
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
|
// 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
|
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 headTags = (...args: any) => callMiddleware('headTags', args)
|
||||||
|
|
||||||
const didRewrite = (req as any)._nextDidRewrite
|
|
||||||
const isFallback = !!query.__nextFallback
|
const isFallback = !!query.__nextFallback
|
||||||
delete 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) {
|
if (hasPageGetInitialProps && isSSG) {
|
||||||
throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT + ` ${pathname}`)
|
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
|
// the response might be finished on the getInitialProps call
|
||||||
if (isResSent(res) && !isSSG) return null
|
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
|
// AMP First pages do not have client-side JavaScript files
|
||||||
const files = ampState.ampFirst
|
const files = ampState.ampFirst
|
||||||
? []
|
? []
|
||||||
|
@ -700,6 +703,7 @@ export async function renderToHTML(
|
||||||
? getPageFiles(buildManifest, pathname)
|
? getPageFiles(buildManifest, pathname)
|
||||||
: []),
|
: []),
|
||||||
]),
|
]),
|
||||||
|
...additionalFiles,
|
||||||
]
|
]
|
||||||
|
|
||||||
const renderPage: RenderPage = (
|
const renderPage: RenderPage = (
|
||||||
|
@ -766,6 +770,7 @@ export async function renderToHTML(
|
||||||
|
|
||||||
let html = renderDocument(Document, {
|
let html = renderDocument(Document, {
|
||||||
...renderOpts,
|
...renderOpts,
|
||||||
|
buildManifest: filteredBuildManifest,
|
||||||
// Only enabled in production as development mode has features relying on HMR (style injection for example)
|
// Only enabled in production as development mode has features relying on HMR (style injection for example)
|
||||||
unstable_runtimeJS:
|
unstable_runtimeJS:
|
||||||
process.env.NODE_ENV === 'production'
|
process.env.NODE_ENV === 'production'
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { IncomingMessage, ServerResponse } from 'http'
|
import { IncomingMessage, ServerResponse } from 'http'
|
||||||
import { parse as parseUrl, UrlWithParsedQuery } from 'url'
|
import { UrlWithParsedQuery } from 'url'
|
||||||
import { ParsedUrlQuery } from 'querystring'
|
|
||||||
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
|
|
||||||
import pathMatch from './lib/path-match'
|
import pathMatch from './lib/path-match'
|
||||||
|
|
||||||
export const route = pathMatch()
|
export const route = pathMatch()
|
||||||
|
@ -37,86 +36,6 @@ export type PageChecker = (pathname: string) => Promise<boolean>
|
||||||
|
|
||||||
const customRouteTypes = new Set(['rewrite', 'redirect', 'header'])
|
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) {
|
function replaceBasePath(basePath: string, pathname: string) {
|
||||||
return pathname!.replace(basePath, '') || '/'
|
return pathname!.replace(basePath, '') || '/'
|
||||||
}
|
}
|
||||||
|
|
|
@ -549,6 +549,7 @@ export class NextScript extends Component<OriginProps> {
|
||||||
? { type: 'module' }
|
? { type: 'module' }
|
||||||
: { noModule: true }
|
: { noModule: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<script
|
<script
|
||||||
key={file}
|
key={file}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import getRouteFromEntrypoint from '../next-server/server/get-route-from-entrypo
|
||||||
import { isWriteable } from '../build/is-writeable'
|
import { isWriteable } from '../build/is-writeable'
|
||||||
import { ClientPagesLoaderOptions } from '../build/webpack/loaders/next-client-pages-loader'
|
import { ClientPagesLoaderOptions } from '../build/webpack/loaders/next-client-pages-loader'
|
||||||
import { stringify } from 'querystring'
|
import { stringify } from 'querystring'
|
||||||
|
import { Rewrite } from '../lib/load-custom-routes'
|
||||||
|
|
||||||
export async function renderScriptError(
|
export async function renderScriptError(
|
||||||
res: ServerResponse,
|
res: ServerResponse,
|
||||||
|
@ -152,6 +153,7 @@ export default class HotReloader {
|
||||||
private onDemandEntries: any
|
private onDemandEntries: any
|
||||||
private previewProps: __ApiPreviewProps
|
private previewProps: __ApiPreviewProps
|
||||||
private watcher: any
|
private watcher: any
|
||||||
|
private rewrites: Rewrite[]
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
dir: string,
|
dir: string,
|
||||||
|
@ -160,11 +162,13 @@ export default class HotReloader {
|
||||||
pagesDir,
|
pagesDir,
|
||||||
buildId,
|
buildId,
|
||||||
previewProps,
|
previewProps,
|
||||||
|
rewrites,
|
||||||
}: {
|
}: {
|
||||||
config: object
|
config: object
|
||||||
pagesDir: string
|
pagesDir: string
|
||||||
buildId: string
|
buildId: string
|
||||||
previewProps: __ApiPreviewProps
|
previewProps: __ApiPreviewProps
|
||||||
|
rewrites: Rewrite[]
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
this.buildId = buildId
|
this.buildId = buildId
|
||||||
|
@ -178,6 +182,7 @@ export default class HotReloader {
|
||||||
|
|
||||||
this.config = config
|
this.config = config
|
||||||
this.previewProps = previewProps
|
this.previewProps = previewProps
|
||||||
|
this.rewrites = rewrites
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run(
|
public async run(
|
||||||
|
@ -285,6 +290,7 @@ export default class HotReloader {
|
||||||
config: this.config,
|
config: this.config,
|
||||||
buildId: this.buildId,
|
buildId: this.buildId,
|
||||||
pagesDir: this.pagesDir,
|
pagesDir: this.pagesDir,
|
||||||
|
rewrites: this.rewrites,
|
||||||
entrypoints: { ...entrypoints.client, ...additionalClientEntrypoints },
|
entrypoints: { ...entrypoints.client, ...additionalClientEntrypoints },
|
||||||
}),
|
}),
|
||||||
getBaseWebpackConfig(this.dir, {
|
getBaseWebpackConfig(this.dir, {
|
||||||
|
@ -293,6 +299,7 @@ export default class HotReloader {
|
||||||
config: this.config,
|
config: this.config,
|
||||||
buildId: this.buildId,
|
buildId: this.buildId,
|
||||||
pagesDir: this.pagesDir,
|
pagesDir: this.pagesDir,
|
||||||
|
rewrites: this.rewrites,
|
||||||
entrypoints: entrypoints.server,
|
entrypoints: entrypoints.server,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
@ -509,7 +516,7 @@ export default class HotReloader {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
private send(action: string, ...args: any[]): void {
|
public send(action?: string, ...args: any[]): void {
|
||||||
this.webpackHotMiddleware!.publish({ action, data: args })
|
this.webpackHotMiddleware!.publish({ action, data: args })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,11 @@ import { fileExists } from '../lib/file-exists'
|
||||||
import { findPagesDir } from '../lib/find-pages-dir'
|
import { findPagesDir } from '../lib/find-pages-dir'
|
||||||
import loadCustomRoutes, { CustomRoutes } from '../lib/load-custom-routes'
|
import loadCustomRoutes, { CustomRoutes } from '../lib/load-custom-routes'
|
||||||
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
|
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 {
|
import {
|
||||||
getRouteMatcher,
|
getRouteMatcher,
|
||||||
getRouteRegex,
|
getRouteRegex,
|
||||||
|
@ -46,6 +50,8 @@ export default class DevServer extends Server {
|
||||||
private webpackWatcher?: Watchpack | null
|
private webpackWatcher?: Watchpack | null
|
||||||
private hotReloader?: HotReloader
|
private hotReloader?: HotReloader
|
||||||
private isCustomServer: boolean
|
private isCustomServer: boolean
|
||||||
|
protected sortedRoutes?: string[]
|
||||||
|
|
||||||
protected staticPathsWorker: import('jest-worker').default & {
|
protected staticPathsWorker: import('jest-worker').default & {
|
||||||
loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths
|
loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths
|
||||||
}
|
}
|
||||||
|
@ -206,8 +212,22 @@ export default class DevServer extends Server {
|
||||||
|
|
||||||
routedPages.push(pageName)
|
routedPages.push(pageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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)
|
.filter(isDynamicRoute)
|
||||||
.map((page) => ({
|
.map((page) => ({
|
||||||
page,
|
page,
|
||||||
|
@ -257,6 +277,7 @@ export default class DevServer extends Server {
|
||||||
config: this.nextConfig,
|
config: this.nextConfig,
|
||||||
previewProps: this.getPreviewProps(),
|
previewProps: this.getPreviewProps(),
|
||||||
buildId: this.buildId,
|
buildId: this.buildId,
|
||||||
|
rewrites: this.customRoutes.rewrites,
|
||||||
})
|
})
|
||||||
await super.prepare()
|
await super.prepare()
|
||||||
await this.addExportPathMapRoutes()
|
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({
|
fsRoutes.push({
|
||||||
match: route('/:path*'),
|
match: route('/:path*'),
|
||||||
type: 'route',
|
type: 'route',
|
||||||
|
|
|
@ -98,13 +98,13 @@ describe('Build Output', () => {
|
||||||
expect(parseFloat(indexFirstLoad) - 60).toBeLessThanOrEqual(0)
|
expect(parseFloat(indexFirstLoad) - 60).toBeLessThanOrEqual(0)
|
||||||
expect(indexFirstLoad.endsWith('kB')).toBe(true)
|
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(err404Size.endsWith('kB')).toBe(true)
|
||||||
|
|
||||||
expect(parseFloat(err404FirstLoad) - 63).toBeLessThanOrEqual(0)
|
expect(parseFloat(err404FirstLoad) - 63).toBeLessThanOrEqual(0)
|
||||||
expect(err404FirstLoad.endsWith('kB')).toBe(true)
|
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)
|
expect(sharedByAll.endsWith('kB')).toBe(true)
|
||||||
|
|
||||||
if (_appSize.endsWith('kB')) {
|
if (_appSize.endsWith('kB')) {
|
||||||
|
|
|
@ -10,6 +10,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
{
|
||||||
|
source: '/rewriting-to-auto-export',
|
||||||
|
destination: '/auto-export/hello',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: '/to-another',
|
source: '/to-another',
|
||||||
destination: '/another/one',
|
destination: '/another/one',
|
||||||
|
|
|
@ -36,6 +36,12 @@ let appPort
|
||||||
let app
|
let app
|
||||||
|
|
||||||
const runTests = (isDev = false) => {
|
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 () => {
|
it('should handle one-to-one rewrite successfully', async () => {
|
||||||
const html = await renderViaHTTP(appPort, '/first')
|
const html = await renderViaHTTP(appPort, '/first')
|
||||||
expect(html).toMatch(/hello/)
|
expect(html).toMatch(/hello/)
|
||||||
|
@ -512,6 +518,7 @@ const runTests = (isDev = false) => {
|
||||||
version: 3,
|
version: 3,
|
||||||
pages404: true,
|
pages404: true,
|
||||||
basePath: '',
|
basePath: '',
|
||||||
|
dataRoutes: [],
|
||||||
redirects: [
|
redirects: [
|
||||||
{
|
{
|
||||||
destination: '/:path+',
|
destination: '/:path+',
|
||||||
|
@ -804,6 +811,11 @@ const runTests = (isDev = false) => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rewrites: [
|
rewrites: [
|
||||||
|
{
|
||||||
|
destination: '/auto-export/hello',
|
||||||
|
regex: normalizeRegEx('^\\/rewriting-to-auto-export$'),
|
||||||
|
source: '/rewriting-to-auto-export',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
destination: '/another/one',
|
destination: '/another/one',
|
||||||
regex: normalizeRegEx('^\\/to-another$'),
|
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)
|
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', () => {
|
describe('no-op rewrite', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
appPort = await findPort()
|
appPort = await findPort()
|
||||||
|
@ -1044,26 +1059,13 @@ describe('Custom routes', () => {
|
||||||
})
|
})
|
||||||
afterAll(() => killApp(app))
|
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 browser = await webdriver(appPort, '/auto-export/my-slug')
|
||||||
const html = await browser.eval(() => document.documentElement.innerHTML)
|
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`)
|
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', () => {
|
describe('server mode', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const { stdout: buildStdout } = await nextBuild(appDir, [], {
|
const { stdout: buildStdout } = await nextBuild(appDir, [], {
|
||||||
|
|
|
@ -9,6 +9,10 @@ const Page = () => {
|
||||||
<a id="view-post-1">View post 1</a>
|
<a id="view-post-1">View post 1</a>
|
||||||
</Link>
|
</Link>
|
||||||
<br />
|
<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">
|
<Link href="/[name]/comments" as="/post-1/comments">
|
||||||
<a id="view-post-1-comments">View post 1 comments</a>
|
<a id="view-post-1-comments">View post 1 comments</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -17,6 +21,14 @@ const Page = () => {
|
||||||
<a id="view-post-1-comment-1">View comment 1 on post 1</a>
|
<a id="view-post-1-comment-1">View comment 1 on post 1</a>
|
||||||
</Link>
|
</Link>
|
||||||
<br />
|
<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">
|
<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>
|
<a id="view-nested-dynamic-cmnt">View comment 123 on blog post 321</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -49,6 +61,9 @@ const Page = () => {
|
||||||
<Link href="/p1/p2/all-ssg/[...rest]" as="/p1/p2/all-ssg/hello1/hello2">
|
<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>
|
<a id="ssg-catch-all-multi">Catch-all route (multi)</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/p1/p2/all-ssg/hello1/hello2">
|
||||||
|
<a id="ssg-catch-all-multi-no-as">Catch-all route (multi)</a>
|
||||||
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/p1/p2/nested-all-ssg/[...rest]"
|
href="/p1/p2/nested-all-ssg/[...rest]"
|
||||||
as="/p1/p2/nested-all-ssg/hello"
|
as="/p1/p2/nested-all-ssg/hello"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-env jest */
|
/* eslint-env jest */
|
||||||
|
|
||||||
import webdriver from 'next-webdriver'
|
import webdriver from 'next-webdriver'
|
||||||
import { join } from 'path'
|
import { join, dirname } from 'path'
|
||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import {
|
import {
|
||||||
renderViaHTTP,
|
renderViaHTTP,
|
||||||
|
@ -13,6 +13,7 @@ import {
|
||||||
nextBuild,
|
nextBuild,
|
||||||
nextStart,
|
nextStart,
|
||||||
normalizeRegEx,
|
normalizeRegEx,
|
||||||
|
check,
|
||||||
} from 'next-test-utils'
|
} from 'next-test-utils'
|
||||||
import cheerio from 'cheerio'
|
import cheerio from 'cheerio'
|
||||||
import escapeRegex from 'escape-string-regexp'
|
import escapeRegex from 'escape-string-regexp'
|
||||||
|
@ -86,9 +87,29 @@ function runTests(dev) {
|
||||||
let browser
|
let browser
|
||||||
try {
|
try {
|
||||||
browser = await webdriver(appPort, '/')
|
browser = await webdriver(appPort, '/')
|
||||||
|
await browser.eval('window.beforeNav = 1')
|
||||||
await browser.elementByCss('#view-post-1').click()
|
await browser.elementByCss('#view-post-1').click()
|
||||||
await browser.waitForElementByCss('#asdf')
|
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()
|
const text = await browser.elementByCss('#asdf').text()
|
||||||
expect(text).toMatch(/this is.*?post-1/i)
|
expect(text).toMatch(/this is.*?post-1/i)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -137,9 +158,29 @@ function runTests(dev) {
|
||||||
let browser
|
let browser
|
||||||
try {
|
try {
|
||||||
browser = await webdriver(appPort, '/')
|
browser = await webdriver(appPort, '/')
|
||||||
|
await browser.eval('window.beforeNav = 1')
|
||||||
await browser.elementByCss('#view-post-1-comment-1').click()
|
await browser.elementByCss('#view-post-1-comment-1').click()
|
||||||
await browser.waitForElementByCss('#asdf')
|
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()
|
const text = await browser.elementByCss('#asdf').text()
|
||||||
expect(text).toMatch(/i am.*comment-1.*on.*post-1/i)
|
expect(text).toMatch(/i am.*comment-1.*on.*post-1/i)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -354,9 +395,29 @@ function runTests(dev) {
|
||||||
let browser
|
let browser
|
||||||
try {
|
try {
|
||||||
browser = await webdriver(appPort, '/')
|
browser = await webdriver(appPort, '/')
|
||||||
|
await browser.eval('window.beforeNav = 1')
|
||||||
await browser.elementByCss('#ssg-catch-all-multi').click()
|
await browser.elementByCss('#ssg-catch-all-multi').click()
|
||||||
await browser.waitForElementByCss('#all-ssg-content')
|
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()
|
const text = await browser.elementByCss('#all-ssg-content').text()
|
||||||
expect(text).toBe('{"rest":["hello1","hello2"]}')
|
expect(text).toBe('{"rest":["hello1","hello2"]}')
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -501,6 +562,39 @@ function runTests(dev) {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (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 () => {
|
it('should work with HMR correctly', async () => {
|
||||||
const browser = await webdriver(appPort, '/post-1/comments')
|
const browser = await webdriver(appPort, '/post-1/comments')
|
||||||
let text = await browser.eval(`document.documentElement.innerHTML`)
|
let text = await browser.eval(`document.documentElement.innerHTML`)
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
// target
|
||||||
rewrites() {
|
rewrites() {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
source: '/some-rewrite/:item',
|
||||||
|
destination: '/blog/post-:item',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
source: '/about',
|
source: '/about',
|
||||||
destination: '/lang/en/about',
|
destination: '/lang/en/about',
|
||||||
|
|
|
@ -29,15 +29,15 @@ import url from 'url'
|
||||||
|
|
||||||
jest.setTimeout(1000 * 60 * 2)
|
jest.setTimeout(1000 * 60 * 2)
|
||||||
const appDir = join(__dirname, '..')
|
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 indexPage = join(__dirname, '../pages/index.js')
|
||||||
|
const nextConfig = new File(nextConfigPath)
|
||||||
let app
|
let app
|
||||||
let appPort
|
let appPort
|
||||||
let buildId
|
let buildId
|
||||||
let distPagesDir
|
let distPagesDir
|
||||||
let exportDir
|
let exportDir
|
||||||
let stderr
|
let stderr
|
||||||
let origConfig
|
|
||||||
|
|
||||||
const startServer = async (optEnv = {}) => {
|
const startServer = async (optEnv = {}) => {
|
||||||
const scriptPath = join(appDir, 'server.js')
|
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.eval('window.beforeNav')).toBe('hi')
|
||||||
expect(await browser.elementByCss('#about').text()).toBe('About: en')
|
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) {
|
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 () => {
|
it('should not show warning from url prop being returned', async () => {
|
||||||
const urlPropPage = join(appDir, 'pages/url-prop.js')
|
const urlPropPage = join(appDir, 'pages/url-prop.js')
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
|
@ -1707,30 +1717,6 @@ const runTests = (dev = false, isEmulatedServerless = false) => {
|
||||||
describe('SSG Prerender', () => {
|
describe('SSG Prerender', () => {
|
||||||
describe('dev mode', () => {
|
describe('dev mode', () => {
|
||||||
beforeAll(async () => {
|
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()
|
appPort = await findPort()
|
||||||
app = await launchApp(appDir, appPort, {
|
app = await launchApp(appDir, appPort, {
|
||||||
env: { __NEXT_TEST_WITH_DEVTOOL: 1 },
|
env: { __NEXT_TEST_WITH_DEVTOOL: 1 },
|
||||||
|
@ -1741,7 +1727,6 @@ describe('SSG Prerender', () => {
|
||||||
buildId = 'development'
|
buildId = 'development'
|
||||||
})
|
})
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await fs.writeFile(nextConfig, origConfig)
|
|
||||||
await killApp(app)
|
await killApp(app)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1750,13 +1735,10 @@ describe('SSG Prerender', () => {
|
||||||
|
|
||||||
describe('dev mode getStaticPaths', () => {
|
describe('dev mode getStaticPaths', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
origConfig = await fs.readFile(nextConfig, 'utf8')
|
nextConfig.write(
|
||||||
await fs.writeFile(
|
|
||||||
nextConfig,
|
|
||||||
// we set cpus to 1 so that we make sure the requests
|
// we set cpus to 1 so that we make sure the requests
|
||||||
// aren't being cached at the jest-worker level
|
// aren't being cached at the jest-worker level
|
||||||
`module.exports = { experimental: { cpus: 1 } }`,
|
`module.exports = { experimental: { cpus: 1 } }`
|
||||||
'utf8'
|
|
||||||
)
|
)
|
||||||
await fs.remove(join(appDir, '.next'))
|
await fs.remove(join(appDir, '.next'))
|
||||||
appPort = await findPort()
|
appPort = await findPort()
|
||||||
|
@ -1765,7 +1747,7 @@ describe('SSG Prerender', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await fs.writeFile(nextConfig, origConfig)
|
nextConfig.restore()
|
||||||
await killApp(app)
|
await killApp(app)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1808,7 +1790,6 @@ describe('SSG Prerender', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// remove firebase import since it breaks in legacy serverless mode
|
// remove firebase import since it breaks in legacy serverless mode
|
||||||
origBlogPageContent = await fs.readFile(blogPagePath, 'utf8')
|
origBlogPageContent = await fs.readFile(blogPagePath, 'utf8')
|
||||||
origConfig = await fs.readFile(nextConfig, 'utf8')
|
|
||||||
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
blogPagePath,
|
blogPagePath,
|
||||||
|
@ -1818,25 +1799,7 @@ describe('SSG Prerender', () => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await fs.writeFile(
|
nextConfig.replace('// target', `target: 'serverless',`)
|
||||||
nextConfig,
|
|
||||||
`module.exports = {
|
|
||||||
target: 'serverless',
|
|
||||||
rewrites() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: '/about',
|
|
||||||
destination: '/lang/en/about'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: '/blocked-create',
|
|
||||||
destination: '/blocking-fallback/blocked-create',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
'utf8'
|
|
||||||
)
|
|
||||||
await fs.remove(join(appDir, '.next'))
|
await fs.remove(join(appDir, '.next'))
|
||||||
await nextBuild(appDir)
|
await nextBuild(appDir)
|
||||||
stderr = ''
|
stderr = ''
|
||||||
|
@ -1850,7 +1813,7 @@ describe('SSG Prerender', () => {
|
||||||
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
|
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
|
||||||
})
|
})
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await fs.writeFile(nextConfig, origConfig)
|
nextConfig.restore()
|
||||||
await fs.writeFile(blogPagePath, origBlogPageContent)
|
await fs.writeFile(blogPagePath, origBlogPageContent)
|
||||||
await killApp(app)
|
await killApp(app)
|
||||||
})
|
})
|
||||||
|
@ -1931,11 +1894,9 @@ describe('SSG Prerender', () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
origConfig = await fs.readFile(nextConfig, 'utf8')
|
await nextConfig.replace(
|
||||||
await fs.writeFile(
|
'// target',
|
||||||
nextConfig,
|
`target: 'experimental-serverless-trace',`
|
||||||
`module.exports = { target: 'experimental-serverless-trace' }`,
|
|
||||||
'utf8'
|
|
||||||
)
|
)
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
cstmError,
|
cstmError,
|
||||||
|
@ -1963,8 +1924,8 @@ describe('SSG Prerender', () => {
|
||||||
app = await startServerlessEmulator(appDir, appPort, buildId)
|
app = await startServerlessEmulator(appDir, appPort, buildId)
|
||||||
})
|
})
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
nextConfig.restore()
|
||||||
await fs.remove(cstmError)
|
await fs.remove(cstmError)
|
||||||
await fs.writeFile(nextConfig, origConfig)
|
|
||||||
await killApp(app)
|
await killApp(app)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -2024,9 +1985,7 @@ describe('SSG Prerender', () => {
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
exportDir = join(appDir, 'out')
|
exportDir = join(appDir, 'out')
|
||||||
origConfig = await fs.readFile(nextConfig, 'utf8')
|
nextConfig.write(
|
||||||
await fs.writeFile(
|
|
||||||
nextConfig,
|
|
||||||
`module.exports = {
|
`module.exports = {
|
||||||
exportTrailingSlash: true,
|
exportTrailingSlash: true,
|
||||||
exportPathMap: function(defaultPathMap) {
|
exportPathMap: function(defaultPathMap) {
|
||||||
|
@ -2075,7 +2034,7 @@ describe('SSG Prerender', () => {
|
||||||
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
|
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
|
||||||
})
|
})
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await fs.writeFile(nextConfig, origConfig)
|
nextConfig.restore()
|
||||||
await stopApp(app)
|
await stopApp(app)
|
||||||
|
|
||||||
for (const page of fallbackTruePages) {
|
for (const page of fallbackTruePages) {
|
||||||
|
|
|
@ -80,7 +80,7 @@ describe('Production response size', () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// These numbers are without gzip compression!
|
// 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).toBeLessThanOrEqual(1024) // don't increase size more than 1kb
|
||||||
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
|
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue