Use chunkhash instead of buildId for pages (#13937)

Co-authored-by: JJ Kasper <jj@jjsweb.site>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Tim Neutkens 2020-06-11 10:57:24 +02:00 committed by GitHub
parent 1ffc7af36a
commit 76fddcd7ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 306 additions and 163 deletions

View file

@ -100,7 +100,7 @@ export function createEntrypoints(
const bundleFile = `${normalizePagePath(page)}.js`
const isApiRoute = page.match(API_ROUTE)
const bundlePath = join('static', buildId, 'pages', bundleFile)
const bundlePath = join('static', 'BUILD_ID', 'pages', bundleFile)
const isLikeServerless = isTargetLikeServerless(target)

View file

@ -499,7 +499,6 @@ export default async function build(dir: string, conf = null): Promise<void> {
const [selfSize, allSize] = await getJsPageSizeInKb(
actualPage,
distDir,
buildId,
buildManifest,
config.experimental.modern
)
@ -989,8 +988,6 @@ export default async function build(dir: string, conf = null): Promise<void> {
JSON.stringify(prerenderManifest),
'utf8'
)
// No need to call this fn as we already emitted a default SSG manifest:
// await generateClientSsgManifest(prerenderManifest, { distDir, buildId })
}
await promises.writeFile(

View file

@ -22,6 +22,7 @@ import { getRouteMatcher, getRouteRegex } from '../next-server/lib/router/utils'
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
import { findPageFile } from '../server/lib/find-page-file'
import { GetStaticPaths } from 'next/types'
import { denormalizePagePath } from '../next-server/server/normalize-page-path'
const fileGzipStats: { [k: string]: Promise<number> } = {}
const fsStatGzip = (file: string) => {
@ -113,7 +114,6 @@ export async function printTreeView(
const sizeData = await computeFromManifest(
buildManifest,
distPath,
buildId,
isModern,
pageInfos
)
@ -366,7 +366,6 @@ let lastComputePageInfo: boolean | undefined
async function computeFromManifest(
manifest: BuildManifestShape,
distPath: string,
buildId: string,
isModern: boolean,
pageInfos?: Map<string, PageInfo>
): Promise<ComputeManifestShape> {
@ -408,15 +407,6 @@ async function computeFromManifest(
})
})
// Add well-known shared file
files.set(
path.posix.join(
`static/${buildId}/pages/`,
`/_app${isModern ? '.module' : ''}.js`
),
Infinity
)
const commonFiles = [...files.entries()]
.filter(([, len]) => len === expected || len === Infinity)
.map(([f]) => f)
@ -490,21 +480,17 @@ function sum(a: number[]): number {
export async function getJsPageSizeInKb(
page: string,
distPath: string,
buildId: string,
buildManifest: BuildManifestShape,
isModern: boolean
): Promise<[number, number]> {
const data = await computeFromManifest(
buildManifest,
distPath,
buildId,
isModern
)
const data = await computeFromManifest(buildManifest, distPath, isModern)
const fnFilterModern = (entry: string) =>
entry.endsWith('.js') && entry.endsWith('.module.js') === isModern
const pageFiles = (buildManifest.pages[page] || []).filter(fnFilterModern)
const pageFiles = (
buildManifest.pages[denormalizePagePath(page)] || []
).filter(fnFilterModern)
const appFiles = (buildManifest.pages['/_app'] || []).filter(fnFilterModern)
const fnMapRealPath = (dep: string) => `${distPath}/${dep}`
@ -517,27 +503,12 @@ export async function getJsPageSizeInKb(
data.commonFiles
).map(fnMapRealPath)
const clientBundle = path.join(
distPath,
`static/${buildId}/pages/`,
`${page}${isModern ? '.module' : ''}.js`
)
const appBundle = path.join(
distPath,
`static/${buildId}/pages/`,
`/_app${isModern ? '.module' : ''}.js`
)
selfFilesReal.push(clientBundle)
allFilesReal.push(clientBundle)
if (clientBundle !== appBundle) {
allFilesReal.push(appBundle)
}
try {
// Doesn't use `Promise.all`, as we'd double compute duplicate files. This
// function is memoized, so the second one will instantly resolve.
const allFilesSize = sum(await Promise.all(allFilesReal.map(fsStatGzip)))
const selfFilesSize = sum(await Promise.all(selfFilesReal.map(fsStatGzip)))
return [selfFilesSize, allFilesSize]
} catch (_) {}
return [-1, -1]

View file

@ -687,6 +687,14 @@ export default async function getBaseWebpackConfig(
) {
return chunk.name.replace(/\.js$/, '-[contenthash].js')
}
if (chunk.name.includes('BUILD_ID')) {
return escapePathVariables(chunk.name).replace(
'BUILD_ID',
isServer || dev ? buildId : '[contenthash]'
)
}
return '[name]'
},
libraryTarget: isServer ? 'commonjs2' : 'var',

View file

@ -38,6 +38,10 @@ function generateClientManifest(
return devalue(clientManifest)
}
function isJsFile(file: string): boolean {
return file.endsWith('.js')
}
// This plugin creates a build-manifest.json for all assets that are being output
// It has a mapping of "entry" filename to real filename. Because the real filename can be hashed in production
export default class BuildManifestPlugin {
@ -65,21 +69,19 @@ export default class BuildManifestPlugin {
(c) => c.name === CLIENT_STATIC_FILES_RUNTIME_MAIN
)
const mainJsFiles: string[] =
mainJsChunk?.files.filter((file: string) => file.endsWith('.js')) ??
[]
const mainJsFiles: string[] = mainJsChunk?.files.filter(isJsFile) ?? []
const polyfillChunk = chunks.find(
(c) => c.name === CLIENT_STATIC_FILES_RUNTIME_POLYFILLS
)
// Create a separate entry for polyfills
assetMap.polyfillFiles = polyfillChunk?.files ?? []
assetMap.polyfillFiles = polyfillChunk?.files.filter(isJsFile) ?? []
const reactRefreshChunk = chunks.find(
(c) => c.name === CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH
)
assetMap.devFiles = reactRefreshChunk?.files ?? []
assetMap.devFiles = reactRefreshChunk?.files.filter(isJsFile) ?? []
for (const entrypoint of compilation.entrypoints.values()) {
const pagePath = getRouteFromEntrypoint(entrypoint.name)
@ -92,7 +94,7 @@ export default class BuildManifestPlugin {
// getFiles() - helper function to read the files for an entrypoint from stats object
for (const file of entrypoint.getFiles()) {
if (!(file.endsWith('.js') || file.endsWith('.css'))) {
if (!(isJsFile(file) || file.endsWith('.css'))) {
continue
}

View file

@ -17,18 +17,33 @@ export default class PagesManifestPlugin implements Plugin {
apply(compiler: Compiler): void {
compiler.hooks.emit.tap('NextJsPagesManifest', (compilation) => {
const { chunks } = compilation
const entrypoints = compilation.entrypoints
const pages: PagesManifest = {}
for (const chunk of chunks) {
const pagePath = getRouteFromEntrypoint(chunk.name, this.serverless)
for (const entrypoint of entrypoints.values()) {
const pagePath = getRouteFromEntrypoint(
entrypoint.name,
this.serverless
)
if (!pagePath) {
continue
}
const files = entrypoint
.getFiles()
.filter((file: string) => file.endsWith('.js'))
if (files.length > 1) {
console.log(
`Found more than one file in server entrypoint ${entrypoint.name}`,
files
)
continue
}
// Write filename, replace any backslashes in path (on windows) with forwardslashes for cross-platform consistency.
pages[pagePath] = chunk.name.replace(/\\/g, '/')
pages[pagePath] = files[0].replace(/\\/g, '/')
}
compilation.assets[PAGES_MANIFEST] = new RawSource(

View file

@ -358,10 +358,6 @@ export default class Server {
type: 'route',
name: '_next/static catchall',
fn: async (req, res, params, parsedUrl) => {
// The commons folder holds commonschunk files
// The chunks folder holds dynamic entries
// The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached.
// make sure to 404 for /_next/static itself
if (!params.path) {
await this.render404(req, res, parsedUrl)
@ -375,7 +371,8 @@ export default class Server {
params.path[0] === 'chunks' ||
params.path[0] === 'css' ||
params.path[0] === 'media' ||
params.path[0] === this.buildId
params.path[0] === this.buildId ||
params.path[1] === 'pages'
) {
this.setImmutableAssetCacheControl(res)
}

View file

@ -383,8 +383,7 @@ export default class HotReloader {
// We only watch `_document` for changes on the server compilation
// the rest of the files will be triggered by the client compilation
const documentChunk = compilation.chunks.find(
(c) =>
c.name === normalize(`static/${this.buildId}/pages/_document.js`)
(c) => c.name === normalize(`static/BUILD_ID/pages/_document.js`)
)
// If the document chunk can't be found we do nothing
if (!documentChunk) {
@ -488,7 +487,6 @@ export default class HotReloader {
webpackDevMiddleware,
multiCompiler,
{
buildId: this.buildId,
pagesDir: this.pagesDir,
pageExtensions: this.config.pageExtensions,
...(this.config.onDemandEntries as {

View file

@ -42,13 +42,11 @@ export default function onDemandEntryHandler(
devMiddleware: WebpackDevMiddleware.WebpackDevMiddleware,
multiCompiler: webpack.MultiCompiler,
{
buildId,
pagesDir,
pageExtensions,
maxInactiveAge,
pagesBufferLength,
}: {
buildId: string
pagesDir: string
pageExtensions: string[]
maxInactiveAge: number
@ -212,7 +210,7 @@ export default function onDemandEntryHandler(
pageUrl = pageUrl === '' ? '/' : pageUrl
const bundleFile = `${normalizePagePath(pageUrl)}.js`
const name = join('static', buildId, 'pages', bundleFile)
const name = join('static', 'BUILD_ID', 'pages', bundleFile)
const absolutePagePath = pagePath.startsWith('next/dist/pages')
? require.resolve(pagePath)
: join(pagesDir, pagePath)

View file

@ -12,6 +12,7 @@ const fixturesDir = join(__dirname, '..', 'fixtures')
describe('Build Output', () => {
describe('Basic Application Output', () => {
let stdout
const appDir = join(fixturesDir, 'basic-app')
beforeAll(async () => {
@ -19,9 +20,9 @@ describe('Build Output', () => {
})
it('should not include internal pages', async () => {
const { stdout } = await nextBuild(appDir, [], {
;({ stdout } = await nextBuild(appDir, [], {
stdout: true,
})
}))
expect(stdout).toMatch(/\/ [ ]* \d{1,} B/)
expect(stdout).toMatch(/\+ First Load JS shared by all [ 0-9.]* kB/)
@ -36,6 +37,82 @@ describe('Build Output', () => {
expect(stdout).toContain('○ /')
})
it('should not deviate from snapshot', async () => {
console.log(stdout)
const parsePageSize = (page) =>
stdout.match(
new RegExp(` ${page} .*?((?:\\d|\\.){1,} (?:\\w{1,})) `)
)[1]
const parsePageFirstLoad = (page) =>
stdout.match(
new RegExp(
` ${page} .*?(?:(?:\\d|\\.){1,}) .*? ((?:\\d|\\.){1,} (?:\\w{1,}))`
)
)[1]
const parseSharedSize = (sharedPartName) =>
stdout.match(
new RegExp(`${sharedPartName} .*? ((?:\\d|\\.){1,} (?:\\w{1,}))`)
)[1]
const indexSize = parsePageSize('/')
const indexFirstLoad = parsePageFirstLoad('/')
const err404Size = parsePageSize('/404')
const err404FirstLoad = parsePageFirstLoad('/404')
const sharedByAll = parseSharedSize('shared by all')
const _appSize = parseSharedSize('_app\\.js')
const webpackSize = parseSharedSize('webpack\\..*?\\.js')
const mainSize = parseSharedSize('main\\..*?\\.js')
const frameworkSize = parseSharedSize('framework\\..*?\\.js')
for (const size of [
indexSize,
indexFirstLoad,
err404Size,
err404FirstLoad,
sharedByAll,
_appSize,
webpackSize,
mainSize,
frameworkSize,
]) {
expect(parseFloat(size)).toBeGreaterThan(0)
}
// should be no bigger than 265 bytes
expect(parseFloat(indexSize) - 265).toBeLessThanOrEqual(0)
expect(indexSize.endsWith('B')).toBe(true)
// should be no bigger than 62 kb
expect(parseFloat(indexFirstLoad) - 61).toBeLessThanOrEqual(0)
expect(indexFirstLoad.endsWith('kB')).toBe(true)
expect(parseFloat(err404Size) - 3.4).toBeLessThanOrEqual(0)
expect(err404Size.endsWith('kB')).toBe(true)
expect(parseFloat(err404FirstLoad) - 64).toBeLessThanOrEqual(0)
expect(err404FirstLoad.endsWith('kB')).toBe(true)
expect(parseFloat(sharedByAll) - 61).toBeLessThanOrEqual(0)
expect(sharedByAll.endsWith('kB')).toBe(true)
expect(parseFloat(_appSize) - 1000).toBeLessThanOrEqual(0)
expect(_appSize.endsWith('B')).toBe(true)
expect(parseFloat(webpackSize) - 775).toBeLessThanOrEqual(0)
expect(webpackSize.endsWith('B')).toBe(true)
expect(parseFloat(mainSize) - 6.3).toBeLessThanOrEqual(0)
expect(mainSize.endsWith('kB')).toBe(true)
expect(parseFloat(frameworkSize) - 41).toBeLessThanOrEqual(0)
expect(frameworkSize.endsWith('kB')).toBe(true)
})
it('should not emit extracted comments', async () => {
const files = await recursiveReadDir(
join(appDir, '.next'),

View file

@ -3,6 +3,7 @@
import cheerio from 'cheerio'
import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST } from 'next/constants'
import { join } from 'path'
import url from 'url'
export default function (render, fetch) {
async function get$(path, query) {
@ -17,6 +18,18 @@ export default function (render, fetch) {
expect(html.includes('My component!')).toBeTruthy()
})
it('should should not contain scripts that are not js', async () => {
const $ = await get$('/')
$('script[src]').each((_index, element) => {
const parsedUrl = url.parse($(element).attr('src'))
if (!parsedUrl.pathname.endsWith('.js')) {
throw new Error(
`Page includes script that is not a javascript file ${parsedUrl.pathname}`
)
}
})
})
it('should handle undefined prop in head server-side', async () => {
const html = await render('/head')
const $ = cheerio.load(html)
@ -293,8 +306,6 @@ export default function (render, fetch) {
})
it('should set Cache-Control header', async () => {
const buildId = 'development'
// build dynamic page
await fetch('/dynamic/ssr')
@ -305,9 +316,6 @@ export default function (render, fetch) {
))
const resources = []
// test a regular page
resources.push(`/_next/static/${buildId}/pages/index.js`)
// test dynamic chunk
resources.push(
'/_next/' + reactLoadableManifest['../../components/hello1'][0].file

View file

@ -20,16 +20,13 @@ let app
const runTests = () => {
it('should rewrite to /_next/static correctly', async () => {
// ensure the bundle is built
await renderViaHTTP(appPort, '/hello')
const bundlePath = await join(
'/docs/_next/static/',
buildId,
'pages/hello.js'
'_buildManifest.js'
)
const data = await renderViaHTTP(appPort, bundlePath)
expect(data).toContain('hello from hello.js')
expect(data).toContain('/hello')
})
it('should rewrite and render page correctly', async () => {

View file

@ -303,10 +303,9 @@ const runTests = (isDev = false) => {
await renderViaHTTP(appPort, '/hello')
const data = await renderViaHTTP(
appPort,
`/hidden/_next/static/${buildId}/pages/hello.js`
`/hidden/_next/static/${buildId}/_buildManifest.js`
)
expect(data).toContain('Hello')
expect(data).toContain('createElement')
expect(data).toContain('/hello')
})
it('should allow redirecting to external resource', async () => {

View file

@ -499,15 +499,13 @@ function runTests(dev) {
})
} else {
it('should output modern bundles with dynamic route correctly', async () => {
const bundlePath = join(
appDir,
'.next/static/',
buildId,
'pages/blog/[name]/comment/[id]'
)
const buildManifest = require(join('../.next', 'build-manifest.json'))
await fs.access(bundlePath + '.js', fs.constants.F_OK)
await fs.access(bundlePath + '.module.js', fs.constants.F_OK)
const files = buildManifest.pages[
'/blog/[name]/comment/[id]'
].filter((filename) => filename.includes('/blog/[name]/comment/[id]'))
expect(files.length).toBe(2)
})
it('should output a routes-manifest correctly', async () => {

View file

@ -18,7 +18,6 @@ jest.setTimeout(1000 * 60 * 2)
let app
let appPort
let buildId
const appDir = join(__dirname, '../app')
const getEnvFromHtml = async (path) => {
@ -72,10 +71,19 @@ const runTests = (mode = 'dev') => {
// make sure to build page
await renderViaHTTP(appPort, '/global')
const buildManifest = require(join(
__dirname,
'../app/.next/build-manifest.json'
))
const pageFile = buildManifest.pages['/global'].find((filename) =>
filename.includes('pages/global')
)
// read client bundle contents since a server side render can
// have the value available during render but it not be injected
const bundleContent = await fs.readFile(
join(appDir, '.next/static', buildId, 'pages/global.js'),
join(appDir, '.next', pageFile),
'utf8'
)
expect(bundleContent).toContain('another')
@ -128,7 +136,6 @@ describe('Env Config', () => {
PROCESS_ENV_KEY: 'processenvironment',
},
})
buildId = 'development'
})
afterAll(() => killApp(app))
@ -144,7 +151,6 @@ describe('Env Config', () => {
NODE_ENV: 'test',
},
})
buildId = 'development'
})
afterAll(() => killApp(app))
@ -207,7 +213,6 @@ describe('Env Config', () => {
}
app = await nextStart(appDir, appPort)
buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
})
afterAll(async () => {
for (const file of envFiles) {

View file

@ -3,7 +3,14 @@
import { join } from 'path'
import fs from 'fs-extra'
import webdriver from 'next-webdriver'
import { nextBuild, nextStart, findPort, killApp, check } from 'next-test-utils'
import {
nextBuild,
nextStart,
findPort,
killApp,
check,
getPageFileFromBuildManifest,
} from 'next-test-utils'
jest.setTimeout(1000 * 60 * 1)
const appDir = join(__dirname, '..')
@ -14,7 +21,6 @@ describe('Failing to load _error', () => {
it('handles failing to load _error correctly', async () => {
await nextBuild(appDir)
const buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8')
const appPort = await findPort()
app = await nextStart(appDir, appPort)
@ -24,8 +30,9 @@ describe('Failing to load _error', () => {
await browser.elementByCss('#to-broken').moveTo()
await browser.waitForElementByCss('script[src*="broken.js"')
const errorPageFilePath = getPageFileFromBuildManifest(appDir, '/_error')
// remove _error client bundle so that it can't be loaded
await fs.remove(join(appDir, '.next/static/', buildId, 'pages/_error.js'))
await fs.remove(join(appDir, '.next', errorPageFilePath))
await browser.elementByCss('#to-broken').click()

View file

@ -1,17 +1,23 @@
/* eslint-env jest */
import { join } from 'path'
import { readFileSync, readdirSync } from 'fs'
import rimraf from 'rimraf'
import { promisify } from 'util'
import { nextServer, runNextCommand, startApp, stopApp } from 'next-test-utils'
import {
renderViaHTTP,
nextServer,
runNextCommand,
startApp,
stopApp,
} from 'next-test-utils'
import cheerio from 'cheerio'
jest.setTimeout(1000 * 60 * 5)
const rimrafPromise = promisify(rimraf)
let appDir = join(__dirname, '..')
let server
// let appPort
let appPort
describe('Modern Mode', () => {
beforeAll(async () => {
@ -31,31 +37,29 @@ describe('Modern Mode', () => {
})
server = await startApp(app)
// appPort = server.address().port
appPort = server.address().port
})
afterAll(async () => {
stopApp(server)
rimrafPromise(join(appDir, '.next'))
})
it('should generate client side modern and legacy build files', async () => {
const buildId = readFileSync(join(appDir, '.next/BUILD_ID'), 'utf8')
const html = await renderViaHTTP(appPort, '/')
const $ = cheerio.load(html)
const expectedFiles = ['index', '_app', '_error', 'main', 'webpack']
const buildFiles = [
...readdirSync(join(appDir, '.next/static', buildId, 'pages')),
...readdirSync(join(appDir, '.next/static/runtime')).map(
(file) => file.replace(/-\w+\./, '.') // remove hash
),
...readdirSync(join(appDir, '.next/static/chunks')).map(
(file) => file.replace(/\.\w+\./, '.') // remove hash
),
]
const moduleScripts = $('script[src][type=module]').toArray()
const nomoduleScripts = $('script[src][nomodule]').toArray()
console.log(`Client files: ${buildFiles.join(', ')}`)
const moduleIndex = moduleScripts.find((script) =>
script.attribs.src.includes('pages/index')
)
expectedFiles.forEach((file) => {
expect(buildFiles).toContain(`${file}.js`)
expect(buildFiles).toContain(`${file}.module.js`)
})
expect(moduleIndex).toBeDefined()
const nomoduleIndex = nomoduleScripts.find((script) =>
script.attribs.src.includes('pages/index')
)
expect(nomoduleIndex).toBeDefined()
})
})

View file

@ -151,29 +151,37 @@ describe('Production Usage', () => {
})
it('should return 412 on static file when If-Unmodified-Since is provided and file is modified', async () => {
const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8')
const buildManifest = require(join(
__dirname,
'../.next/build-manifest.json'
))
const res = await fetch(
`http://localhost:${appPort}/_next/static/${buildId}/pages/index.js`,
{
const files = buildManifest.pages['/']
for (const file of files) {
const res = await fetch(`http://localhost:${appPort}/_next/${file}`, {
method: 'GET',
headers: { 'if-unmodified-since': 'Fri, 12 Jul 2019 20:00:13 GMT' },
}
)
expect(res.status).toBe(412)
})
expect(res.status).toBe(412)
}
})
it('should return 200 on static file if If-Unmodified-Since is invalid date', async () => {
const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8')
const buildManifest = require(join(
__dirname,
'../.next/build-manifest.json'
))
const res = await fetch(
`http://localhost:${appPort}/_next/static/${buildId}/pages/index.js`,
{
const files = buildManifest.pages['/']
for (const file of files) {
const res = await fetch(`http://localhost:${appPort}/_next/${file}`, {
method: 'GET',
headers: { 'if-unmodified-since': 'nextjs' },
}
)
expect(res.status).toBe(200)
})
expect(res.status).toBe(200)
}
})
it('should set Content-Length header', async () => {
@ -183,7 +191,6 @@ describe('Production Usage', () => {
})
it('should set Cache-Control header', async () => {
const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8')
const buildManifest = require(join('../.next', BUILD_MANIFEST))
const reactLoadableManifest = require(join(
'../.next',
@ -193,9 +200,6 @@ describe('Production Usage', () => {
const resources = new Set()
// test a regular page
resources.add(`${url}static/${buildId}/pages/index.js`)
// test dynamic chunk
resources.add(
url + reactLoadableManifest['../../components/hello1'][0].file

View file

@ -1,10 +1,12 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
import { promises } from 'fs'
import { join } from 'path'
import {
readNextBuildClientPageFile,
readNextBuildServerPageFile,
} from 'next-test-utils'
const readNextBuildFile = (relativePath) =>
promises.readFile(join(__dirname, '../.next', relativePath), 'utf8')
const appDir = join(__dirname, '..')
export default (context) => {
describe('process.env', () => {
@ -18,9 +20,9 @@ export default (context) => {
describe('process.browser', () => {
it('should eliminate server only code on the client', async () => {
const buildId = await readNextBuildFile('./BUILD_ID')
const clientCode = await readNextBuildFile(
`./static/${buildId}/pages/process-env.js`
const clientCode = await readNextBuildClientPageFile(
appDir,
'/process-env'
)
expect(clientCode).toMatch(
/__THIS_SHOULD_ONLY_BE_DEFINED_IN_BROWSER_CONTEXT__/
@ -31,9 +33,9 @@ export default (context) => {
})
it('should eliminate client only code on the server', async () => {
const buildId = await readNextBuildFile('./BUILD_ID')
const serverCode = await readNextBuildFile(
`./server/static/${buildId}/pages/process-env.js`
const serverCode = await readNextBuildServerPageFile(
appDir,
'/process-env'
)
expect(serverCode).not.toMatch(
/__THIS_SHOULD_ONLY_BE_DEFINED_IN_BROWSER_CONTEXT__/

View file

@ -2,7 +2,7 @@
import webdriver from 'next-webdriver'
import { join } from 'path'
import { existsSync, readdirSync, readFileSync } from 'fs'
import { existsSync, readdirSync } from 'fs'
import {
killApp,
findPort,
@ -10,13 +10,13 @@ import {
nextStart,
fetchViaHTTP,
renderViaHTTP,
readNextBuildClientPageFile,
} from 'next-test-utils'
import fetch from 'node-fetch'
const appDir = join(__dirname, '../')
const serverlessDir = join(appDir, '.next/serverless/pages')
const chunksDir = join(appDir, '.next/static/chunks')
const buildIdFile = join(appDir, '.next/BUILD_ID')
let appPort
let app
jest.setTimeout(1000 * 60 * 5)
@ -107,15 +107,8 @@ describe('Serverless Trace', () => {
it('should not have combined client-side chunks', () => {
expect(readdirSync(chunksDir).length).toBeGreaterThanOrEqual(2)
const buildId = readFileSync(buildIdFile, 'utf8').trim()
const pageContent = join(
appDir,
'.next/static',
buildId,
'pages/dynamic.js'
)
expect(readFileSync(pageContent, 'utf8')).not.toContain('Hello!')
const contents = readNextBuildClientPageFile(appDir, '/dynamic')
expect(contents).not.toContain('Hello!')
})
it('should not output _app.js and _document.js to serverless build', () => {

View file

@ -12,6 +12,7 @@ import {
nextStart,
fetchViaHTTP,
renderViaHTTP,
getPageFileFromBuildManifest,
} from 'next-test-utils'
import qs from 'querystring'
import path from 'path'
@ -20,7 +21,6 @@ import fetch from 'node-fetch'
const appDir = join(__dirname, '../')
const serverlessDir = join(appDir, '.next/serverless/pages')
const chunksDir = join(appDir, '.next/static/chunks')
const buildIdFile = join(appDir, '.next/BUILD_ID')
let stderr = ''
let appPort
let app
@ -172,15 +172,12 @@ describe('Serverless', () => {
it('should not have combined client-side chunks', () => {
expect(readdirSync(chunksDir).length).toBeGreaterThanOrEqual(2)
const buildId = readFileSync(buildIdFile, 'utf8').trim()
const pageContent = join(
appDir,
'.next/static',
buildId,
'pages/dynamic.js'
)
expect(readFileSync(pageContent, 'utf8')).not.toContain('Hello!')
const pageFile = getPageFileFromBuildManifest(appDir, '/')
expect(
readFileSync(join(__dirname, '..', '.next', pageFile), 'utf8')
).not.toContain('Hello!')
})
it('should not output _app.js and _document.js to serverless build', () => {

View file

@ -7,33 +7,52 @@ import { nextBuild } from 'next-test-utils'
jest.setTimeout(1000 * 60 * 1)
const appDir = path.join(__dirname, '../app')
let buildId
let buildManifest
let pagesManifest
describe('typeof window replace', () => {
beforeAll(async () => {
await nextBuild(appDir)
buildId = await fs.readFile(path.join(appDir, '.next/BUILD_ID'), 'utf8')
buildManifest = require(path.join(
appDir,
'.next/build-manifest.json'
), 'utf8')
pagesManifest = require(path.join(
appDir,
'.next/server/pages-manifest.json'
), 'utf8')
})
it('Replaces `typeof window` with object for client code', async () => {
const pageFile = buildManifest.pages['/'].find(
(file) => file.endsWith('.js') && file.includes('pages/index')
)
const content = await fs.readFile(
path.join(appDir, '.next/static/', buildId, 'pages/index.js'),
path.join(appDir, '.next', pageFile),
'utf8'
)
expect(content).toMatch(/Hello.*?,.*?("|')object("|')/)
})
it('Replaces `typeof window` with undefined for server code', async () => {
const pageFile = pagesManifest['/']
const content = await fs.readFile(
path.join(appDir, '.next/server/static', buildId, 'pages/index.js'),
path.join(appDir, '.next', 'server', pageFile),
'utf8'
)
expect(content).toMatch(/Hello.*?,.*?("|')undefined("|')/)
})
it('Does not replace `typeof window` for `node_modules` code', async () => {
const pageFile = buildManifest.pages['/'].find(
(file) => file.endsWith('.js') && file.includes('pages/index')
)
const content = await fs.readFile(
path.join(appDir, '.next/static/', buildId, 'pages/index.js'),
path.join(appDir, '.next', pageFile),
'utf8'
)
expect(content).toMatch(/MyComp:.*?,.*?typeof window/)

View file

@ -471,3 +471,50 @@ export function getBrowserBodyText(browser) {
export function normalizeRegEx(src) {
return new RegExp(src).source.replace(/\^\//g, '^\\/')
}
export function getBuildManifest(dir) {
return require(path.join(dir, '.next/build-manifest.json'))
}
export function getPageFileFromBuildManifest(dir, page) {
const buildManifest = getBuildManifest(dir)
const pageFiles = buildManifest.pages[page]
if (!pageFiles) {
throw new Error(`No files for page ${page}`)
}
const pageFile = pageFiles.find(
(file) =>
file.endsWith('.js') &&
file.includes(`pages${page === '' ? '/index' : page}`)
)
if (!pageFile) {
throw new Error(`No page file for page ${page}`)
}
return pageFile
}
export function readNextBuildClientPageFile(appDir, page) {
const pageFile = getPageFileFromBuildManifest(appDir, page)
return readFileSync(path.join(appDir, '.next', pageFile), 'utf8')
}
export function getPagesManifest(dir) {
return require(path.join(dir, '.next/server/pages-manifest.json'))
}
export function getPageFileFromPagesManifest(dir, page) {
const pagesManifest = getPagesManifest(dir)
const pageFile = pagesManifest[page]
if (!pageFile) {
throw new Error(`No file for page ${page}`)
}
return pageFile
}
export function readNextBuildServerPageFile(appDir, page) {
const pageFile = getPageFileFromPagesManifest(appDir, page)
return readFileSync(path.join(appDir, '.next', 'server', pageFile), 'utf8')
}