From 86808bbce295e46b1ff36100de8215fcaac01a00 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Sun, 29 Dec 2019 20:03:12 +0100 Subject: [PATCH] Add basePath support (#9872) * Add basePath support * Add tests including copy of HMR tests * Add production tests * Add tests for serverless target * Add missing quotes --- packages/next/build/entries.ts | 1 + packages/next/build/webpack-config.ts | 3 + .../webpack/loaders/next-serverless-loader.ts | 22 + .../next/next-server/lib/router/router.ts | 10 +- packages/next/next-server/server/config.ts | 42 +- .../next/next-server/server/next-server.ts | 10 + .../components/hello-chunkfilename.js | 1 + .../basepath/components/hello-context.js | 13 + .../integration/basepath/components/hello1.js | 1 + .../integration/basepath/components/hello2.js | 1 + .../integration/basepath/components/hello3.js | 1 + .../integration/basepath/components/hello4.js | 1 + .../basepath/components/hmr/dynamic.js | 12 + .../basepath/components/nested1.js | 10 + .../basepath/components/nested2.js | 12 + .../basepath/components/welcome.js | 17 + test/integration/basepath/next.config.js | 9 + test/integration/basepath/pages/hello.js | 9 + test/integration/basepath/pages/hmr/about.js | 7 + test/integration/basepath/pages/hmr/about1.js | 7 + test/integration/basepath/pages/hmr/about2.js | 7 + test/integration/basepath/pages/hmr/about3.js | 7 + test/integration/basepath/pages/hmr/about4.js | 7 + test/integration/basepath/pages/hmr/about5.js | 7 + test/integration/basepath/pages/hmr/about6.js | 7 + test/integration/basepath/pages/hmr/about7.js | 7 + .../integration/basepath/pages/hmr/contact.js | 5 + .../integration/basepath/pages/hmr/counter.js | 19 + .../basepath/pages/hmr/error-in-gip.js | 11 + test/integration/basepath/pages/hmr/index.js | 9 + .../pages/hmr/style-dynamic-component.js | 8 + .../pages/hmr/style-stateful-component.js | 20 + test/integration/basepath/pages/hmr/style.js | 17 + test/integration/basepath/pages/other-page.js | 1 + test/integration/basepath/pages/ssr.js | 11 + test/integration/basepath/test/index.test.js | 497 ++++++++++++++++++ 36 files changed, 824 insertions(+), 5 deletions(-) create mode 100644 test/integration/basepath/components/hello-chunkfilename.js create mode 100644 test/integration/basepath/components/hello-context.js create mode 100644 test/integration/basepath/components/hello1.js create mode 100644 test/integration/basepath/components/hello2.js create mode 100644 test/integration/basepath/components/hello3.js create mode 100644 test/integration/basepath/components/hello4.js create mode 100644 test/integration/basepath/components/hmr/dynamic.js create mode 100644 test/integration/basepath/components/nested1.js create mode 100644 test/integration/basepath/components/nested2.js create mode 100644 test/integration/basepath/components/welcome.js create mode 100644 test/integration/basepath/next.config.js create mode 100644 test/integration/basepath/pages/hello.js create mode 100644 test/integration/basepath/pages/hmr/about.js create mode 100644 test/integration/basepath/pages/hmr/about1.js create mode 100644 test/integration/basepath/pages/hmr/about2.js create mode 100644 test/integration/basepath/pages/hmr/about3.js create mode 100644 test/integration/basepath/pages/hmr/about4.js create mode 100644 test/integration/basepath/pages/hmr/about5.js create mode 100644 test/integration/basepath/pages/hmr/about6.js create mode 100644 test/integration/basepath/pages/hmr/about7.js create mode 100644 test/integration/basepath/pages/hmr/contact.js create mode 100644 test/integration/basepath/pages/hmr/counter.js create mode 100644 test/integration/basepath/pages/hmr/error-in-gip.js create mode 100644 test/integration/basepath/pages/hmr/index.js create mode 100644 test/integration/basepath/pages/hmr/style-dynamic-component.js create mode 100644 test/integration/basepath/pages/hmr/style-stateful-component.js create mode 100644 test/integration/basepath/pages/hmr/style.js create mode 100644 test/integration/basepath/pages/other-page.js create mode 100644 test/integration/basepath/pages/ssr.js create mode 100644 test/integration/basepath/test/index.test.js diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 5f01296a0e0dd..ccbebdbc73625 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -77,6 +77,7 @@ export function createEntrypoints( generateEtags: config.generateEtags, ampBindInitData: config.experimental.ampBindInitData, canonicalBase: config.canonicalBase, + basePath: config.experimental.basePath, } Object.keys(pages).forEach(page => { diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 157fc76689a30..05f4f97b367b0 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -724,6 +724,9 @@ export default async function getBaseWebpackConfig( 'process.env.__NEXT_REACT_MODE': JSON.stringify( config.experimental.reactMode ), + 'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify( + config.experimental.basePath + ), ...(isServer ? { // Fix bad-actors in the npm ecosystem (e.g. `node-formidable`) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index bc69d3d8d1269..0b645f8c23cf3 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -21,6 +21,7 @@ export type ServerlessLoaderQuery = { ampBindInitData: boolean | string generateEtags: string canonicalBase: string + basePath: string } const nextServerlessLoader: loader.Loader = function() { @@ -36,8 +37,10 @@ const nextServerlessLoader: loader.Loader = function() { absoluteDocumentPath, absoluteErrorPath, generateEtags, + basePath, }: ServerlessLoaderQuery = typeof this.query === 'string' ? parse(this.query.substr(1)) : this.query + const buildManifest = join(distDir, BUILD_MANIFEST).replace(/\\/g, '/') const reactLoadableManifest = join(distDir, REACT_LOADABLE_MANIFEST).replace( /\\/g, @@ -128,6 +131,16 @@ const nextServerlessLoader: loader.Loader = function() { export default async (req, res) => { try { await initServer() + + ${ + basePath + ? ` + if(req.url.startsWith('${basePath}')) { + req.url = req.url.replace('${basePath}', '') + } + ` + : '' + } const parsedUrl = parse(req.url, true) const params = ${ @@ -181,6 +194,15 @@ const nextServerlessLoader: loader.Loader = function() { export const config = ComponentInfo['confi' + 'g'] || {} export const _app = App export async function renderReqToHTML(req, res, fromExport, _renderOpts, _params) { + ${ + basePath + ? ` + if(req.url.startsWith('${basePath}')) { + req.url = req.url.replace('${basePath}', '') + } + ` + : '' + } const options = { App, Document, diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 88d5d774c1434..fe99dcca461c2 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -18,6 +18,12 @@ import { isDynamicRoute } from './utils/is-dynamic' import { getRouteMatcher } from './utils/route-matcher' import { getRouteRegex } from './utils/route-regex' +function addBasePath(path: string): string { + // @ts-ignore variable is always a string + const p: string = process.env.__NEXT_ROUTER_BASEPATH + return path.indexOf(p) !== 0 ? p + path : path +} + function toRoute(path: string): string { return path.replace(/\/$/, '') || '/' } @@ -284,7 +290,7 @@ export default class Router implements BaseRouter { if (!options._h && this.onlyAHashChange(as)) { this.asPath = as Router.events.emit('hashChangeStart', as) - this.changeState(method, url, as) + this.changeState(method, url, addBasePath(as)) this.scrollToHash(as) Router.events.emit('hashChangeComplete', as) return resolve(true) @@ -346,7 +352,7 @@ export default class Router implements BaseRouter { } Router.events.emit('beforeHistoryChange', as) - this.changeState(method, url, as, options) + this.changeState(method, url, addBasePath(as), options) const hash = window.location.hash.substring(1) if (process.env.NODE_ENV !== 'production') { diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index 42b2242557d7b..e40d2538c484f 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -53,6 +53,7 @@ const defaultConfig: { [key: string]: any } = { deferScripts: false, reactMode: 'legacy', workerThreads: false, + basePath: '', }, future: { excludeDefaultMomentLocales: false, @@ -106,9 +107,44 @@ function assignDefaults(userConfig: { [key: string]: any }) { `Specified assetPrefix is not a string, found type "${typeof result.assetPrefix}" https://err.sh/zeit/next.js/invalid-assetprefix` ) } - if (result.experimental && result.experimental.css) { - // The new CSS support requires granular chunks be enabled. - result.experimental.granularChunks = true + if (result.experimental) { + if (result.experimental.css) { + // The new CSS support requires granular chunks be enabled. + result.experimental.granularChunks = true + } + + if (typeof result.experimental.basePath !== 'string') { + throw new Error( + `Specified basePath is not a string, found type "${typeof result + .experimental.basePath}"` + ) + } + + if (result.experimental.basePath !== '') { + if (result.experimental.basePath === '/') { + throw new Error( + `Specified basePath /. basePath has to be either an empty string or a path prefix"` + ) + } + + if (!result.experimental.basePath.startsWith('/')) { + throw new Error( + `Specified basePath has to start with a /, found "${result.experimental.basePath}"` + ) + } + + if (result.experimental.basePath !== '/') { + if (result.experimental.basePath.endsWith('/')) { + throw new Error( + `Specified basePath should not end with /, found "${result.experimental.basePath}"` + ) + } + + if (result.assetPrefix === '') { + result.assetPrefix = result.experimental.basePath + } + } + } } return result } diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 48e3b09d38eeb..35cafc198c629 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -232,6 +232,16 @@ export default class Server { parsedUrl.query = parseQs(parsedUrl.query) } + if (parsedUrl.pathname!.startsWith(this.nextConfig.experimental.basePath)) { + // If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/` + parsedUrl.pathname = + parsedUrl.pathname!.replace( + this.nextConfig.experimental.basePath, + '' + ) || '/' + req.url = req.url!.replace(this.nextConfig.experimental.basePath, '') + } + res.statusCode = 200 return this.run(req, res, parsedUrl).catch(err => { this.logError(err) diff --git a/test/integration/basepath/components/hello-chunkfilename.js b/test/integration/basepath/components/hello-chunkfilename.js new file mode 100644 index 0000000000000..9b1412df076bc --- /dev/null +++ b/test/integration/basepath/components/hello-chunkfilename.js @@ -0,0 +1 @@ +export default () =>
test chunkfilename
diff --git a/test/integration/basepath/components/hello-context.js b/test/integration/basepath/components/hello-context.js new file mode 100644 index 0000000000000..be67dad99f23b --- /dev/null +++ b/test/integration/basepath/components/hello-context.js @@ -0,0 +1,13 @@ +import React from 'react' +import PropTypes from 'prop-types' + +export default class extends React.Component { + static contextTypes = { + data: PropTypes.object, + } + + render() { + const { data } = this.context + return
{data.title}
+ } +} diff --git a/test/integration/basepath/components/hello1.js b/test/integration/basepath/components/hello1.js new file mode 100644 index 0000000000000..5773ec56fd494 --- /dev/null +++ b/test/integration/basepath/components/hello1.js @@ -0,0 +1 @@ +export default () =>

Hello World 1

diff --git a/test/integration/basepath/components/hello2.js b/test/integration/basepath/components/hello2.js new file mode 100644 index 0000000000000..37f05fda36bac --- /dev/null +++ b/test/integration/basepath/components/hello2.js @@ -0,0 +1 @@ +export default () =>

Hello World 2

diff --git a/test/integration/basepath/components/hello3.js b/test/integration/basepath/components/hello3.js new file mode 100644 index 0000000000000..5773ec56fd494 --- /dev/null +++ b/test/integration/basepath/components/hello3.js @@ -0,0 +1 @@ +export default () =>

Hello World 1

diff --git a/test/integration/basepath/components/hello4.js b/test/integration/basepath/components/hello4.js new file mode 100644 index 0000000000000..37f05fda36bac --- /dev/null +++ b/test/integration/basepath/components/hello4.js @@ -0,0 +1 @@ +export default () =>

Hello World 2

diff --git a/test/integration/basepath/components/hmr/dynamic.js b/test/integration/basepath/components/hmr/dynamic.js new file mode 100644 index 0000000000000..1e383981bbcb1 --- /dev/null +++ b/test/integration/basepath/components/hmr/dynamic.js @@ -0,0 +1,12 @@ +export default () => { + return ( +
+ Dynamic Component + +
+ ) +} diff --git a/test/integration/basepath/components/nested1.js b/test/integration/basepath/components/nested1.js new file mode 100644 index 0000000000000..73f9d1740df24 --- /dev/null +++ b/test/integration/basepath/components/nested1.js @@ -0,0 +1,10 @@ +import dynamic from 'next/dynamic' + +const Nested2 = dynamic(() => import('./nested2')) + +export default () => ( +
+ Nested 1 + +
+) diff --git a/test/integration/basepath/components/nested2.js b/test/integration/basepath/components/nested2.js new file mode 100644 index 0000000000000..8fe2f1ce0a684 --- /dev/null +++ b/test/integration/basepath/components/nested2.js @@ -0,0 +1,12 @@ +import dynamic from 'next/dynamic' + +const BrowserLoaded = dynamic(async () => () =>
Browser hydrated
, { + ssr: false, +}) + +export default () => ( +
+
Nested 2
+ +
+) diff --git a/test/integration/basepath/components/welcome.js b/test/integration/basepath/components/welcome.js new file mode 100644 index 0000000000000..589aac1fc3d85 --- /dev/null +++ b/test/integration/basepath/components/welcome.js @@ -0,0 +1,17 @@ +import React from 'react' + +export default class Welcome extends React.Component { + state = { name: null } + + componentDidMount() { + const { name } = this.props + this.setState({ name }) + } + + render() { + const { name } = this.state + if (!name) return null + + return

Welcome, {name}

+ } +} diff --git a/test/integration/basepath/next.config.js b/test/integration/basepath/next.config.js new file mode 100644 index 0000000000000..d9665f35964d7 --- /dev/null +++ b/test/integration/basepath/next.config.js @@ -0,0 +1,9 @@ +module.exports = { + onDemandEntries: { + // Make sure entries are not getting disposed. + maxInactiveAge: 1000 * 60 * 60, + }, + experimental: { + basePath: '/docs', + }, +} diff --git a/test/integration/basepath/pages/hello.js b/test/integration/basepath/pages/hello.js new file mode 100644 index 0000000000000..b72f0beaf5b77 --- /dev/null +++ b/test/integration/basepath/pages/hello.js @@ -0,0 +1,9 @@ +import Link from 'next/link' + +export default () => ( + + +

Hello World

+
+ +) diff --git a/test/integration/basepath/pages/hmr/about.js b/test/integration/basepath/pages/hmr/about.js new file mode 100644 index 0000000000000..b3411e8ace5ba --- /dev/null +++ b/test/integration/basepath/pages/hmr/about.js @@ -0,0 +1,7 @@ +export default () => { + return ( +
+

This is the about page.

+
+ ) +} diff --git a/test/integration/basepath/pages/hmr/about1.js b/test/integration/basepath/pages/hmr/about1.js new file mode 100644 index 0000000000000..b3411e8ace5ba --- /dev/null +++ b/test/integration/basepath/pages/hmr/about1.js @@ -0,0 +1,7 @@ +export default () => { + return ( +
+

This is the about page.

+
+ ) +} diff --git a/test/integration/basepath/pages/hmr/about2.js b/test/integration/basepath/pages/hmr/about2.js new file mode 100644 index 0000000000000..b3411e8ace5ba --- /dev/null +++ b/test/integration/basepath/pages/hmr/about2.js @@ -0,0 +1,7 @@ +export default () => { + return ( +
+

This is the about page.

+
+ ) +} diff --git a/test/integration/basepath/pages/hmr/about3.js b/test/integration/basepath/pages/hmr/about3.js new file mode 100644 index 0000000000000..b3411e8ace5ba --- /dev/null +++ b/test/integration/basepath/pages/hmr/about3.js @@ -0,0 +1,7 @@ +export default () => { + return ( +
+

This is the about page.

+
+ ) +} diff --git a/test/integration/basepath/pages/hmr/about4.js b/test/integration/basepath/pages/hmr/about4.js new file mode 100644 index 0000000000000..b3411e8ace5ba --- /dev/null +++ b/test/integration/basepath/pages/hmr/about4.js @@ -0,0 +1,7 @@ +export default () => { + return ( +
+

This is the about page.

+
+ ) +} diff --git a/test/integration/basepath/pages/hmr/about5.js b/test/integration/basepath/pages/hmr/about5.js new file mode 100644 index 0000000000000..b3411e8ace5ba --- /dev/null +++ b/test/integration/basepath/pages/hmr/about5.js @@ -0,0 +1,7 @@ +export default () => { + return ( +
+

This is the about page.

+
+ ) +} diff --git a/test/integration/basepath/pages/hmr/about6.js b/test/integration/basepath/pages/hmr/about6.js new file mode 100644 index 0000000000000..b3411e8ace5ba --- /dev/null +++ b/test/integration/basepath/pages/hmr/about6.js @@ -0,0 +1,7 @@ +export default () => { + return ( +
+

This is the about page.

+
+ ) +} diff --git a/test/integration/basepath/pages/hmr/about7.js b/test/integration/basepath/pages/hmr/about7.js new file mode 100644 index 0000000000000..b3411e8ace5ba --- /dev/null +++ b/test/integration/basepath/pages/hmr/about7.js @@ -0,0 +1,7 @@ +export default () => { + return ( +
+

This is the about page.

+
+ ) +} diff --git a/test/integration/basepath/pages/hmr/contact.js b/test/integration/basepath/pages/hmr/contact.js new file mode 100644 index 0000000000000..d6489a0a34107 --- /dev/null +++ b/test/integration/basepath/pages/hmr/contact.js @@ -0,0 +1,5 @@ +export default () => ( +
+

This is the contact page.

+
+) diff --git a/test/integration/basepath/pages/hmr/counter.js b/test/integration/basepath/pages/hmr/counter.js new file mode 100644 index 0000000000000..53cb27eaf1780 --- /dev/null +++ b/test/integration/basepath/pages/hmr/counter.js @@ -0,0 +1,19 @@ +import React from 'react' + +export default class Counter extends React.Component { + state = { count: 0 } + + incr() { + const { count } = this.state + this.setState({ count: count + 1 }) + } + + render() { + return ( +
+

COUNT: {this.state.count}

+ +
+ ) + } +} diff --git a/test/integration/basepath/pages/hmr/error-in-gip.js b/test/integration/basepath/pages/hmr/error-in-gip.js new file mode 100644 index 0000000000000..512cffa7b91be --- /dev/null +++ b/test/integration/basepath/pages/hmr/error-in-gip.js @@ -0,0 +1,11 @@ +import React from 'react' +export default class extends React.Component { + static getInitialProps() { + const error = new Error('an-expected-error-in-gip') + throw error + } + + render() { + return
Hello
+ } +} diff --git a/test/integration/basepath/pages/hmr/index.js b/test/integration/basepath/pages/hmr/index.js new file mode 100644 index 0000000000000..01557c87ae63f --- /dev/null +++ b/test/integration/basepath/pages/hmr/index.js @@ -0,0 +1,9 @@ +import Link from 'next/link' + +export default () => ( +
+ + Bad Page + +
+) diff --git a/test/integration/basepath/pages/hmr/style-dynamic-component.js b/test/integration/basepath/pages/hmr/style-dynamic-component.js new file mode 100644 index 0000000000000..9c8d5c5d342ab --- /dev/null +++ b/test/integration/basepath/pages/hmr/style-dynamic-component.js @@ -0,0 +1,8 @@ +import React from 'react' +import dynamic from 'next/dynamic' + +const HmrDynamic = dynamic(import('../../components/hmr/dynamic')) + +export default () => { + return +} diff --git a/test/integration/basepath/pages/hmr/style-stateful-component.js b/test/integration/basepath/pages/hmr/style-stateful-component.js new file mode 100644 index 0000000000000..47210f1868cda --- /dev/null +++ b/test/integration/basepath/pages/hmr/style-stateful-component.js @@ -0,0 +1,20 @@ +import React, { Component } from 'react' + +export default class StyleStateFul extends Component { + render() { + return ( + +
+

+ This is the style page. + +

+
+
+ ) + } +} diff --git a/test/integration/basepath/pages/hmr/style.js b/test/integration/basepath/pages/hmr/style.js new file mode 100644 index 0000000000000..f1a47a550eee1 --- /dev/null +++ b/test/integration/basepath/pages/hmr/style.js @@ -0,0 +1,17 @@ +import React from 'react' +export default () => { + return ( + +
+

+ This is the style page. + +

+
+
+ ) +} diff --git a/test/integration/basepath/pages/other-page.js b/test/integration/basepath/pages/other-page.js new file mode 100644 index 0000000000000..40e0e084fe120 --- /dev/null +++ b/test/integration/basepath/pages/other-page.js @@ -0,0 +1 @@ +export default () =>

Hello Other

diff --git a/test/integration/basepath/pages/ssr.js b/test/integration/basepath/pages/ssr.js new file mode 100644 index 0000000000000..b1ed3c0c75d31 --- /dev/null +++ b/test/integration/basepath/pages/ssr.js @@ -0,0 +1,11 @@ +function SSRPage({ test }) { + return

{test}

+} + +SSRPage.getInitialProps = () => { + return { + test: 'hello', + } +} + +export default SSRPage diff --git a/test/integration/basepath/test/index.test.js b/test/integration/basepath/test/index.test.js new file mode 100644 index 0000000000000..83ac336803350 --- /dev/null +++ b/test/integration/basepath/test/index.test.js @@ -0,0 +1,497 @@ +/* eslint-env jest */ +/* global jasmine */ +import webdriver from 'next-webdriver' +import { join } from 'path' +import { + nextServer, + launchApp, + findPort, + killApp, + nextBuild, + startApp, + stopApp, + waitFor, + check, + getBrowserBodyText, + renderViaHTTP, + File, + nextStart, +} from 'next-test-utils' +import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs' +import cheerio from 'cheerio' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5 + +describe('basePath development', () => { + let server + + let context = {} + + beforeAll(async () => { + context.appPort = await findPort() + server = await launchApp(join(__dirname, '..'), context.appPort) + }) + afterAll(async () => { + await killApp(server) + }) + + it('should show the hello page under the /docs prefix', async () => { + const browser = await webdriver(context.appPort, '/docs/hello') + try { + const text = await browser.elementByCss('h1').text() + expect(text).toBe('Hello World') + } finally { + await browser.close() + } + }) + + it('should show the other-page page under the /docs prefix', async () => { + const browser = await webdriver(context.appPort, '/docs/other-page') + try { + const text = await browser.elementByCss('h1').text() + expect(text).toBe('Hello Other') + } finally { + await browser.close() + } + }) + + it('should navigate to the page without refresh', async () => { + const browser = await webdriver(context.appPort, '/docs/hello') + try { + await browser.eval('window.itdidnotrefresh = "hello"') + const text = await browser + .elementByCss('#other-page-link') + .click() + .waitForElementByCss('#other-page-title') + .elementByCss('h1') + .text() + + expect(text).toBe('Hello Other') + expect(await browser.eval('window.itdidnotrefresh')).toBe('hello') + } finally { + await browser.close() + } + }) + + describe('Hot Module Reloading', () => { + describe('delete a page and add it back', () => { + it('should load the page properly', async () => { + const contactPagePath = join( + __dirname, + '../', + 'pages', + 'hmr', + 'contact.js' + ) + const newContactPagePath = join( + __dirname, + '../', + 'pages', + 'hmr', + '_contact.js' + ) + let browser + try { + browser = await webdriver(context.appPort, '/docs/hmr/contact') + const text = await browser.elementByCss('p').text() + expect(text).toBe('This is the contact page.') + + // Rename the file to mimic a deleted page + renameSync(contactPagePath, newContactPagePath) + + await check( + () => getBrowserBodyText(browser), + /This page could not be found/ + ) + + // Rename the file back to the original filename + renameSync(newContactPagePath, contactPagePath) + + // wait until the page comes back + await check( + () => getBrowserBodyText(browser), + /This is the contact page/ + ) + } finally { + if (browser) { + await browser.close() + } + if (existsSync(newContactPagePath)) { + renameSync(newContactPagePath, contactPagePath) + } + } + }) + }) + + describe('editing a page', () => { + it('should detect the changes and display it', async () => { + let browser + try { + browser = await webdriver(context.appPort, '/docs/hmr/about') + const text = await browser.elementByCss('p').text() + expect(text).toBe('This is the about page.') + + const aboutPagePath = join( + __dirname, + '../', + 'pages', + 'hmr', + 'about.js' + ) + + const originalContent = readFileSync(aboutPagePath, 'utf8') + const editedContent = originalContent.replace( + 'This is the about page', + 'COOL page' + ) + + // change the content + writeFileSync(aboutPagePath, editedContent, 'utf8') + + await check(() => getBrowserBodyText(browser), /COOL page/) + + // add the original content + writeFileSync(aboutPagePath, originalContent, 'utf8') + + await check( + () => getBrowserBodyText(browser), + /This is the about page/ + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should not reload unrelated pages', async () => { + let browser + try { + browser = await webdriver(context.appPort, '/docs/hmr/counter') + const text = await browser + .elementByCss('button') + .click() + .elementByCss('button') + .click() + .elementByCss('p') + .text() + expect(text).toBe('COUNT: 2') + + const aboutPagePath = join( + __dirname, + '../', + 'pages', + 'hmr', + 'about.js' + ) + + const originalContent = readFileSync(aboutPagePath, 'utf8') + const editedContent = originalContent.replace( + 'This is the about page', + 'COOL page' + ) + + // Change the about.js page + writeFileSync(aboutPagePath, editedContent, 'utf8') + + // wait for 5 seconds + await waitFor(5000) + + // Check whether the this page has reloaded or not. + const newText = await browser.elementByCss('p').text() + expect(newText).toBe('COUNT: 2') + + // restore the about page content. + writeFileSync(aboutPagePath, originalContent, 'utf8') + } finally { + if (browser) { + await browser.close() + } + } + }) + + // Added because of a regression in react-hot-loader, see issues: #4246 #4273 + // Also: https://github.com/zeit/styled-jsx/issues/425 + it('should update styles correctly', async () => { + let browser + try { + browser = await webdriver(context.appPort, '/docs/hmr/style') + const pTag = await browser.elementByCss('.hmr-style-page p') + const initialFontSize = await pTag.getComputedCss('font-size') + + expect(initialFontSize).toBe('100px') + + const pagePath = join(__dirname, '../', 'pages', 'hmr', 'style.js') + + const originalContent = readFileSync(pagePath, 'utf8') + const editedContent = originalContent.replace('100px', '200px') + + // Change the page + writeFileSync(pagePath, editedContent, 'utf8') + + try { + // Check whether the this page has reloaded or not. + await check(async () => { + const editedPTag = await browser.elementByCss('.hmr-style-page p') + return editedPTag.getComputedCss('font-size') + }, /200px/) + } finally { + // Finally is used so that we revert the content back to the original regardless of the test outcome + // restore the about page content. + writeFileSync(pagePath, originalContent, 'utf8') + } + } finally { + if (browser) { + await browser.close() + } + } + }) + + // Added because of a regression in react-hot-loader, see issues: #4246 #4273 + // Also: https://github.com/zeit/styled-jsx/issues/425 + it('should update styles in a stateful component correctly', async () => { + let browser + const pagePath = join( + __dirname, + '../', + 'pages', + 'hmr', + 'style-stateful-component.js' + ) + const originalContent = readFileSync(pagePath, 'utf8') + try { + browser = await webdriver( + context.appPort, + '/docs/hmr/style-stateful-component' + ) + const pTag = await browser.elementByCss('.hmr-style-page p') + const initialFontSize = await pTag.getComputedCss('font-size') + + expect(initialFontSize).toBe('100px') + const editedContent = originalContent.replace('100px', '200px') + + // Change the page + writeFileSync(pagePath, editedContent, 'utf8') + + // Check whether the this page has reloaded or not. + await check(async () => { + const editedPTag = await browser.elementByCss('.hmr-style-page p') + return editedPTag.getComputedCss('font-size') + }, /200px/) + } finally { + if (browser) { + await browser.close() + } + writeFileSync(pagePath, originalContent, 'utf8') + } + }) + + // Added because of a regression in react-hot-loader, see issues: #4246 #4273 + // Also: https://github.com/zeit/styled-jsx/issues/425 + it('should update styles in a dynamic component correctly', async () => { + let browser = null + let secondBrowser = null + const pagePath = join( + __dirname, + '../', + 'components', + 'hmr', + 'dynamic.js' + ) + const originalContent = readFileSync(pagePath, 'utf8') + try { + browser = await webdriver( + context.appPort, + '/docs/hmr/style-dynamic-component' + ) + const div = await browser.elementByCss('#dynamic-component') + const initialClientClassName = await div.getAttribute('class') + const initialFontSize = await div.getComputedCss('font-size') + + expect(initialFontSize).toBe('100px') + + const initialHtml = await renderViaHTTP( + context.appPort, + '/docs/hmr/style-dynamic-component' + ) + expect(initialHtml.includes('100px')).toBeTruthy() + + const $initialHtml = cheerio.load(initialHtml) + const initialServerClassName = $initialHtml( + '#dynamic-component' + ).attr('class') + + expect(initialClientClassName === initialServerClassName).toBeTruthy() + + const editedContent = originalContent.replace('100px', '200px') + + // Change the page + writeFileSync(pagePath, editedContent, 'utf8') + + // wait for 5 seconds + await waitFor(5000) + + secondBrowser = await webdriver( + context.appPort, + '/docs/hmr/style-dynamic-component' + ) + // Check whether the this page has reloaded or not. + const editedDiv = await secondBrowser.elementByCss( + '#dynamic-component' + ) + const editedClientClassName = await editedDiv.getAttribute('class') + const editedFontSize = await editedDiv.getComputedCss('font-size') + const browserHtml = await secondBrowser + .elementByCss('html') + .getAttribute('innerHTML') + + expect(editedFontSize).toBe('200px') + expect(browserHtml.includes('font-size:200px;')).toBe(true) + expect(browserHtml.includes('font-size:100px;')).toBe(false) + + const editedHtml = await renderViaHTTP( + context.appPort, + '/docs/hmr/style-dynamic-component' + ) + expect(editedHtml.includes('200px')).toBeTruthy() + const $editedHtml = cheerio.load(editedHtml) + const editedServerClassName = $editedHtml('#dynamic-component').attr( + 'class' + ) + + expect(editedClientClassName === editedServerClassName).toBe(true) + } finally { + // Finally is used so that we revert the content back to the original regardless of the test outcome + // restore the about page content. + writeFileSync(pagePath, originalContent, 'utf8') + + if (browser) { + await browser.close() + } + + if (secondBrowser) { + secondBrowser.close() + } + } + }) + }) + }) +}) + +describe('basePath production', () => { + const appDir = join(__dirname, '../') + let context = {} + let server + let app + + beforeAll(async () => { + await nextBuild(appDir) + app = nextServer({ + dir: join(__dirname, '../'), + dev: false, + quiet: true, + }) + + server = await startApp(app) + context.appPort = server.address().port + }) + + afterAll(() => stopApp(server)) + + it('should show the hello page under the /docs prefix', async () => { + const browser = await webdriver(context.appPort, '/docs/hello') + try { + const text = await browser.elementByCss('h1').text() + expect(text).toBe('Hello World') + } finally { + await browser.close() + } + }) + + it('should show the other-page page under the /docs prefix', async () => { + const browser = await webdriver(context.appPort, '/docs/other-page') + try { + const text = await browser.elementByCss('h1').text() + expect(text).toBe('Hello Other') + } finally { + await browser.close() + } + }) + + it('should navigate to the page without refresh', async () => { + const browser = await webdriver(context.appPort, '/docs/hello') + try { + await browser.eval('window.itdidnotrefresh = "hello"') + const text = await browser + .elementByCss('#other-page-link') + .click() + .waitForElementByCss('#other-page-title') + .elementByCss('h1') + .text() + + expect(text).toBe('Hello Other') + expect(await browser.eval('window.itdidnotrefresh')).toBe('hello') + } finally { + await browser.close() + } + }) +}) + +describe('basePath serverless', () => { + const appDir = join(__dirname, '../') + let context = {} + let app + + const nextConfig = new File(join(appDir, 'next.config.js')) + + beforeAll(async () => { + await nextConfig.write( + `module.exports = { target: 'serverless', experimental: { basePath: '/docs' } }` + ) + await nextBuild(appDir) + context.appPort = await findPort() + app = await nextStart(appDir, context.appPort) + }) + afterAll(async () => { + await killApp(app) + await nextConfig.restore() + }) + + it('should show the hello page under the /docs prefix', async () => { + const browser = await webdriver(context.appPort, '/docs/hello') + try { + const text = await browser.elementByCss('h1').text() + expect(text).toBe('Hello World') + } finally { + await browser.close() + } + }) + + it('should show the other-page page under the /docs prefix', async () => { + const browser = await webdriver(context.appPort, '/docs/other-page') + try { + const text = await browser.elementByCss('h1').text() + expect(text).toBe('Hello Other') + } finally { + await browser.close() + } + }) + + it('should navigate to the page without refresh', async () => { + const browser = await webdriver(context.appPort, '/docs/hello') + try { + await browser.eval('window.itdidnotrefresh = "hello"') + const text = await browser + .elementByCss('#other-page-link') + .click() + .waitForElementByCss('#other-page-title') + .elementByCss('h1') + .text() + + expect(text).toBe('Hello Other') + expect(await browser.eval('window.itdidnotrefresh')).toBe('hello') + } finally { + await browser.close() + } + }) +})