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:
parent
1ffc7af36a
commit
76fddcd7ef
23 changed files with 306 additions and 163 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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__/
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue