diff --git a/packages/next-server/server/config.js b/packages/next-server/server/config.js index 181c04cf..330b1ee7 100644 --- a/packages/next-server/server/config.js +++ b/packages/next-server/server/config.js @@ -1,7 +1,7 @@ import findUp from 'find-up' import {CONFIG_FILE} from 'next-server/constants' -const targets = ['server', 'serverless'] +const targets = ['server', 'serverless', 'unified'] const defaultConfig = { webpack: null, diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 049f2992..addf3f25 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -2,6 +2,7 @@ import {join} from 'path' import {stringify} from 'querystring' import {PAGES_DIR_ALIAS, DOT_NEXT_ALIAS} from '../lib/constants' import {ServerlessLoaderQuery} from './webpack/loaders/next-serverless-loader' +import {UnifiedLoaderQuery} from './webpack/loaders/next-unified-loader' type PagesMapping = { [page: string]: string @@ -30,7 +31,7 @@ type Entrypoints = { server: WebpackEntrypoints } -export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverless', buildId: string, config: any): Entrypoints { +export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverless'|'unified', buildId: string, config: any): Entrypoints { const client: WebpackEntrypoints = {} const server: WebpackEntrypoints = {} @@ -60,6 +61,22 @@ export function createEntrypoints(pages: PagesMapping, target: 'server'|'serverl client[bundlePath] = `next-client-pages-loader?${stringify({page, absolutePagePath})}!` }) + if(target === 'unified') { + const pagesArray: Array = [] + const absolutePagePaths: Array = [] + + Object.keys(pages).forEach((page) => { + if(page === '/_document') { + return + } + pagesArray.push(page) + absolutePagePaths.push(pages[page]) + }); + + const unifiedLoaderOptions: UnifiedLoaderQuery = {pages: pagesArray.join(','), absolutePagePaths: absolutePagePaths.join(','), ...defaultServerlessOptions}; + server['index.js'] = `next-unified-loader?${stringify(unifiedLoaderOptions)}!` + } + return { client, server diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 0738bdbc..5f77fbad 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -47,8 +47,8 @@ export default async function build (dir: string, conf = null): Promise { ]) let result: CompilerResult = {warnings: [], errors: []} - if (config.target === 'serverless') { - if (config.publicRuntimeConfig) throw new Error('Cannot use publicRuntimeConfig with target=serverless https://err.sh/zeit/next.js/serverless-publicRuntimeConfig') + if (config.target === 'serverless' || config.target === 'unified') { + if (config.publicRuntimeConfig) throw new Error(`Cannot use publicRuntimeConfig with target=${config.target} https://err.sh/zeit/next.js/serverless-publicRuntimeConfig`) const clientResult = await runCompiler([configs[0]]) // Fail build if clientResult contains errors diff --git a/packages/next/build/webpack-config.js b/packages/next/build/webpack-config.js index 8b63d77f..efa4ae9f 100644 --- a/packages/next/build/webpack-config.js +++ b/packages/next/build/webpack-config.js @@ -26,7 +26,7 @@ function externalsConfig (isServer, target) { // When the serverless target is used all node_modules will be compiled into the output bundles // So that the serverless bundles have 0 runtime dependencies - if (!isServer || target === 'serverless') { + if (!isServer || target === 'serverless' || target === 'unified') { return externals } @@ -81,7 +81,7 @@ function optimizationConfig ({ dev, isServer, totalPages, target }) { } } - if (isServer && target === 'serverless') { + if (isServer && (target === 'serverless' || target === 'unified')) { return { splitChunks: false, minimizer: [ @@ -166,7 +166,12 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer .filter((p) => !!p) const distDir = path.join(dir, config.distDir) - const outputDir = target === 'serverless' ? 'serverless' : SERVER_DIRECTORY + let outputDir = SERVER_DIRECTORY + if (target === 'serverless') { + outputDir = 'serverless' + } else if (target === 'unified') { + outputDir = 'unified' + } const outputPath = path.join(distDir, isServer ? outputDir : '') const totalPages = Object.keys(entrypoints).length const clientEntries = !isServer ? { @@ -307,10 +312,10 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer !isServer && dev && new webpack.DefinePlugin({ 'process.env.__NEXT_DIST_DIR': JSON.stringify(distDir) }), - target !== 'serverless' && isServer && new PagesManifestPlugin(), + target !== 'serverless' && target !== 'unified' && isServer && new PagesManifestPlugin(), !isServer && new BuildManifestPlugin(), isServer && new NextJsSsrImportPlugin(), - target !== 'serverless' && isServer && new NextJsSSRModuleCachePlugin({outputPath}), + target !== 'serverless' && target !== 'unified' && isServer && new NextJsSSRModuleCachePlugin({ outputPath }), !dev && new webpack.IgnorePlugin({ checkResource: (resource) => { return /react-is/.test(resource) diff --git a/packages/next/build/webpack/loaders/next-unified-loader.ts b/packages/next/build/webpack/loaders/next-unified-loader.ts new file mode 100644 index 00000000..03886f92 --- /dev/null +++ b/packages/next/build/webpack/loaders/next-unified-loader.ts @@ -0,0 +1,135 @@ +import { loader } from 'webpack'; +import { join } from 'path'; +import { parse } from 'querystring'; +import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST } from 'next-server/constants'; + +export type UnifiedLoaderQuery = { + pages: string + distDir: string + absolutePagePaths: string + absoluteAppPath: string + absoluteDocumentPath: string + absoluteErrorPath: string + buildId: string + assetPrefix: string +} + +const nextUnifiedLoader: loader.Loader = function() { + const {distDir, absolutePagePaths, pages, buildId, assetPrefix, absoluteAppPath, absoluteDocumentPath, absoluteErrorPath}: UnifiedLoaderQuery = + typeof this.query === 'string' ? parse(this.query.substr(1)) : this.query + const buildManifest = join(distDir, BUILD_MANIFEST).replace(/\\/g, '/') + const reactLoadableManifest = join(distDir, REACT_LOADABLE_MANIFEST).replace(/\\/g, '/') + const parsedPagePaths = absolutePagePaths.split(',') + const parsedPages = pages.split(',') + return ` + import {renderToHTML} from 'next-server/dist/server/render' + import buildManifest from '${buildManifest}' + import reactLoadableManifest from '${reactLoadableManifest}' + import Document from '${absoluteDocumentPath}' + import Error from '${absoluteErrorPath}' + import App from '${absoluteAppPath}' + ${parsedPagePaths + .map( + (absolutePagePath, index) => + `import page${index} from '${absolutePagePath}'`, + ) + .join('\n')} + + const errorPage = '/_error' + const routes = ${JSON.stringify( + Object.assign( + {}, + ...parsedPages.map((page, index) => ({ [page]: `page${index}` })), + ), + ).replace(/"(page\d+)"/g, '$1')} + + function matchRoute(url) { + let page = '/index' + if (url === '/' && routes.hasOwnProperty(page)) { + return [page, routes[page]] + } + if (routes.hasOwnProperty(url)) { + return [url, routes[url]] + } + + const splitUrl = url.split('/'); + for (let i = splitUrl.length; i > 0; i--) { + const currentPrefix = splitUrl.slice(0, i).join('/') + + if (routes.hasOwnProperty(currentPrefix)) { + return [currentPrefix, routes[currentPrefix]] + } + } + + return [errorPage, routes[errorPage]] + }; + + const errorResponse = { status: 500, body: 'Internal Server Error', headers: {} } + + export async function render(url, query = {}, reqHeaders = {}) { + const req = { + headers: reqHeaders, + method: 'GET', + url + } + const [page, Component] = matchRoute(url) + const headers = {} + let body = '' + const res = { + statusCode: 200, + finished: false, + headersSent: false, + setHeader(name, value) { + headers[name.toLowerCase()] = value + }, + getHeader(name) { + return headers[name.toLowerCase()] + } + }; + const options = { + App, + Document, + buildManifest, + reactLoadableManifest, + buildId: ${JSON.stringify(buildId)}, + assetPrefix: ${JSON.stringify(assetPrefix)}, + Component + } + try { + if (page === '/_error') { + res.statusCode = 404 + } + body = await renderToHTML(req, res, page, query, Object.assign({}, options)) + } catch (err) { + if (err.code === 'ENOENT') { + res.statusCode = 404 + body = await renderToHTML(req, res, "/_error", query, Object.assign({}, options, { + Component: Error + })) + } else { + console.error(err) + try { + res.statusCode = 500 + body = await renderToHTML(req, res, "/_error", query, Object.assign({}, options, { + Component: Error, + err + })) + } catch (e) { + console.error(e) + return errorResponse; // non-html fatal/fallback error + } + } + } + return { + status: res.statusCode, + headers: Object.assign({ + 'content-type': 'text/html; charset=utf-8', + 'content-length': Buffer.byteLength(body) + }, headers), + body + } + } + `; +}; + +export default nextUnifiedLoader;