Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add automatic reloading when editing GS(S)P methods #16744

Merged
merged 20 commits into from Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bd17872
Add automatic reloading when editing GS(S)P methods
ijjk Sep 1, 2020
f44fa5e
Merge remote-tracking branch 'upstream/canary' into add/gsp-reloading
ijjk Sep 1, 2020
85aab1a
Merge branch 'canary' into add/gsp-reloading
ijjk Sep 1, 2020
86c6c8b
Export return type for GetStaticPaths (#16580)
arthurjdam Sep 1, 2020
2e0986f
[test] Update hydration marker for React 17 (#16756)
Timer Sep 1, 2020
f3c93e0
[EXAMPLE] with-framer-motion: fix broken images (#16714)
Sep 1, 2020
d3cf1ce
Revert #14580 (#16757)
Timer Sep 1, 2020
9c11d2b
v9.5.3-canary.27
Timer Sep 1, 2020
d723471
v9.5.3
Timer Sep 1, 2020
936d3f3
Add Fast Refresh Demo (#16576)
YichiZ Sep 1, 2020
5c8355e
force persistor persist again after persistStore bootstrap done (#16085)
weichienhung Sep 1, 2020
f7c40c7
Make the image post-processor ignore SVG images (#16732)
atcastle Sep 2, 2020
d5562f2
v9.5.4-canary.0
Timer Sep 2, 2020
1dd1f70
Only update lookups for dev overlay if mounted (#16776)
ijjk Sep 2, 2020
deb40ea
Create _document.js to include current language in HTML tag (#16360)
seosmmbusiness Sep 2, 2020
9df55a0
Update to only re-fetch data instead of reloading
ijjk Sep 2, 2020
f4031e3
Update to only re-fetch data instead of reloading
ijjk Sep 2, 2020
f642195
Merge remote-tracking branch 'upstream/canary' into add/gsp-reloading
ijjk Sep 2, 2020
f41f351
Merge remote-tracking branch 'upstream/canary' into add/gsp-reloading
ijjk Sep 2, 2020
498be1b
Merge branch 'add/gsp-reloading' of github.com:ijjk/next.js into add/…
ijjk Sep 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/next/build/utils.ts
Expand Up @@ -475,7 +475,7 @@ async function computeFromManifest(
return lastCompute!
}

function difference<T>(main: T[], sub: T[]): T[] {
export function difference<T>(main: T[] | Set<T>, sub: T[] | Set<T>): T[] {
const a = new Set(main)
const b = new Set(sub)
return [...a].filter((x) => !b.has(x))
Expand Down
7 changes: 7 additions & 0 deletions packages/next/client/next-dev.js
Expand Up @@ -42,6 +42,13 @@ initNext({ webpackHMR })
.catch((err) => {
console.log(`Failed to fetch devPagesManifest`, err)
})
} else if (event.data.indexOf('serverOnlyChanges') !== -1) {
const { pages } = JSON.parse(event.data)

if (pages.includes(window.next.router.pathname)) {
console.log('Reloading page due to server-side page change')
window.location.reload()
}
}
}
devPagesManifestListener.unfiltered = true
Expand Down
57 changes: 55 additions & 2 deletions packages/next/server/hot-reloader.ts
Expand Up @@ -31,6 +31,7 @@ import { isWriteable } from '../build/is-writeable'
import { ClientPagesLoaderOptions } from '../build/webpack/loaders/next-client-pages-loader'
import { stringify } from 'querystring'
import { Rewrite } from '../lib/load-custom-routes'
import { difference } from '../build/utils'

export async function renderScriptError(
res: ServerResponse,
Expand Down Expand Up @@ -356,6 +357,42 @@ export default class HotReloader {

watchCompilers(multiCompiler.compilers[0], multiCompiler.compilers[1])

// Watch for changes to client/server page files so we can tell when just
// the server file changes and trigger a reload for GS(S)P pages
const changedClientPages = new Set<string>()
const changedServerPages = new Set<string>()
const prevClientPageHashes = new Map<string, string>()
const prevServerPageHashes = new Map<string, string>()

const trackPageChanges = (
pageHashMap: Map<string, string>,
changedItems: Set<string>
) => (stats: webpack.compilation.Compilation) => {
stats.entrypoints.forEach((entry, key) => {
if (key.startsWith('pages/')) {
entry.chunks.forEach((chunk: any) => {
if (chunk.id === key) {
const prevHash = pageHashMap.get(key)

if (prevHash && prevHash !== chunk.hash) {
changedItems.add(key)
}
pageHashMap.set(key, chunk.hash)
}
})
}
})
}

multiCompiler.compilers[0].hooks.emit.tap(
'NextjsHotReloaderForClient',
trackPageChanges(prevClientPageHashes, changedClientPages)
)
multiCompiler.compilers[1].hooks.emit.tap(
'NextjsHotReloaderForServer',
trackPageChanges(prevServerPageHashes, changedServerPages)
)

// This plugin watches for changes to _document.js and notifies the client side that it should reload the page
multiCompiler.compilers[1].hooks.failed.tap(
'NextjsHotReloaderForServer',
Expand All @@ -370,6 +407,20 @@ export default class HotReloader {
this.serverError = null
this.serverStats = stats

const serverOnlyChanges = difference<string>(
changedServerPages,
changedClientPages
)
changedClientPages.clear()
changedServerPages.clear()

if (serverOnlyChanges.length > 0) {
this.send({
event: 'serverOnlyChanges',
pages: serverOnlyChanges.map((pg) => pg.substr('pages'.length)),
})
}

const { compilation } = stats

// We only watch `_document` for changes on the server compilation
Expand Down Expand Up @@ -514,8 +565,10 @@ export default class HotReloader {
return []
}

public send(action?: string, ...args: any[]): void {
this.webpackHotMiddleware!.publish({ action, data: args })
public send(action?: string | any, ...args: any[]): void {
this.webpackHotMiddleware!.publish(
action && typeof action === 'object' ? action : { action, data: args }
)
}

public async ensurePage(page: string) {
Expand Down
@@ -0,0 +1,36 @@
import { useRouter } from 'next/router'

export default function Gsp(props) {
if (useRouter().isFallback) {
return 'Loading...'
}

return (
<>
<p id="change">change me</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}

export const getStaticProps = ({ params }) => {
const count = 1

return {
props: {
count,
params,
random: Math.random(),
},
}
}

export const getStaticPaths = () => {
/* eslint-disable-next-line no-unused-vars */
const paths = 1

return {
paths: [{ params: { post: 'first' } }, { params: { post: 'second' } }],
fallback: true,
}
}
@@ -0,0 +1,20 @@
export default function Gssp(props) {
return (
<>
<p id="change">change me</p>
<p id="props">{JSON.stringify(props)}</p>
</>
)
}

export const getServerSideProps = ({ params }) => {
const count = 1

return {
props: {
count,
params,
random: Math.random(),
},
}
}
129 changes: 129 additions & 0 deletions test/integration/gssp-ssr-change-reloading/test/index.test.js
@@ -0,0 +1,129 @@
/* eslint-env jest */

import { join } from 'path'
import webdriver from 'next-webdriver'
import { killApp, findPort, launchApp, File, check } from 'next-test-utils'

jest.setTimeout(1000 * 60 * 2)
const appDir = join(__dirname, '..')

let appPort
let app

describe('GS(S)P Server-Side Change Reloading', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(() => killApp(app))

it('should not reload page when client-side is changed too GSP', async () => {
const browser = await webdriver(appPort, '/gsp-blog/first')
await browser.eval(() => (window.beforeChange = 'hi'))

const props = JSON.parse(await browser.elementByCss('#props').text())

const page = new File(join(appDir, 'pages/gsp-blog/[post].js'))
page.replace('change me', 'changed')

await check(() => browser.elementByCss('#change').text(), 'changed')
expect(await browser.eval(() => window.beforeChange)).toBe('hi')

const props2 = JSON.parse(await browser.elementByCss('#props').text())
expect(props).toEqual(props2)

page.restore()

await check(() => browser.elementByCss('#change').text(), 'change me')
})

it('should reload page when getStaticProps is changed only', async () => {
const browser = await webdriver(appPort, '/gsp-blog/first')
await browser.eval(() => (window.beforeChange = 'hi'))

const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)

const page = new File(join(appDir, 'pages/gsp-blog/[post].js'))
page.replace('count = 1', 'count = 2')

await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'2'
)
expect(await browser.eval(() => window.beforeChange)).toBe(null)
page.restore()

await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'1'
)
})

it('should reload page when getStaticPaths is changed only', async () => {
const browser = await webdriver(appPort, '/gsp-blog/first')
await browser.eval(() => (window.beforeChange = 'hi'))

const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)

const page = new File(join(appDir, 'pages/gsp-blog/[post].js'))
page.replace('paths = 1', 'paths = 2')

await check(
async () =>
(await browser.eval(() => window.beforeChange)) === null
? 'pass'
: 'fail',
'pass'
)
page.restore()
})

it('should not reload page when client-side is changed too GSSP', async () => {
const browser = await webdriver(appPort, '/gssp-blog/first')
await browser.eval(() => (window.beforeChange = 'hi'))

const props = JSON.parse(await browser.elementByCss('#props').text())

const page = new File(join(appDir, 'pages/gssp-blog/[post].js'))
page.replace('change me', 'changed')

await check(() => browser.elementByCss('#change').text(), 'changed')
expect(await browser.eval(() => window.beforeChange)).toBe('hi')

const props2 = JSON.parse(await browser.elementByCss('#props').text())
expect(props).toEqual(props2)

page.restore()

await check(() => browser.elementByCss('#change').text(), 'change me')
})

it('should reload page when getServerSideProps is changed only', async () => {
const browser = await webdriver(appPort, '/gssp-blog/first')
await browser.eval(() => (window.beforeChange = 'hi'))

const props = JSON.parse(await browser.elementByCss('#props').text())
expect(props.count).toBe(1)

const page = new File(join(appDir, 'pages/gssp-blog/[post].js'))
page.replace('count = 1', 'count = 2')

await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'2'
)
expect(await browser.eval(() => window.beforeChange)).toBe(null)
page.restore()

await check(
async () =>
JSON.parse(await browser.elementByCss('#props').text()).count + '',
'1'
)
})
})