Skip to content

Commit

Permalink
Update filename generation for client-side compilation (#14279)
Browse files Browse the repository at this point in the history
Updates the way filenames are generated for browser compilation.
Notably:
- All entry bundles now have hashes in production, this includes pages (previously pages used a buildId in the path)
- The AmpFiles no longer depends on hardcoded bundle names, it uses the buildManifest instead (internals)
- All cases where we match the page name from the chunk/entrypoint name now use the same function `getRouteFromEntrypoint` (internals)
- In development we no longer include the "faked" `buildId` set to `development` for page files, instead we just use the `/_next/static/pages` path (was `/_next/static/development/pages`). This was changed as it caused unneeded complexity and makes generating the bundles easier (internals)
- Updated tons of tests to be more resilient to these changes by relying on the buildManifest instead of hardcoded paths (internals)

Follow up of these PRs:
#13759
#13870
#13937
#14130
#14176
#14268


Fixes #6303
Fixes #12087 
Fixes #1948
Fixes #4368
Fixes #4255
Fixes #2548
  • Loading branch information
timneutkens authored Jun 20, 2020
1 parent 76673fd commit bef9b56
Show file tree
Hide file tree
Showing 25 changed files with 218 additions and 154 deletions.
9 changes: 5 additions & 4 deletions packages/next/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ export function createEntrypoints(

Object.keys(pages).forEach((page) => {
const absolutePagePath = pages[page]
const bundleFile = `${normalizePagePath(page)}.js`
const bundleFile = normalizePagePath(page)
const isApiRoute = page.match(API_ROUTE)

const bundlePath = join('static', 'BUILD_ID', 'pages', bundleFile)
const clientBundlePath = join('static', 'pages', bundleFile)
const serverBundlePath = join('static', 'BUILD_ID', 'pages', bundleFile)

const isLikeServerless = isTargetLikeServerless(target)

Expand All @@ -114,7 +115,7 @@ export function createEntrypoints(
serverlessLoaderOptions
)}!`
} else if (isApiRoute || target === 'server') {
server[bundlePath] = [absolutePagePath]
server[serverBundlePath] = [absolutePagePath]
} else if (isLikeServerless && page !== '/_app' && page !== '/_document') {
const serverlessLoaderOptions: ServerlessLoaderQuery = {
page,
Expand Down Expand Up @@ -143,7 +144,7 @@ export function createEntrypoints(
// might cause the router to not be able to load causing hydration
// to fail

client[bundlePath] =
client[clientBundlePath] =
page === '/_app'
? [pageLoader, require.resolve('../client/router')]
: pageLoader
Expand Down
2 changes: 1 addition & 1 deletion packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
error.indexOf('__next_polyfill__') > -1
) {
throw new Error(
'> webpack config.resolve.alias was incorrectly overriden. https://err.sh/vercel/next.js/invalid-resolve-alias'
'> webpack config.resolve.alias was incorrectly overridden. https://err.sh/vercel/next.js/invalid-resolve-alias'
)
}
throw new Error('> Build failed because of webpack errors')
Expand Down
36 changes: 17 additions & 19 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,26 +679,24 @@ export default async function getBaseWebpackConfig(
},
output: {
path: outputPath,
filename: ({ chunk }: { chunk: { name: string } }) => {
// Use `[name]-[contenthash].js` in production
if (
!dev &&
(chunk.name === CLIENT_STATIC_FILES_RUNTIME_MAIN ||
chunk.name === CLIENT_STATIC_FILES_RUNTIME_WEBPACK ||
chunk.name === CLIENT_STATIC_FILES_RUNTIME_POLYFILLS)
) {
return chunk.name.replace(/\.js$/, '-[contenthash].js')
}

if (chunk.name.includes('BUILD_ID')) {
return escapePathVariables(chunk.name).replace(
'BUILD_ID',
isServer || dev ? buildId : '[contenthash]'
)
}
filename: isServer
? ({ chunk }: { chunk: { name: string } }) => {
// Use `[name]-[contenthash].js` in production
if (chunk.name.includes('BUILD_ID')) {
return (
escapePathVariables(chunk.name).replace(
'BUILD_ID',
isServer || dev ? buildId : '[contenthash]'
) + '.js'
)
}

return '[name]'
},
return '[name].js'
}
: // Client compilation only
dev
? '[name].js'
: '[name]-[chunkhash].js',
libraryTarget: isServer ? 'commonjs2' : 'var',
hotUpdateChunkFilename: isWebpack5
? 'static/webpack/[id].[fullhash].hot-update.js'
Expand Down
15 changes: 15 additions & 0 deletions packages/next/build/webpack/plugins/build-manifest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CLIENT_STATIC_FILES_RUNTIME_MAIN,
CLIENT_STATIC_FILES_RUNTIME_POLYFILLS,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
CLIENT_STATIC_FILES_RUNTIME_AMP,
} from '../../../next-server/lib/constants'
import { BuildManifest } from '../../../next-server/server/get-page-files'
import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-entrypoint'
Expand Down Expand Up @@ -62,6 +63,7 @@ export default class BuildManifestPlugin {
const assetMap: BuildManifest = {
polyfillFiles: [],
devFiles: [],
ampDevFiles: [],
lowPriorityFiles: [],
pages: { '/_app': [] },
ampFirstPages: [],
Expand Down Expand Up @@ -98,6 +100,19 @@ export default class BuildManifestPlugin {
assetMap.devFiles = reactRefreshChunk?.files.filter(isJsFile) ?? []

for (const entrypoint of compilation.entrypoints.values()) {
const isAmpRuntime =
entrypoint.name === CLIENT_STATIC_FILES_RUNTIME_AMP

if (isAmpRuntime) {
for (const file of entrypoint.getFiles()) {
if (!(isJsFile(file) || file.endsWith('.css'))) {
continue
}

assetMap.ampDevFiles.push(file.replace(/\\/g, '/'))
}
continue
}
const pagePath = getRouteFromEntrypoint(entrypoint.name)

if (!pagePath) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import webpack from 'webpack'
import { RawSource } from 'webpack-sources'
import { join, relative, dirname } from 'path'
import { IS_BUNDLED_PAGE_REGEX } from '../../../next-server/lib/constants'

import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-entrypoint'
const SSR_MODULE_CACHE_FILENAME = 'ssr-module-cache.js'

// By default webpack keeps initialized modules per-module.
Expand Down Expand Up @@ -44,7 +43,8 @@ export default class NextJsSsrImportPlugin {
// If the chunk is not part of the pages directory we have to keep the original behavior,
// otherwise webpack will error out when the file is used before the compilation finishes
// this is the case with mini-css-extract-plugin
if (!IS_BUNDLED_PAGE_REGEX.exec(chunk.name)) {

if (!getRouteFromEntrypoint(chunk.name)) {
return originalFn(source, chunk)
}
const pagePath = join(outputPath, dirname(chunk.name))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { join } from 'path'
import { promises } from 'fs'
import { IS_BUNDLED_PAGE_REGEX } from '../../../next-server/lib/constants'
import { Compiler } from 'webpack'

import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-entrypoint'
// Makes sure removed pages are removed from `.next` in development
export class UnlinkRemovedPagesPlugin {
prevAssets: any
Expand All @@ -15,7 +14,7 @@ export class UnlinkRemovedPagesPlugin {
'NextJsUnlinkRemovedPages',
(compilation, callback) => {
const removed = Object.keys(this.prevAssets).filter(
(a) => IS_BUNDLED_PAGE_REGEX.test(a) && !compilation.assets[a]
(a) => getRouteFromEntrypoint(a) && !compilation.assets[a]
)

this.prevAssets = compilation.assets
Expand Down
6 changes: 3 additions & 3 deletions packages/next/client/page-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,9 @@ export default class PageLoader {
route = normalizeRoute(route)
let scriptRoute = getAssetPath(route)

const url = `${this.assetPrefix}/_next/static/${encodeURIComponent(
this.buildId
)}/pages${encodeURI(scriptRoute)}.js`
const url = `${this.assetPrefix}/_next/static/pages${encodeURI(
scriptRoute
)}.js`
this.loadScript(url, route)
}
}
Expand Down
12 changes: 5 additions & 7 deletions packages/next/next-server/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,15 @@ export const AMP_RENDER_TARGET = '__NEXT_AMP_RENDER_TARGET__'
export const STRING_LITERAL_DROP_BUNDLE = '__NEXT_DROP_CLIENT_FILE__'
export const CLIENT_STATIC_FILES_RUNTIME_PATH = `${CLIENT_STATIC_FILES_PATH}/${CLIENT_STATIC_FILES_RUNTIME}`
// static/runtime/main.js
export const CLIENT_STATIC_FILES_RUNTIME_MAIN = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/main.js`
export const CLIENT_STATIC_FILES_RUNTIME_MAIN = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/main`
// static/runtime/react-refresh.js
export const CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/react-refresh.js`
export const CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/react-refresh`
// static/runtime/amp.js
export const CLIENT_STATIC_FILES_RUNTIME_AMP = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/amp.js`
export const CLIENT_STATIC_FILES_RUNTIME_AMP = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/amp`
// static/runtime/webpack.js
export const CLIENT_STATIC_FILES_RUNTIME_WEBPACK = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/webpack.js`
export const CLIENT_STATIC_FILES_RUNTIME_WEBPACK = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/webpack`
// static/runtime/polyfills.js
export const CLIENT_STATIC_FILES_RUNTIME_POLYFILLS = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/polyfills.js`
// matches static/<buildid>/pages/<page>.js
export const IS_BUNDLED_PAGE_REGEX = /^static[/\\][^/\\]+[/\\]pages.*\.js$/
export const CLIENT_STATIC_FILES_RUNTIME_POLYFILLS = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/polyfills`
export const TEMPORARY_REDIRECT_STATUS = 307
export const PERMANENT_REDIRECT_STATUS = 308
export const STATIC_PROPS_ID = '__N_SSG'
Expand Down
1 change: 1 addition & 0 deletions packages/next/next-server/server/get-page-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { normalizePagePath, denormalizePagePath } from './normalize-page-path'

export type BuildManifest = {
devFiles: string[]
ampDevFiles: string[]
polyfillFiles: string[]
lowPriorityFiles: string[]
pages: {
Expand Down
36 changes: 22 additions & 14 deletions packages/next/next-server/server/get-route-from-entrypoint.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import { denormalizePagePath } from './normalize-page-path'

// matches static/<buildid>/pages/:page*.js
const ROUTE_NAME_REGEX = /^static[/\\][^/\\]+[/\\]pages[/\\](.*)\.js$/
const SERVERLESS_ROUTE_NAME_REGEX = /^pages[/\\](.*)\.js$/
const ROUTE_NAME_REGEX = /^static[/\\][^/\\]+[/\\]pages[/\\](.*)$/
// matches pages/:page*.js
const SERVERLESS_ROUTE_NAME_REGEX = /^pages[/\\](.*)$/
// matches static/pages/:page*.js
const BROWSER_ROUTE_NAME_REGEX = /^static[/\\]pages[/\\](.*)$/

export default function getRouteFromEntrypoint(
entryFile: string,
isServerlessLike: boolean = false
): string | null {
const result = (isServerlessLike
? SERVERLESS_ROUTE_NAME_REGEX
: ROUTE_NAME_REGEX
).exec(entryFile)
function matchBundle(regex: RegExp, input: string): string | null {
const result = regex.exec(input)

if (!result) {
return null
}

const pagePath = result[1]
return denormalizePagePath(`/${result[1]}`)
}

if (!pagePath) {
return null
export default function getRouteFromEntrypoint(
entryFile: string,
isServerlessLike: boolean = false
): string | null {
let pagePath = matchBundle(
isServerlessLike ? SERVERLESS_ROUTE_NAME_REGEX : ROUTE_NAME_REGEX,
entryFile
)

if (pagePath) {
return pagePath
}

return denormalizePagePath(`/${pagePath}`)
// Potentially the passed item is a browser bundle so we try to match that also
return matchBundle(BROWSER_ROUTE_NAME_REGEX, entryFile)
}
1 change: 1 addition & 0 deletions packages/next/next-server/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export default class Server {
params.path[0] === 'css' ||
params.path[0] === 'media' ||
params.path[0] === this.buildId ||
params.path[0] === 'pages' ||
params.path[1] === 'pages'
) {
this.setImmutableAssetCacheControl(res)
Expand Down
38 changes: 15 additions & 23 deletions packages/next/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import PropTypes from 'prop-types'
import React, { useContext, Component } from 'react'
import flush from 'styled-jsx/server'
import {
AMP_RENDER_TARGET,
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
CLIENT_STATIC_FILES_RUNTIME_WEBPACK,
} from '../next-server/lib/constants'
import { AMP_RENDER_TARGET } from '../next-server/lib/constants'
import { DocumentContext as DocumentComponentContext } from '../next-server/lib/document-context'
import {
DocumentContext,
Expand Down Expand Up @@ -619,10 +614,9 @@ export class NextScript extends Component<OriginProps> {
return null
}

const AmpDevFiles = [
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_WEBPACK,
const ampDevFiles = [
...buildManifest.devFiles,
...buildManifest.ampDevFiles,
]

return (
Expand All @@ -643,19 +637,17 @@ export class NextScript extends Component<OriginProps> {
data-ampdevmode
/>
)}
{AmpDevFiles
? AmpDevFiles.map((file) => (
<script
key={file}
src={`${assetPrefix}/_next/${file}${_devOnlyInvalidateCacheQueryString}`}
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
data-ampdevmode
/>
))
: null}
{ampDevFiles.map((file) => (
<script
key={file}
src={`${assetPrefix}/_next/${file}${_devOnlyInvalidateCacheQueryString}`}
nonce={this.props.nonce}
crossOrigin={
this.props.crossOrigin || process.env.__NEXT_CROSS_ORIGIN
}
data-ampdevmode
/>
))}
</>
)
}
Expand Down
17 changes: 7 additions & 10 deletions packages/next/server/hot-reloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
BLOCKED_PAGES,
CLIENT_STATIC_FILES_RUNTIME_AMP,
CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
IS_BUNDLED_PAGE_REGEX,
} from '../next-server/lib/constants'
import { __ApiPreviewProps } from '../next-server/server/api-utils'
import { route } from '../next-server/server/router'
Expand Down Expand Up @@ -80,7 +79,7 @@ function addCorsSupport(req: IncomingMessage, res: ServerResponse) {
}

const matchNextPageBundleRequest = route(
'/_next/static/:buildId/pages/:path*.js(\\.map|)'
'/_next/static/pages/:path*.js(\\.map|)'
)

// Recursively look up the issuer till it ends up at the root
Expand Down Expand Up @@ -109,7 +108,7 @@ function erroredPages(
}

// Only pages have to be reloaded
if (!IS_BUNDLED_PAGE_REGEX.test(name)) {
if (!getRouteFromEntrypoint(name)) {
continue
}

Expand Down Expand Up @@ -195,15 +194,13 @@ export default class HotReloader {
parsedPageBundleUrl: UrlObject
): Promise<{ finished?: true }> => {
const { pathname } = parsedPageBundleUrl
const params = matchNextPageBundleRequest(pathname)
const params: { path: string[] } | null = matchNextPageBundleRequest(
pathname
)
if (!params) {
return {}
}

if (params.buildId !== this.buildId) {
return {}
}

const page = denormalizePagePath(`/${params.path.join('/')}`)
if (page === '/_error' || BLOCKED_PAGES.indexOf(page) === -1) {
try {
Expand Down Expand Up @@ -383,7 +380,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/BUILD_ID/pages/_document.js`)
(c) => c.name === normalize(`static/BUILD_ID/pages/_document`)
)
// If the document chunk can't be found we do nothing
if (!documentChunk) {
Expand Down Expand Up @@ -415,7 +412,7 @@ export default class HotReloader {
const chunkNames = new Set(
compilation.chunks
.map((c) => c.name)
.filter((name) => IS_BUNDLED_PAGE_REGEX.test(name))
.filter((name) => !!getRouteFromEntrypoint(name))
)

if (this.initialized) {
Expand Down
Loading

0 comments on commit bef9b56

Please sign in to comment.