diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 5f68dfcb643e9..0212ecd552fdc 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -288,7 +288,10 @@ export default async function build(dir: string, conf = null): Promise { console.error(error) console.error() - if (error.indexOf('private-next-pages') > -1) { + if ( + error.indexOf('private-next-pages') > -1 || + error.indexOf('__next_polyfill__') > -1 + ) { throw new Error( '> webpack config.resolve.alias was incorrectly overriden. https://err.sh/zeit/next.js/invalid-resolve-alias' ) diff --git a/packages/next/build/polyfills/fetch.js b/packages/next/build/polyfills/fetch.js new file mode 100644 index 0000000000000..ded91e814a85a --- /dev/null +++ b/packages/next/build/polyfills/fetch.js @@ -0,0 +1 @@ +export default window.fetch diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 982c3401127c6..28e941525d221 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -17,6 +17,7 @@ import { fileExists } from '../lib/file-exists' import { resolveRequest } from '../lib/resolve-request' import { CLIENT_STATIC_FILES_RUNTIME_MAIN, + CLIENT_STATIC_FILES_RUNTIME_POLYFILLS, CLIENT_STATIC_FILES_RUNTIME_WEBPACK, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY, @@ -54,6 +55,20 @@ const escapePathVariables = (value: any) => { : value } +function getOptimizedAliases(isServer: boolean): { [pkg: string]: string } { + if (isServer) { + return {} + } + + const stubWindowFetch = path.join(__dirname, 'polyfills', 'fetch.js') + return { + __next_polyfill__fetch: require.resolve('whatwg-fetch'), + unfetch$: stubWindowFetch, + 'isomorphic-unfetch$': stubWindowFetch, + 'whatwg-fetch$': stubWindowFetch, + } +} + export default async function getBaseWebpackConfig( dir: string, { @@ -148,6 +163,10 @@ export default async function getBaseWebpackConfig( dev ? `next-dev.js` : 'next.js' ) ), + [CLIENT_STATIC_FILES_RUNTIME_POLYFILLS]: path.join( + NEXT_PROJECT_ROOT_DIST_CLIENT, + 'polyfills.js' + ), } : undefined @@ -196,6 +215,7 @@ export default async function getBaseWebpackConfig( next: NEXT_PROJECT_ROOT, [PAGES_DIR_ALIAS]: pagesDir, [DOT_NEXT_ALIAS]: distDir, + ...getOptimizedAliases(isServer), }, mainFields: isServer ? ['main', 'module'] : ['browser', 'module', 'main'], plugins: [PnpWebpackPlugin], @@ -531,7 +551,8 @@ export default async function getBaseWebpackConfig( if ( !dev && (chunk.name === CLIENT_STATIC_FILES_RUNTIME_MAIN || - chunk.name === CLIENT_STATIC_FILES_RUNTIME_WEBPACK) + chunk.name === CLIENT_STATIC_FILES_RUNTIME_WEBPACK || + chunk.name === CLIENT_STATIC_FILES_RUNTIME_POLYFILLS) ) { return chunk.name.replace(/\.js$/, '-[contenthash].js') } diff --git a/packages/next/build/webpack/plugins/build-manifest-plugin.ts b/packages/next/build/webpack/plugins/build-manifest-plugin.ts index 0fae803aa88fc..ee404591390f0 100644 --- a/packages/next/build/webpack/plugins/build-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/build-manifest-plugin.ts @@ -1,13 +1,15 @@ import devalue from 'devalue' +import { Compiler } from 'webpack' +import { RawSource } from 'webpack-sources' + import { BUILD_MANIFEST, CLIENT_STATIC_FILES_PATH, CLIENT_STATIC_FILES_RUNTIME_MAIN, + CLIENT_STATIC_FILES_RUNTIME_POLYFILLS, IS_BUNDLED_PAGE_REGEX, ROUTE_NAME_REGEX, } from '../../../next-server/lib/constants' -import { Compiler } from 'webpack' -import { RawSource } from 'webpack-sources' interface AssetMap { devFiles: string[] @@ -27,7 +29,7 @@ const generateClientManifest = ( const appDependencies = new Set(assetMap.pages['/_app']) Object.entries(assetMap.pages).forEach(([page, dependencies]) => { - if (page === '/_app') return + if (page === '/_app' || page === '/_polyfills') return // Filter out dependencies in the _app entry, because those will have already // been loaded by the client prior to a navigation event const filteredDeps = dependencies.filter( @@ -74,6 +76,11 @@ export default class BuildManifestPlugin { ? mainJsChunk.files.filter((file: string) => /\.js$/.test(file)) : [] + const polyfillChunk = chunks.find( + c => c.name === CLIENT_STATIC_FILES_RUNTIME_POLYFILLS + ) + const polyfillFiles: string[] = polyfillChunk ? polyfillChunk.files : [] + for (const filePath of Object.keys(compilation.assets)) { const path = filePath.replace(/\\/g, '/') if (/^static\/development\/dll\//.test(path)) { @@ -125,6 +132,9 @@ export default class BuildManifestPlugin { assetMap.pages['/'] = assetMap.pages['/index'] } + // Create a separate entry for polyfills + assetMap.pages['/_polyfills'] = polyfillFiles + // Add the runtime build manifest file (generated later in this file) // as a dependency for the app. If the flag is false, the file won't be // downloaded by the client. diff --git a/packages/next/client/polyfills.js b/packages/next/client/polyfills.js new file mode 100644 index 0000000000000..5750e9a194ed9 --- /dev/null +++ b/packages/next/client/polyfills.js @@ -0,0 +1 @@ +import '__next_polyfill__fetch' diff --git a/packages/next/next-server/lib/constants.ts b/packages/next/next-server/lib/constants.ts index 039bbc2395613..741ec7fb5e240 100644 --- a/packages/next/next-server/lib/constants.ts +++ b/packages/next/next-server/lib/constants.ts @@ -21,6 +21,8 @@ export const CLIENT_STATIC_FILES_RUNTIME_MAIN = `${CLIENT_STATIC_FILES_RUNTIME_P export const CLIENT_STATIC_FILES_RUNTIME_AMP = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/amp.js` // static/runtime/webpack.js export const CLIENT_STATIC_FILES_RUNTIME_WEBPACK = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/webpack.js` +// static/runtime/polyfills.js +export const CLIENT_STATIC_FILES_RUNTIME_POLYFILLS = `${CLIENT_STATIC_FILES_RUNTIME_PATH}/polyfills.js` // matches static//pages/.js export const IS_BUNDLED_PAGE_REGEX = /^static[/\\][^/\\]+[/\\]pages.*\.js$/ // matches static//pages/:page*.js diff --git a/packages/next/next-server/lib/utils.ts b/packages/next/next-server/lib/utils.ts index 4661baa6bef56..590cf8a33cfa7 100644 --- a/packages/next/next-server/lib/utils.ts +++ b/packages/next/next-server/lib/utils.ts @@ -150,6 +150,7 @@ export type DocumentProps = DocumentInitialProps & { hasCssMode: boolean devFiles: string[] files: string[] + polyfillFiles: string[] dynamicImports: ManifestItem[] assetPrefix?: string canonicalBase: string diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 485ee6c5f00a6..a2f4a7b8001f0 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -188,6 +188,7 @@ function renderDocument( staticMarkup, devFiles, files, + polyfillFiles, dynamicImports, htmlProps, bodyTags, @@ -207,6 +208,7 @@ function renderDocument( dynamicImports: ManifestItem[] files: string[] devFiles: string[] + polyfillFiles: string[] htmlProps: any bodyTags: any headTags: any @@ -242,6 +244,7 @@ function renderDocument( staticMarkup, devFiles, files, + polyfillFiles, dynamicImports, assetPrefix, htmlProps, @@ -488,6 +491,7 @@ export async function renderToHTML( ...getPageFiles(buildManifest, '/_app'), ]), ] + const polyfillFiles = getPageFiles(buildManifest, '/_polyfills') const renderElementToString = staticMarkup ? renderToStaticMarkup @@ -642,6 +646,7 @@ export async function renderToHTML( dynamicImports, files, devFiles, + polyfillFiles, }) if (inAmpMode && html) { diff --git a/packages/next/package.json b/packages/next/package.json index 88627eb8c4391..ed52879f8966e 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -127,7 +127,8 @@ "webpack": "4.39.0", "webpack-dev-middleware": "3.7.0", "webpack-hot-middleware": "2.25.0", - "webpack-sources": "1.4.3" + "webpack-sources": "1.4.3", + "whatwg-fetch": "3.0.0" }, "peerDependencies": { "react": "^16.6.0", diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index a1957d3ace005..098a5dc330ea1 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -596,6 +596,25 @@ export class NextScript extends Component { }) } + getPolyfillScripts() { + // polyfills.js has to be rendered as nomodule without async + // It also has to be the first script to load + const { assetPrefix, polyfillFiles } = this.context._documentProps + const { _devOnlyInvalidateCacheQueryString } = this.context + + return polyfillFiles + .filter(polyfill => !/\.module\.js$/.test(polyfill)) + .map(polyfill => ( +