未验证 提交 1b36f0c0 编写于 作者: J Jan Potoms 提交者: GitHub

Fix pages/index.js and pages/index/index.js behavior (#13699)

Disambiguate between pages/index.js and pages/index/index.js so that they resolve differently.
It all started with a bug in pagesmanifest that propagated throughout the codebase. After fixing pagesmanifest I was able to remove a few hacks here and there and more logic is shared now. especially the logic that resolves an entrypoint back into a route path. To sum up what happened:

- `getRouteFromEntrypoint` is the inverse operation of `getPageFile` that's under `pages/_document.tsx`
- `denormalizePagePath` is the inverse operation of `normalizePagePath`.

Everything is refactored in terms of these operations, that makes their behavior uniform and easier to update/patch in a central place. Before there were subtle differences between those that made `index/index.js` hard to handle.

Some potential follow up on this PR:
- [`hot-reloader`](https://github.com/vercel/next.js/pull/13699/files#diff-6161346d2c5f4b7abc87059d8768c44bR207) still has one place that does very similar behavior to `getRouteFromEntrypoint`. It can probably be rewritten in terms of `getRouteFromEntrypoint`.
- There are a few places where `denormalizePagePath(normalizePagePath(...))` is happening. This is a sign that `normalizePagePath` is doing some validation that is independent of its rewriting logic. That should probably be factored out in its own function. after that I should probably investigate whether `normalizePagePath` is even still needed at all.
- a lot of code is doing `.replace(/\\/g, '')`. If wanted, that could be replaced with `normalizePathSep`.
- It looks to me like some logic that's spread across the project can be centralized in 4 functions 
  - `getRouteFromEntrypoint` (part of this PR)
  - its inverse `getEntrypointFromRoute` (already exists in `_document.tsx` as `getPageFile`)
  - `getRouteFromPageFile` 
  - its inverse `getPageFileFromRoute` (already exists as `findPageFile ` in `server/lib/find-page-file.ts`)

  It could be beneficial to structure the code to keep these fuctionalities close together and name them similarly.
 - revise `index.amp` handling in pagesmanifest. I left it alone in this PR to keep it scoped, but it may be broken wrt nested index files as well. It might even make sense to reshape the pagesmanifest altogether to handle html/json/amp/... better
上级 d8f2f816
......@@ -24,7 +24,6 @@ export function createPagesMapping(
let page = `${pagePath
.replace(new RegExp(`\\.+(${extensions.join('|')})$`), '')
.replace(/\\/g, '/')}`.replace(/\/index$/, '')
page = page === '/index' ? '/' : page
const pageKey = page === '' ? '/' : page
......
......@@ -853,9 +853,11 @@ export default async function build(dir: string, conf = null): Promise<void> {
)
if (!isSsg) {
pagesManifest[page] = relativeDest
if (page === '/') pagesManifest['/index'] = relativeDest
if (page === '/.amp') pagesManifest['/index.amp'] = relativeDest
if (page === '/.amp') {
pagesManifest['/index.amp'] = relativeDest
} else {
pagesManifest[page] = relativeDest
}
}
await promises.mkdir(path.dirname(dest), { recursive: true })
await promises.rename(orig, dest)
......@@ -943,7 +945,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
await promises.rmdir(exportOptions.outdir)
await promises.writeFile(
manifestPath,
JSON.stringify(pagesManifest),
JSON.stringify(pagesManifest, null, 2),
'utf8'
)
}
......
......@@ -8,9 +8,9 @@ import {
CLIENT_STATIC_FILES_RUNTIME_POLYFILLS,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
IS_BUNDLED_PAGE_REGEX,
ROUTE_NAME_REGEX,
} from '../../../next-server/lib/constants'
import { BuildManifest } from '../../../next-server/server/get-page-files'
import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-entrypoint'
// This function takes the asset map generated in BuildManifestPlugin and creates a
// reduced version to send to the client.
......@@ -79,14 +79,8 @@ export default class BuildManifestPlugin {
)
assetMap.devFiles.push(...(reactRefreshChunk?.files ?? []))
// compilation.entrypoints is a Map object, so iterating over it 0 is the key and 1 is the value
for (const [, entrypoint] of compilation.entrypoints.entries()) {
const result = ROUTE_NAME_REGEX.exec(entrypoint.name)
if (!result) {
continue
}
const pagePath = result[1]
for (const entrypoint of compilation.entrypoints.values()) {
const pagePath = getRouteFromEntrypoint(entrypoint.name)
if (!pagePath) {
continue
......@@ -113,10 +107,7 @@ export default class BuildManifestPlugin {
filesForEntry.push(file.replace(/\\/g, '/'))
}
assetMap.pages[`/${pagePath.replace(/\\/g, '/')}`] = [
...filesForEntry,
...mainJsFiles,
]
assetMap.pages[pagePath] = [...filesForEntry, ...mainJsFiles]
}
if (typeof assetMap.pages['/index'] !== 'undefined') {
......
import { Compiler, Plugin } from 'webpack'
import { RawSource } from 'webpack-sources'
import {
PAGES_MANIFEST,
ROUTE_NAME_REGEX,
SERVERLESS_ROUTE_NAME_REGEX,
} from '../../../next-server/lib/constants'
import { PAGES_MANIFEST } from '../../../next-server/lib/constants'
import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-entrypoint'
export type PagesManifest = { [page: string]: string }
......@@ -24,33 +21,19 @@ export default class PagesManifestPlugin implements Plugin {
const pages: PagesManifest = {}
for (const chunk of chunks) {
const result = (this.serverless
? SERVERLESS_ROUTE_NAME_REGEX
: ROUTE_NAME_REGEX
).exec(chunk.name)
if (!result) {
continue
}
const pagePath = result[1]
const pagePath = getRouteFromEntrypoint(chunk.name, this.serverless)
if (!pagePath) {
continue
}
// Write filename, replace any backslashes in path (on windows) with forwardslashes for cross-platform consistency.
pages[`/${pagePath.replace(/\\/g, '/')}`] = chunk.name.replace(
/\\/g,
'/'
)
}
if (typeof pages['/index'] !== 'undefined') {
pages['/'] = pages['/index']
pages[pagePath] = chunk.name.replace(/\\/g, '/')
}
compilation.assets[PAGES_MANIFEST] = new RawSource(JSON.stringify(pages))
compilation.assets[PAGES_MANIFEST] = new RawSource(
JSON.stringify(pages, null, 2)
)
})
}
}
......@@ -28,12 +28,19 @@ function normalizeRoute(route) {
if (route[0] !== '/') {
throw new Error(`Route name should start with a "/", got "${route}"`)
}
route = route.replace(/\/index$/, '/')
if (route === '/') return route
return route.replace(/\/$/, '')
}
export function getAssetPath(route) {
return route === '/'
? '/index'
: /^\/index(\/|$)/.test(route)
? `/index${route}`
: `${route}`
}
function appendLink(href, rel, as) {
return new Promise((res, rej, link) => {
link = document.createElement('link')
......@@ -99,9 +106,8 @@ export default class PageLoader {
getDataHref(href, asPath) {
const getHrefForSlug = (/** @type string */ path) => {
path = delBasePath(path)
return `${this.assetPrefix}/_next/data/${this.buildId}${
path === '/' ? '/index' : path
}.json`
const dataRoute = getAssetPath(path)
return `${this.assetPrefix}/_next/data/${this.buildId}${dataRoute}.json`
}
const { pathname: hrefPathname, query } = parse(href, true)
......@@ -245,11 +251,11 @@ export default class PageLoader {
loadRoute(route) {
route = normalizeRoute(route)
let scriptRoute = route === '/' ? '/index.js' : `${route}.js`
let scriptRoute = getAssetPath(route)
const url = `${this.assetPrefix}/_next/static/${encodeURIComponent(
this.buildId
)}/pages${encodeURI(scriptRoute)}`
)}/pages${encodeURI(scriptRoute)}.js`
this.loadScript(url, route, true)
}
......@@ -327,14 +333,13 @@ export default class PageLoader {
} else {
route = normalizeRoute(route)
let scriptRoute = `${route === '/' ? '/index' : route}.js`
if (process.env.__NEXT_MODERN_BUILD && hasNoModule) {
scriptRoute = scriptRoute.replace(/\.js$/, '.module.js')
}
const scriptRoute = getAssetPath(route)
const ext =
process.env.__NEXT_MODERN_BUILD && hasNoModule ? '.module.js' : '.js'
url = `${this.assetPrefix}/_next/static/${encodeURIComponent(
this.buildId
)}/pages${encodeURI(scriptRoute)}`
)}/pages${encodeURI(scriptRoute)}${ext}`
}
return Promise.all(
......
......@@ -33,7 +33,10 @@ import loadConfig, {
} from '../next-server/server/config'
import { eventCliSession } from '../telemetry/events'
import { Telemetry } from '../telemetry/storage'
import { normalizePagePath } from '../next-server/server/normalize-page-path'
import {
normalizePagePath,
denormalizePagePath,
} from '../next-server/server/normalize-page-path'
import { loadEnvConfig } from '../lib/load-env-config'
import { PrerenderManifest } from '../build'
import type exportPage from './worker'
......@@ -287,8 +290,8 @@ export default async function exportApp(
// make sure to prevent duplicates
const exportPaths = [
...new Set(
Object.keys(exportPathMap).map(
(path) => normalizePagePath(path).replace(/^\/index$/, '') || '/'
Object.keys(exportPathMap).map((path) =>
denormalizePagePath(normalizePagePath(path))
)
),
]
......
......@@ -31,9 +31,6 @@ export const CLIENT_STATIC_FILES_RUNTIME_WEBPACK = `${CLIENT_STATIC_FILES_RUNTIM
export const CLIENT_STATIC_FILES_RUNTIME_POLYFILLS = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/polyfills.js`
// matches static/<buildid>/pages/<page>.js
export const IS_BUNDLED_PAGE_REGEX = /^static[/\\][^/\\]+[/\\]pages.*\.js$/
// matches static/<buildid>/pages/:page*.js
export const ROUTE_NAME_REGEX = /^static[/\\][^/\\]+[/\\]pages[/\\](.*)\.js$/
export const SERVERLESS_ROUTE_NAME_REGEX = /^pages[/\\](.*)\.js$/
export const TEMPORARY_REDIRECT_STATUS = 307
export const PERMANENT_REDIRECT_STATUS = 308
export const STATIC_PROPS_ID = '__N_SSG'
......
import { normalizePagePath } from './normalize-page-path'
import { normalizePagePath, denormalizePagePath } from './normalize-page-path'
export type BuildManifest = {
devFiles: string[]
......@@ -17,7 +17,7 @@ export function getPageFiles(
let files = buildManifest.pages[normalizedPage]
if (!files) {
files = buildManifest.pages[normalizedPage.replace(/\/index$/, '') || '/']
files = buildManifest.pages[denormalizePagePath(normalizedPage)]
}
if (!files) {
......
import { denormalizePagePath } from './normalize-page-path'
// matches static/<buildid>/pages/:page*.js
const ROUTE_NAME_REGEX = /^static[/\\][^/\\]+[/\\]pages[/\\](.*)\.js$/
const SERVERLESS_ROUTE_NAME_REGEX = /^pages[/\\](.*)\.js$/
export default function getRouteFromEntrypoint(
entryFile: string,
isServerlessLike: boolean = false
): string | null {
const result = (isServerlessLike
? SERVERLESS_ROUTE_NAME_REGEX
: ROUTE_NAME_REGEX
).exec(entryFile)
if (!result) {
return null
}
const pagePath = result[1]
if (!pagePath) {
return null
}
return denormalizePagePath(`/${pagePath}`)
}
......@@ -834,6 +834,16 @@ export default class Server {
)
}
if (
this.renderOpts.customServer &&
pathname === '/index' &&
!(await this.hasPage('/index'))
) {
// maintain backwards compatibility for custom server
// (see custom-server integration tests)
pathname = '/'
}
const url: any = req.url
// we allow custom servers to call render for all URLs
......
import { posix } from 'path'
export function normalizePathSep(path: string): string {
return path.replace(/\\/g, '/')
}
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 (page === '/') {
page = '/index'
} else if (/^\/index(\/|$)/.test(page)) {
page = `/index${page}`
}
// Resolve on anything that doesn't start with `/`
if (!page.startsWith('/')) {
......@@ -17,3 +24,13 @@ 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
}
......@@ -290,7 +290,6 @@ export async function renderToHTML(
query: ParsedUrlQuery,
renderOpts: RenderOpts
): Promise<string | null> {
pathname = pathname === '/index' ? '/' : pathname
const {
err,
dev = false,
......
......@@ -5,7 +5,7 @@ import {
SERVER_DIRECTORY,
SERVERLESS_DIRECTORY,
} from '../lib/constants'
import { normalizePagePath } from './normalize-page-path'
import { normalizePagePath, denormalizePagePath } from './normalize-page-path'
import { PagesManifest } from '../../build/webpack/plugins/pages-manifest-plugin'
export function pageNotFoundError(page: string): Error {
......@@ -30,7 +30,7 @@ export function getPagePath(
)) as PagesManifest
try {
page = normalizePagePath(page)
page = denormalizePagePath(normalizePagePath(page))
} catch (err) {
// tslint:disable-next-line
console.error(err)
......@@ -38,12 +38,7 @@ export function getPagePath(
}
if (!pagesManifest[page]) {
const cleanedPage = page.replace(/^\/index$/, '') || '/'
if (!pagesManifest[cleanedPage]) {
throw pageNotFoundError(page)
} else {
page = cleanedPage
}
throw pageNotFoundError(page)
}
return join(serverBuildPath, pagesManifest[page])
}
......
......@@ -826,6 +826,11 @@ function getAmpPath(ampPath: string, asPath: string): string {
}
function getPageFile(page: string, buildId?: string): string {
const startingUrl = page === '/' ? '/index' : page
const startingUrl =
page === '/'
? '/index'
: /^\/index(\/|$)/.test(page)
? `/index${page}`
: page
return buildId ? `${startingUrl}.${buildId}.js` : `${startingUrl}.js`
}
......@@ -16,13 +16,17 @@ import {
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
IS_BUNDLED_PAGE_REGEX,
ROUTE_NAME_REGEX,
} from '../next-server/lib/constants'
import { __ApiPreviewProps } from '../next-server/server/api-utils'
import { route } from '../next-server/server/router'
import errorOverlayMiddleware from './lib/error-overlay-middleware'
import { findPageFile } from './lib/find-page-file'
import onDemandEntryHandler, { normalizePage } from './on-demand-entry-handler'
import onDemandEntryHandler from './on-demand-entry-handler'
import {
denormalizePagePath,
normalizePathSep,
} from '../next-server/server/normalize-page-path'
import getRouteFromEntrypoint from '../next-server/server/get-route-from-entrypoint'
export async function renderScriptError(res: ServerResponse, error: Error) {
// Asks CDNs and others to not to cache the errored page
......@@ -200,7 +204,7 @@ export default class HotReloader {
return {}
}
const page = `/${params.path.join('/')}`
const page = denormalizePagePath(`/${params.path.join('/')}`)
if (page === '/_error' || BLOCKED_PAGES.indexOf(page) === -1) {
try {
await this.ensurePage(page)
......@@ -423,18 +427,14 @@ export default class HotReloader {
if (addedPages.size > 0) {
for (const addedPage of addedPages) {
let page =
'/' + ROUTE_NAME_REGEX.exec(addedPage)![1].replace(/\\/g, '/')
page = page === '/index' ? '/' : page
const page = getRouteFromEntrypoint(addedPage)
this.send('addedPage', page)
}
}
if (removedPages.size > 0) {
for (const removedPage of removedPages) {
let page =
'/' + ROUTE_NAME_REGEX.exec(removedPage)![1].replace(/\\/g, '/')
page = page === '/index' ? '/' : page
const page = getRouteFromEntrypoint(removedPage)
this.send('removedPage', page)
}
}
......@@ -512,13 +512,13 @@ export default class HotReloader {
}
public async getCompilationErrors(page: string) {
const normalizedPage = normalizePage(page)
const normalizedPage = normalizePathSep(page)
if (this.stats.hasErrors()) {
const { compilation } = this.stats
const failedPages = erroredPages(compilation, {
enhanceName(name) {
return '/' + ROUTE_NAME_REGEX.exec(name)![1]
return getRouteFromEntrypoint(name) as string
},
})
......
......@@ -4,6 +4,7 @@ import { isWriteable } from '../../build/is-writeable'
import { warn } from '../../build/output/log'
import fs from 'fs'
import { promisify } from 'util'
import { denormalizePagePath } from '../../next-server/server/normalize-page-path'
const readdir = promisify(fs.readdir)
......@@ -24,27 +25,21 @@ export async function findPageFile(
normalizedPagePath: string,
pageExtensions: string[]
): Promise<string | null> {
let foundPagePaths: string[] = []
const foundPagePaths: string[] = []
const page = denormalizePagePath(normalizedPagePath)
for (const extension of pageExtensions) {
const relativePagePath = `${normalizedPagePath}.${extension}`
const pagePath = join(rootDir, relativePagePath)
if (!normalizedPagePath.endsWith('/index')) {
const relativePagePath = `${page}.${extension}`
const pagePath = join(rootDir, relativePagePath)
// only /index and /sub/index when /sub/index/index.js is allowed
// see test/integration/route-indexes for expected index handling
if (
normalizedPagePath.startsWith('/index') ||
!normalizedPagePath.endsWith('/index')
) {
if (await isWriteable(pagePath)) {
foundPagePaths.push(relativePagePath)
}
}
const relativePagePathWithIndex = join(
normalizedPagePath,
`index.${extension}`
)
const relativePagePathWithIndex = join(page, `index.${extension}`)
const pagePathWithIndex = join(rootDir, relativePagePathWithIndex)
if (await isWriteable(pagePathWithIndex)) {
foundPagePaths.push(relativePagePathWithIndex)
......
......@@ -10,10 +10,13 @@ import { isWriteable } from '../build/is-writeable'
import * as Log from '../build/output/log'
import { ClientPagesLoaderOptions } from '../build/webpack/loaders/next-client-pages-loader'
import { API_ROUTE } from '../lib/constants'
import { ROUTE_NAME_REGEX } from '../next-server/lib/constants'
import { normalizePagePath } from '../next-server/server/normalize-page-path'
import {
normalizePagePath,
normalizePathSep,
} from '../next-server/server/normalize-page-path'
import { pageNotFoundError } from '../next-server/server/require'
import { findPageFile } from './lib/find-page-file'
import getRouteFromEntrypoint from '../next-server/server/get-route-from-entrypoint'
const ADDED = Symbol('added')
const BUILDING = Symbol('building')
......@@ -93,21 +96,13 @@ export default function onDemandEntryHandler(
)
}
function getPagePathsFromEntrypoints(entrypoints: any) {
function getPagePathsFromEntrypoints(entrypoints: any): string[] {
const pagePaths = []
for (const [, entrypoint] of entrypoints.entries()) {
const result = ROUTE_NAME_REGEX.exec(entrypoint.name)
if (!result) {
continue
}
const pagePath = result[1]
if (!pagePath) {
continue
for (const entrypoint of entrypoints.values()) {
const page = getRouteFromEntrypoint(entrypoint.name)
if (page) {
pagePaths.push(page)
}
pagePaths.push(pagePath)
}
return pagePaths
......@@ -120,10 +115,7 @@ export default function onDemandEntryHandler(
...getPagePathsFromEntrypoints(serverStats.compilation.entrypoints),
])
// compilation.entrypoints is a Map object, so iterating over it 0 is the key and 1 is the value
for (const pagePath of pagePaths) {
const page = normalizePage('/' + pagePath)
for (const page of pagePaths) {
const entry = entries[page]
if (!entry) {
continue
......@@ -153,7 +145,7 @@ export default function onDemandEntryHandler(
disposeHandler.unref()
function handlePing(pg: string) {
const page = normalizePage(pg)
const page = normalizePathSep(pg)
const entryInfo = entries[page]
let toSend
......@@ -229,7 +221,7 @@ export default function onDemandEntryHandler(
return new Promise((resolve, reject) => {
// Makes sure the page that is being kept in on-demand-entries matches the webpack output
const normalizedPage = normalizePage(page)
const normalizedPage = normalizePathSep(page)
const entryInfo = entries[normalizedPage]
if (entryInfo) {
......@@ -316,16 +308,6 @@ function disposeInactiveEntries(
}
}
// /index and / is the same. So, we need to identify both pages as the same.
// This also applies to sub pages as well.
export function normalizePage(page: string) {
const unixPagePath = page.replace(/\\/g, '/')
if (unixPagePath === '/index' || unixPagePath === '/') {
return '/'
}
return unixPagePath.replace(/\/index$/, '')
}
// Make sure only one invalidation happens at a time
// Otherwise, webpack hash gets changed and it'll force the client to reload.
class Invalidator {
......
......@@ -789,11 +789,12 @@ describe('Client Navigation', () => {
await browser.close()
})
it('should work with /index page', async () => {
it('should not work with /index page', async () => {
const browser = await webdriver(context.appPort, '/index')
const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.')
expect(await browser.elementByCss('h1').text()).toBe('404')
expect(await browser.elementByCss('h2').text()).toBe(
'This page could not be found.'
)
await browser.close()
})
......
export default function Index() {
return <div id="page">index</div>
}
export default function Index() {
return <div id="page">index > index</div>
}
export default function Index() {
return <div id="page">index > index > index</div>
}
export default function Index() {
return <div id="page">index > project</div>
}
export default function Index() {
return <div id="page">index > user</div>
}
import Link from 'next/link'
export default function Index() {
return (
<div id="page">
<Link href="/">
<a id="link1">link to /</a>
</Link>
{' | '}
<Link href="/index">
<a id="link2">link to /index</a>
</Link>
{' | '}
<Link href="/index/index">
<a id="link3">link to /index/index</a>
</Link>
{' | '}
<Link href="/index/index/index">
<a id="link4">link to /index/index/index</a>
</Link>
{' | '}
<Link href="/index/user">
<a id="link5">link to /index/user</a>
</Link>
{' | '}
<Link href="/index/project">
<a id="link6">link to /index/project</a>
</Link>
</div>
)
}
/* eslint-env jest */
import cheerio from 'cheerio'
import fs from 'fs-extra'
import {
fetchViaHTTP,
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
renderViaHTTP,
check,
waitFor,
} from 'next-test-utils'
import webdriver from 'next-webdriver'
import { join } from 'path'
jest.setTimeout(1000 * 60 * 2)
let app
let appPort
const appDir = join(__dirname, '../')
function runTests() {
it('should ssr page /', async () => {
const html = await renderViaHTTP(appPort, '/')
const $ = cheerio.load(html)
expect($('#page').text()).toBe('index')
})
it('should client render page /', async () => {
const browser = await webdriver(appPort, '/')
try {
const text = await browser.elementByCss('#page').text()
expect(text).toBe('index')
} finally {
await browser.close()
}
})
it('should follow link to /', async () => {
const browser = await webdriver(appPort, '/links')
try {
await browser.elementByCss('#link1').click()
await waitFor(1000)
await check(() => browser.elementByCss('#page').text(), /^index$/)
} finally {
await browser.close()
}
})
it('should ssr page /index', async () => {
const html = await renderViaHTTP(appPort, '/index')
const $ = cheerio.load(html)
expect($('#page').text()).toBe('index > index')
})
it('should client render page /index', async () => {
const browser = await webdriver(appPort, '/index')
try {
const text = await browser.elementByCss('#page').text()
expect(text).toBe('index > index')
} finally {
await browser.close()
}
})
it('should follow link to /index', async () => {
const browser = await webdriver(appPort, '/links')
try {
await browser.elementByCss('#link2').click()
await waitFor(1000)
await check(() => browser.elementByCss('#page').text(), /^index > index$/)
} finally {
await browser.close()
}
})
it('should ssr page /index/user', async () => {
const html = await renderViaHTTP(appPort, '/index/user')
const $ = cheerio.load(html)
expect($('#page').text()).toBe('index > user')
})
it('should client render page /index/user', async () => {
const browser = await webdriver(appPort, '/index/user')
try {
const text = await browser.elementByCss('#page').text()
expect(text).toBe('index > user')
} finally {
await browser.close()
}
})
it('should follow link to /index/user', async () => {
const browser = await webdriver(appPort, '/links')
try {
await browser.elementByCss('#link5').click()
await waitFor(1000)
await check(() => browser.elementByCss('#page').text(), /^index > user$/)
} finally {
await browser.close()
}
})
it('should ssr page /index/project', async () => {
const html = await renderViaHTTP(appPort, '/index/project')
const $ = cheerio.load(html)
expect($('#page').text()).toBe('index > project')
})
it('should client render page /index/project', async () => {
const browser = await webdriver(appPort, '/index/project')
try {
const text = await browser.elementByCss('#page').text()
expect(text).toBe('index > project')
} finally {
await browser.close()
}
})
it('should follow link to /index/project', async () => {
const browser = await webdriver(appPort, '/links')
try {
await browser.elementByCss('#link6').click()
await waitFor(1000)
await check(
() => browser.elementByCss('#page').text(),
/^index > project$/
)
} finally {
await browser.close()
}
})
it('should ssr page /index/index', async () => {
const html = await renderViaHTTP(appPort, '/index/index')
const $ = cheerio.load(html)
expect($('#page').text()).toBe('index > index > index')
})
it('should client render page /index/index', async () => {
const browser = await webdriver(appPort, '/index/index')
try {
const text = await browser.elementByCss('#page').text()
expect(text).toBe('index > index > index')
} finally {
await browser.close()
}
})
it('should follow link to /index/index', async () => {
const browser = await webdriver(appPort, '/links')
try {
await browser.elementByCss('#link3').click()
await waitFor(1000)
await check(
() => browser.elementByCss('#page').text(),
/^index > index > index$/
)
} finally {
await browser.close()
}
})
it('should 404 on /index/index/index', async () => {
const response = await fetchViaHTTP(appPort, '/index/index/index')
expect(response.status).toBe(404)
})
it('should not find a link to /index/index/index', async () => {
const browser = await webdriver(appPort, '/links')
try {
await browser.elementByCss('#link4').click()
await waitFor(1000)
await check(() => browser.elementByCss('h1').text(), /404/)
} finally {
await browser.close()
}
})
}
const nextConfig = join(appDir, 'next.config.js')
describe('nested index.js', () => {
describe('dev mode', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(() => killApp(app))
runTests()
})
describe('production mode', () => {
beforeAll(async () => {
const curConfig = await fs.readFile(nextConfig, 'utf8')
if (curConfig.includes('target')) {
await fs.writeFile(nextConfig, `module.exports = {}`)
}
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(() => killApp(app))
runTests()
})
describe('serverless mode', () => {
let origNextConfig
beforeAll(async () => {
origNextConfig = await fs.readFile(nextConfig, 'utf8')
await fs.writeFile(
nextConfig,
`module.exports = { target: 'serverless' }`
)
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await fs.writeFile(nextConfig, origNextConfig)
await killApp(app)
})
runTests()
})
})
......@@ -19,8 +19,8 @@ const appDir = join(__dirname, '../')
const runTests = () => {
it('should handle / correctly', async () => {
const res = await fetchViaHTTP(appPort, '/')
expect(res.status).toBe(200)
expect(await res.text()).toContain('hello from index')
expect(res.status).toBe(404)
expect(await res.text()).toContain('page could not be found')
})
it('should handle /index correctly', async () => {
......
......@@ -25,8 +25,8 @@ const runTests = () => {
it('should handle /index correctly', async () => {
const res = await fetchViaHTTP(appPort, '/index')
expect(res.status).toBe(200)
expect(await res.text()).toContain('hello from index')
expect(res.status).toBe(404)
expect(await res.text()).toContain('page could not be found')
})
it('should handle /index/index correctly', async () => {
......
......@@ -54,9 +54,10 @@ describe('normalizePagePath', () => {
})
describe('getPagePath', () => {
it('Should append /index to the / page', () => {
const pagePath = getPagePath('/', distDir)
expect(pagePath).toBe(join(pathToBundles, `${sep}index.js`))
it('Should not append /index to the / page', () => {
expect(() => getPagePath('/', distDir)).toThrow(
'Cannot find module for page: /'
)
})
it('Should prepend / when a page does not have it', () => {
......@@ -70,9 +71,10 @@ describe('getPagePath', () => {
})
describe('requirePage', () => {
it('Should require /index.js when using /', async () => {
const page = await requirePage('/', distDir)
expect(page.test).toBe('hello')
it('Should not find page /index when using /', async () => {
await expect(() => requirePage('/', distDir)).toThrow(
'Cannot find module for page: /'
)
})
it('Should require /index.js when using /index', async () => {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册