From 76fddcd7efd930ca1fcef9573c83719f47233d3e Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Thu, 11 Jun 2020 10:57:24 +0200 Subject: [PATCH] Use chunkhash instead of buildId for pages (#13937) Co-authored-by: JJ Kasper Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/next/build/entries.ts | 2 +- packages/next/build/index.ts | 3 - packages/next/build/utils.ts | 41 ++-------- packages/next/build/webpack-config.ts | 8 ++ .../webpack/plugins/build-manifest-plugin.ts | 14 ++-- .../webpack/plugins/pages-manifest-plugin.ts | 23 +++++- .../next/next-server/server/next-server.ts | 7 +- packages/next/server/hot-reloader.ts | 4 +- .../next/server/on-demand-entry-handler.ts | 4 +- .../build-output/test/index.test.js | 81 ++++++++++++++++++- .../client-navigation/test/rendering.js | 18 +++-- .../custom-routes-catchall/test/index.test.js | 7 +- .../custom-routes/test/index.test.js | 5 +- .../dynamic-routing/test/index.test.js | 14 ++-- .../integration/env-config/test/index.test.js | 15 ++-- .../error-load-fail/test/index.test.js | 13 ++- .../modern-mode/test/index.test.js | 44 +++++----- .../integration/production/test/index.test.js | 40 ++++----- .../production/test/process-env.js | 20 ++--- .../serverless-trace/test/index.test.js | 15 +--- .../integration/serverless/test/index.test.js | 15 ++-- .../typeof-window-replace/test/index.test.js | 29 +++++-- test/lib/next-test-utils.js | 47 +++++++++++ 23 files changed, 306 insertions(+), 163 deletions(-) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 97efab0d08..43bd89fd55 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -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) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 12a57efbeb..a584ac3f80 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -499,7 +499,6 @@ export default async function build(dir: string, conf = null): Promise { 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 { 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( diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 0ed91ba49e..bf53ba32f7 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -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 } = {} 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 ): Promise { @@ -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] diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 6b97311ef1..8a061c6650 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -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', diff --git a/packages/next/build/webpack/plugins/build-manifest-plugin.ts b/packages/next/build/webpack/plugins/build-manifest-plugin.ts index 78399e462e..d20eecea21 100644 --- a/packages/next/build/webpack/plugins/build-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/build-manifest-plugin.ts @@ -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 } diff --git a/packages/next/build/webpack/plugins/pages-manifest-plugin.ts b/packages/next/build/webpack/plugins/pages-manifest-plugin.ts index 30c35c3fae..bb983c7f7a 100644 --- a/packages/next/build/webpack/plugins/pages-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/pages-manifest-plugin.ts @@ -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( diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 5b124c673f..a4b78eeca8 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -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) } diff --git a/packages/next/server/hot-reloader.ts b/packages/next/server/hot-reloader.ts index cd5738ac0c..4d30dac754 100644 --- a/packages/next/server/hot-reloader.ts +++ b/packages/next/server/hot-reloader.ts @@ -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 { diff --git a/packages/next/server/on-demand-entry-handler.ts b/packages/next/server/on-demand-entry-handler.ts index c8a26ae3e0..99bd623434 100644 --- a/packages/next/server/on-demand-entry-handler.ts +++ b/packages/next/server/on-demand-entry-handler.ts @@ -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) diff --git a/test/integration/build-output/test/index.test.js b/test/integration/build-output/test/index.test.js index 383b9dc832..362a78880f 100644 --- a/test/integration/build-output/test/index.test.js +++ b/test/integration/build-output/test/index.test.js @@ -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'), diff --git a/test/integration/client-navigation/test/rendering.js b/test/integration/client-navigation/test/rendering.js index 2a5ac2d152..c63da021ff 100644 --- a/test/integration/client-navigation/test/rendering.js +++ b/test/integration/client-navigation/test/rendering.js @@ -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 diff --git a/test/integration/custom-routes-catchall/test/index.test.js b/test/integration/custom-routes-catchall/test/index.test.js index 80a0b640fb..da6605ebdd 100644 --- a/test/integration/custom-routes-catchall/test/index.test.js +++ b/test/integration/custom-routes-catchall/test/index.test.js @@ -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 () => { diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index c2e4b9f773..9c113d4a2c 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -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 () => { diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index edddcaf86c..9d59b3f39f 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -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 () => { diff --git a/test/integration/env-config/test/index.test.js b/test/integration/env-config/test/index.test.js index 4088ed5ef4..2ae15211a8 100644 --- a/test/integration/env-config/test/index.test.js +++ b/test/integration/env-config/test/index.test.js @@ -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) { diff --git a/test/integration/error-load-fail/test/index.test.js b/test/integration/error-load-fail/test/index.test.js index bfabd63b85..31bbe0bc1d 100644 --- a/test/integration/error-load-fail/test/index.test.js +++ b/test/integration/error-load-fail/test/index.test.js @@ -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() diff --git a/test/integration/modern-mode/test/index.test.js b/test/integration/modern-mode/test/index.test.js index f76778bd3c..735dcc68f6 100644 --- a/test/integration/modern-mode/test/index.test.js +++ b/test/integration/modern-mode/test/index.test.js @@ -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() }) }) diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index 45d69c267a..b227758fcd 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -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 diff --git a/test/integration/production/test/process-env.js b/test/integration/production/test/process-env.js index 0ff038cc6f..00d4aaa20f 100644 --- a/test/integration/production/test/process-env.js +++ b/test/integration/production/test/process-env.js @@ -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__/ diff --git a/test/integration/serverless-trace/test/index.test.js b/test/integration/serverless-trace/test/index.test.js index bec099d52b..db75f3bfd1 100644 --- a/test/integration/serverless-trace/test/index.test.js +++ b/test/integration/serverless-trace/test/index.test.js @@ -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', () => { diff --git a/test/integration/serverless/test/index.test.js b/test/integration/serverless/test/index.test.js index 93731bb4e1..b7cf8cc4d7 100644 --- a/test/integration/serverless/test/index.test.js +++ b/test/integration/serverless/test/index.test.js @@ -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', () => { diff --git a/test/integration/typeof-window-replace/test/index.test.js b/test/integration/typeof-window-replace/test/index.test.js index 2e37393035..29f6dbeff3 100644 --- a/test/integration/typeof-window-replace/test/index.test.js +++ b/test/integration/typeof-window-replace/test/index.test.js @@ -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/) diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index 7792dd2cd3..81604c0bd5 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -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') +}