diff --git a/packages/next-server/server/config.js b/packages/next-server/server/config.js index b560b475..943edc6b 100644 --- a/packages/next-server/server/config.js +++ b/packages/next-server/server/config.js @@ -1,6 +1,8 @@ import findUp from 'find-up' import {CONFIG_FILE} from 'next-server/constants' +const targets = ['server', 'serverless'] + const defaultConfig = { webpack: null, webpackDevMiddleware: null, @@ -11,13 +13,21 @@ const defaultConfig = { useFileSystemPublicRoutes: true, generateBuildId: () => null, generateEtags: true, - pageExtensions: ['jsx', 'js'] + pageExtensions: ['jsx', 'js'], + target: 'server' +} + +function normalizeConfig (phase, config) { + if (typeof config === 'function') { + return config(phase, {defaultConfig}) + } + + return config } export default function loadConfig (phase, dir, customConfig) { if (customConfig) { - customConfig.configOrigin = 'server' - return {...defaultConfig, ...customConfig} + return {...defaultConfig, configOrigin: 'server', ...customConfig} } const path = findUp.sync(CONFIG_FILE, { cwd: dir @@ -26,12 +36,11 @@ export default function loadConfig (phase, dir, customConfig) { // If config file was found if (path && path.length) { const userConfigModule = require(path) - const userConfigInitial = userConfigModule.default || userConfigModule - if (typeof userConfigInitial === 'function') { - return {...defaultConfig, configOrigin: CONFIG_FILE, ...userConfigInitial(phase, {defaultConfig})} + const userConfig = normalizeConfig(phase, userConfigModule.default || userConfigModule) + if (userConfig.target && !targets.includes(userConfig.target)) { + throw new Error(`Specified target is invalid. Provided: "${userConfig.target}" should be one of ${targets.join(', ')}`) } - - return {...defaultConfig, configOrigin: CONFIG_FILE, ...userConfigInitial} + return {...defaultConfig, configOrigin: CONFIG_FILE, ...userConfig} } return defaultConfig diff --git a/packages/next-server/server/get-page-files.ts b/packages/next-server/server/get-page-files.ts index 2621c027..6e230959 100644 --- a/packages/next-server/server/get-page-files.ts +++ b/packages/next-server/server/get-page-files.ts @@ -1,4 +1,4 @@ -import {normalizePagePath} from './require' +import { normalizePagePath } from './normalize-page-path' export type BuildManifest = { devFiles: string[], diff --git a/packages/next-server/server/next-server.ts b/packages/next-server/server/next-server.ts index 96bb4ff9..ee1ec065 100644 --- a/packages/next-server/server/next-server.ts +++ b/packages/next-server/server/next-server.ts @@ -10,7 +10,7 @@ import {serveStatic} from './serve-static' import Router, {route, Route} from './router' import { isInternalUrl, isBlockedPage } from './utils' import loadConfig from 'next-server/next-config' -import {PHASE_PRODUCTION_SERVER, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH, CLIENT_STATIC_FILES_RUNTIME, BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY} from 'next-server/constants' +import {PHASE_PRODUCTION_SERVER, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH, CLIENT_STATIC_FILES_RUNTIME} from 'next-server/constants' import * as asset from '../lib/asset' import * as envConfig from '../lib/runtime-config' import {loadComponents} from './load-components' @@ -32,7 +32,6 @@ export default class Server { buildId: string renderOpts: { staticMarkup: boolean, - distDir: string, buildId: string, generateEtags: boolean, runtimeConfig?: {[key: string]: any}, @@ -54,7 +53,6 @@ export default class Server { this.buildId = this.readBuildId() this.renderOpts = { staticMarkup, - distDir: this.distDir, buildId: this.buildId, generateEtags } diff --git a/packages/next-server/server/normalize-page-path.ts b/packages/next-server/server/normalize-page-path.ts new file mode 100644 index 00000000..58afe2b4 --- /dev/null +++ b/packages/next-server/server/normalize-page-path.ts @@ -0,0 +1,17 @@ +import { posix } from 'path' +export function normalizePagePath (page: string): string { + // If the page is `/` we need to append `/index`, otherwise the returned directory root will be bundles instead of pages + if (page === '/') { + page = '/index' + } + // Resolve on anything that doesn't start with `/` + if (page[0] !== '/') { + page = `/${page}` + } + // Throw when using ../ etc in the pathname + const resolvedPage = posix.normalize(page) + if (page !== resolvedPage) { + throw new Error('Requested and resolved page mismatch') + } + return page +} diff --git a/packages/next-server/server/render.tsx b/packages/next-server/server/render.tsx index e3f37eba..c8646c58 100644 --- a/packages/next-server/server/render.tsx +++ b/packages/next-server/server/render.tsx @@ -46,7 +46,6 @@ function render(renderElementToString: (element: React.ReactElement) => str type RenderOpts = { staticMarkup: boolean, - distDir: string, buildId: string, runtimeConfig?: {[key: string]: any}, assetPrefix?: string, diff --git a/packages/next-server/server/require.ts b/packages/next-server/server/require.ts index 3ea44abc..8f30eb01 100644 --- a/packages/next-server/server/require.ts +++ b/packages/next-server/server/require.ts @@ -1,5 +1,6 @@ -import {join, posix} from 'path' +import {join} from 'path' import {PAGES_MANIFEST, SERVER_DIRECTORY} from 'next-server/constants' +import { normalizePagePath } from './normalize-page-path' export function pageNotFoundError (page: string): Error { const err: any = new Error(`Cannot find module for page: ${page}`) @@ -7,26 +8,6 @@ export function pageNotFoundError (page: string): Error { return err } -export function normalizePagePath (page: string): string { - // If the page is `/` we need to append `/index`, otherwise the returned directory root will be bundles instead of pages - if (page === '/') { - page = '/index' - } - - // Resolve on anything that doesn't start with `/` - if (page[0] !== '/') { - page = `/${page}` - } - - // Throw when using ../ etc in the pathname - const resolvedPage = posix.normalize(page) - if (page !== resolvedPage) { - throw new Error('Requested and resolved page mismatch') - } - - return page -} - export function getPagePath (page: string, distDir: string): string { const serverBuildPath = join(distDir, SERVER_DIRECTORY) const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST)) diff --git a/packages/next/bin/next-build.ts b/packages/next/bin/next-build.ts index 34bb1f78..04988be8 100755 --- a/packages/next/bin/next-build.ts +++ b/packages/next/bin/next-build.ts @@ -8,11 +8,8 @@ import { printAndExit } from '../server/lib/utils' const args = arg({ // Types '--help': Boolean, - '--lambdas': Boolean, - // Aliases - '-h': '--help', - '-l': '--lambdas' + '-h': '--help' }) if (args['--help']) { @@ -30,14 +27,15 @@ if (args['--help']) { } const dir = resolve(args._[0] || '.') -const lambdas = args['--lambdas'] -// Check if pages dir exists and warn if not +// Check if the provided directory exists if (!existsSync(dir)) { printAndExit(`> No such directory exists as the project root: ${dir}`) } +// Check if the pages directory exists if (!existsSync(join(dir, 'pages'))) { + // Check one level down the tree to see if the pages directory might be there if (existsSync(join(dir, '..', 'pages'))) { printAndExit('> No `pages` directory found. Did you mean to run `next` in the parent (`../`) directory?') } @@ -45,8 +43,8 @@ if (!existsSync(join(dir, 'pages'))) { printAndExit('> Couldn\'t find a `pages` directory. Please create one under the project root') } -build(dir, null, lambdas) +build(dir) .catch((err) => { - console.error('> Build error occured') + console.error('> Build error occurred') printAndExit(err) }) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 721c1666..f3dd6852 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -7,24 +7,84 @@ import {generateBuildId} from './generate-build-id' import {writeBuildId} from './write-build-id' import {isWriteable} from './is-writeable' import {runCompiler, CompilerResult} from './compiler' +import globModule from 'glob' +import {promisify} from 'util' +import {stringify} from 'querystring' +import {ServerlessLoaderQuery} from './webpack/loaders/next-serverless-loader' -export default async function build (dir: string, conf = null, lambdas: boolean = false): Promise { +const glob = promisify(globModule) + +function collectPages (directory: string, pageExtensions: string[]): Promise { + return glob(`**/*.+(${pageExtensions.join('|')})`, {cwd: directory}) +} + +export default async function build (dir: string, conf = null): Promise { if (!await isWriteable(dir)) { throw new Error('> Build directory is not writeable. https://err.sh/zeit/next.js/build-dir-not-writeable') } const config = loadConfig(PHASE_PRODUCTION_BUILD, dir, conf) - const lambdasOption = config.lambdas ? config.lambdas : lambdas - const distDir = join(dir, config.distDir) const buildId = await generateBuildId(config.generateBuildId, nanoid) + const distDir = join(dir, config.distDir) + const pagesDir = join(dir, 'pages') + + const pagePaths = await collectPages(pagesDir, config.pageExtensions) + type Result = {[page: string]: string} + const pages: Result = pagePaths.reduce((result: Result, pagePath): Result => { + let page = `/${pagePath.replace(new RegExp(`\\.+(${config.pageExtensions.join('|')})$`), '').replace(/\\/g, '/')}`.replace(/\/index$/, '') + page = page === '' ? '/' : page + result[page] = pagePath + return result + }, {}) + + let entrypoints + if (config.target === 'serverless') { + const serverlessEntrypoints: any = {} + // Because on Windows absolute paths in the generated code can break because of numbers, eg 1 in the path, + // we have to use a private alias + const pagesDirAlias = 'private-next-pages' + const dotNextDirAlias = 'private-dot-next' + const absoluteAppPath = pages['/_app'] ? join(pagesDirAlias, pages['/_app']).replace(/\\/g, '/') : 'next/dist/pages/_app' + const absoluteDocumentPath = pages['/_document'] ? join(pagesDirAlias, pages['/_document']).replace(/\\/g, '/') : 'next/dist/pages/_document' + const absoluteErrorPath = pages['/_error'] ? join(pagesDirAlias, pages['/_error']).replace(/\\/g, '/') : 'next/dist/pages/_error' + + const defaultOptions = { + absoluteAppPath, + absoluteDocumentPath, + absoluteErrorPath, + distDir: dotNextDirAlias, + buildId, + assetPrefix: config.assetPrefix, + generateEtags: config.generateEtags + } + + Object.keys(pages).forEach(async (page) => { + if (page === '/_app' || page === '/_document') { + return + } + + const absolutePagePath = join(pagesDirAlias, pages[page]).replace(/\\/g, '/') + const bundleFile = page === '/' ? '/index.js' : `${page}.js` + const serverlessLoaderOptions: ServerlessLoaderQuery = {page, absolutePagePath, ...defaultOptions} + serverlessEntrypoints[join('pages', bundleFile)] = `next-serverless-loader?${stringify(serverlessLoaderOptions)}!` + }) + + const errorPage = join('pages', '/_error.js') + if (!serverlessEntrypoints[errorPage]) { + const serverlessLoaderOptions: ServerlessLoaderQuery = {page: '/_error', absolutePagePath: 'next/dist/pages/_error', ...defaultOptions} + serverlessEntrypoints[errorPage] = `next-serverless-loader?${stringify(serverlessLoaderOptions)}!` + } + + entrypoints = serverlessEntrypoints + } const configs: any = await Promise.all([ - getBaseWebpackConfig(dir, { buildId, isServer: false, config, lambdas: lambdasOption }), - getBaseWebpackConfig(dir, { buildId, isServer: true, config, lambdas: lambdasOption }) + getBaseWebpackConfig(dir, { buildId, isServer: false, config, target: config.target }), + getBaseWebpackConfig(dir, { buildId, isServer: true, config, target: config.target, entrypoints }) ]) let result: CompilerResult = {warnings: [], errors: []} - if (lambdasOption) { + if (config.target === 'serverless') { const clientResult = await runCompiler([configs[0]]) const serverResult = await runCompiler([configs[1]]) result = {warnings: [...clientResult.warnings, ...serverResult.warnings], errors: [...clientResult.errors, ...serverResult.errors]} @@ -34,11 +94,11 @@ export default async function build (dir: string, conf = null, lambdas: boolean if (result.warnings.length > 0) { console.warn('> Emitted warnings from webpack') - console.warn(...result.warnings) + result.warnings.forEach((warning) => console.warn(warning)) } if (result.errors.length > 0) { - console.error(...result.errors) + result.errors.forEach((error) => console.error(error)) throw new Error('> Build failed because of webpack errors') } await writeBuildId(distDir, buildId) diff --git a/packages/next/build/webpack-config.js b/packages/next/build/webpack-config.js index 3979f2c3..8bc71e45 100644 --- a/packages/next/build/webpack-config.js +++ b/packages/next/build/webpack-config.js @@ -15,54 +15,24 @@ import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin' import ChunkNamesPlugin from './webpack/plugins/chunk-names-plugin' import { ReactLoadablePlugin } from './webpack/plugins/react-loadable-plugin' import {SERVER_DIRECTORY, REACT_LOADABLE_MANIFEST, CLIENT_STATIC_FILES_RUNTIME_WEBPACK, CLIENT_STATIC_FILES_RUNTIME_MAIN} from 'next-server/constants' -import {NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_NODE_MODULES, NEXT_PROJECT_ROOT_DIST_CLIENT, NEXT_PROJECT_ROOT_DIST_SERVER, DEFAULT_PAGES_DIR} from '../lib/constants' +import {NEXT_PROJECT_ROOT, NEXT_PROJECT_ROOT_NODE_MODULES, NEXT_PROJECT_ROOT_DIST_CLIENT, DEFAULT_PAGES_DIR} from '../lib/constants' import AutoDllPlugin from 'autodll-webpack-plugin' import TerserPlugin from 'terser-webpack-plugin' import AssetsSizePlugin from './webpack/plugins/assets-size-plugin' +import {ServerlessPlugin} from './webpack/plugins/serverless-plugin' // The externals config makes sure that // on the server side when modules are // in node_modules they don't get compiled by webpack -function externalsConfig (dir, isServer, lambdas) { +function externalsConfig (isServer, target) { const externals = [] - if (!isServer) { + // 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') { return externals } - // When lambdas mode is enabled all node_modules will be compiled into the server bundles - // So that all dependencies can be devDependencies and are not required to be installed - if (lambdas) { - return [ - (context, request, callback) => { - // Make react/react-dom external until we bundle the server/renderer. - if (request === 'react' || request === 'react-dom') { - return callback(null, `commonjs ${request}`) - } - - resolve(request, { basedir: context, preserveSymlinks: true }, (err, res) => { - if (err) { - return callback() - } - if (res.match(/next-server[/\\]dist[/\\]lib[/\\]head/)) { - return callback(null, `commonjs next-server/dist/lib/head.js`) - } - if (res.match(/next-server[/\\]dist[/\\]lib[/\\]asset/)) { - return callback(null, `commonjs next-server/dist/lib/asset.js`) - } - if (res.match(/next-server[/\\]dist[/\\]lib[/\\]runtime-config/)) { - return callback(null, `commonjs next-server/dist/lib/runtime-config.js`) - } - // Default pages have to be transpiled - if (res.match(/next-server[/\\]dist[/\\]lib[/\\]loadable/)) { - return callback(null, `commonjs next-server/dist/lib/loadable.js`) - } - callback() - }) - } - ] - } - const notExternalModules = ['next/app', 'next/document', 'next/link', 'next/router', 'next/error', 'http-status', 'string-hash', 'ansi-html', 'hoist-non-react-statics', 'htmlescape'] externals.push((context, request, callback) => { @@ -101,7 +71,7 @@ function externalsConfig (dir, isServer, lambdas) { return externals } -function optimizationConfig ({ dir, dev, isServer, totalPages, lambdas }) { +function optimizationConfig ({ dev, isServer, totalPages, target }) { const terserPluginConfig = { parallel: true, sourceMap: false, @@ -114,7 +84,7 @@ function optimizationConfig ({ dir, dev, isServer, totalPages, lambdas }) { } } - if (isServer && lambdas) { + if (isServer && target === 'serverless') { return { splitChunks: false, minimizer: [ @@ -169,7 +139,7 @@ function optimizationConfig ({ dir, dev, isServer, totalPages, lambdas }) { return config } -export default async function getBaseWebpackConfig (dir, {dev = false, isServer = false, buildId, config, lambdas = false}) { +export default async function getBaseWebpackConfig (dir, {dev = false, isServer = false, buildId, config, target = 'server', entrypoints = false}) { const defaultLoaders = { babel: { loader: 'next-babel-loader', @@ -194,7 +164,8 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer .filter((p) => !!p) const distDir = path.join(dir, config.distDir) - const outputPath = path.join(distDir, isServer ? SERVER_DIRECTORY : '') + const outputDir = target === 'serverless' ? 'serverless' : SERVER_DIRECTORY + const outputPath = path.join(distDir, isServer ? outputDir : '') const pagesEntries = await getPages(dir, {nextPagesDir: DEFAULT_PAGES_DIR, dev, buildId, isServer, pageExtensions: config.pageExtensions.join('|')}) const totalPages = Object.keys(pagesEntries).length const clientEntries = !isServer ? { @@ -204,9 +175,6 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer path.join(NEXT_PROJECT_ROOT_DIST_CLIENT, (dev ? `next-dev` : 'next')) ].filter(Boolean) } : {} - const devServerEntries = dev && isServer ? { - 'error-debug.js': path.join(NEXT_PROJECT_ROOT_DIST_SERVER, 'error-debug.js') - } : {} const resolveConfig = { // Disable .mjs for node_modules bundling @@ -217,7 +185,9 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer ...nodePathList // Support for NODE_PATH environment variable ], alias: { - next: NEXT_PROJECT_ROOT + next: NEXT_PROJECT_ROOT, + 'private-next-pages': path.join(dir, 'pages'), + 'private-dot-next': distDir }, mainFields: isServer ? ['main'] : ['browser', 'module', 'main'] } @@ -229,15 +199,17 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer devtool: dev ? 'cheap-module-source-map' : false, name: isServer ? 'server' : 'client', target: isServer ? 'node' : 'web', - externals: externalsConfig(dir, isServer, lambdas), - optimization: optimizationConfig({dir, dev, isServer, totalPages, lambdas}), + externals: externalsConfig(isServer, target), + optimization: optimizationConfig({dir, dev, isServer, totalPages, target}), recordsPath: path.join(outputPath, 'records.json'), context: dir, // Kept as function to be backwards compatible entry: async () => { + if (entrypoints) { + return entrypoints + } return { ...clientEntries, - ...devServerEntries, // Only _error and _document when in development. The rest is handled by on-demand-entries ...pagesEntries } @@ -292,6 +264,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer ].filter(Boolean) }, plugins: [ + target === 'serverless' && isServer && new ServerlessPlugin(), // Precompile react / react-dom for development, speeding up webpack dev && !isServer && new AutoDllPlugin({ filename: '[name]_[hash].js', @@ -334,11 +307,11 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer !isServer && dev && new webpack.DefinePlugin({ 'process.env.__NEXT_DIST_DIR': JSON.stringify(distDir) }), - isServer && new PagesManifestPlugin(), + target !== 'serverless' && isServer && new PagesManifestPlugin(), !isServer && new BuildManifestPlugin(), !isServer && new PagesPlugin(), isServer && new NextJsSsrImportPlugin(), - isServer && new NextJsSSRModuleCachePlugin({outputPath}), + target !== 'serverless' && isServer && new NextJsSSRModuleCachePlugin({outputPath}), !isServer && !dev && new AssetsSizePlugin({buildId, distDir}) ].filter(Boolean) } diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts new file mode 100644 index 00000000..045176eb --- /dev/null +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -0,0 +1,92 @@ +import {loader} from 'webpack' +import {join} from 'path' +import {parse} from 'querystring' +import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST } from 'next-server/constants' + +export type ServerlessLoaderQuery = { + page: string, + distDir: string, + absolutePagePath: string, + absoluteAppPath: string, + absoluteDocumentPath: string, + absoluteErrorPath: string, + buildId: string, + assetPrefix: string, + generateEtags: string +} + +const nextServerlessLoader: loader.Loader = function () { + const { + distDir, + absolutePagePath, + page, + buildId, + assetPrefix, + absoluteAppPath, + absoluteDocumentPath, + absoluteErrorPath, + generateEtags + }: ServerlessLoaderQuery = 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, '/') + return ` + import {parse} from 'url' + import {renderToHTML} from 'next-server/dist/server/render'; + import {sendHTML} from 'next-server/dist/server/send-html'; + import buildManifest from '${buildManifest}'; + import reactLoadableManifest from '${reactLoadableManifest}'; + import Document from '${absoluteDocumentPath}'; + import Error from '${absoluteErrorPath}'; + import App from '${absoluteAppPath}'; + import Component from '${absolutePagePath}'; + async function render(req, res) { + const options = { + App, + Document, + buildManifest, + reactLoadableManifest, + buildId: "${buildId}", + assetPrefix: "${assetPrefix}" + } + const parsedUrl = parse(req.url, true) + try { + ${page === '/_error' ? `res.statusCode = 404` : ''} + const result = await renderToHTML(req, res, "${page}", parsedUrl.query, { + ...options, + Component + }) + return result + } catch (err) { + if (err.code === 'ENOENT') { + res.statusCode = 404 + const result = await renderToHTML(req, res, "/_error", parsedUrl.query, { + ...options, + Component: Error + }) + return result + } else { + console.error(err) + res.statusCode = 500 + const result = await renderToHTML(req, res, "/_error", parsedUrl.query, { + ...options, + Component: Error, + err + }) + return result + } + } + } + export default async (req, res) => { + try { + const html = await render(req, res) + sendHTML(req, res, html, {generateEtags: ${generateEtags}}) + } catch(err) { + console.error(err) + res.statusCode = 500 + res.end('Internal Server Error') + } + } + ` +} + +export default nextServerlessLoader diff --git a/packages/next/build/webpack/plugins/serverless-plugin.ts b/packages/next/build/webpack/plugins/serverless-plugin.ts new file mode 100644 index 00000000..e170d324 --- /dev/null +++ b/packages/next/build/webpack/plugins/serverless-plugin.ts @@ -0,0 +1,31 @@ +import {Compiler} from 'webpack' +import GraphHelpers from 'webpack/lib/GraphHelpers' +/** + * Makes sure there are no dynamic chunks when the target is serverless + * The dynamic chunks are integrated back into their parent chunk + * This is to make sure there is a single render bundle instead of that bundle importing dynamic chunks + */ +export class ServerlessPlugin { + apply (compiler: Compiler) { + compiler.hooks.compilation.tap('ServerlessPlugin', compilation => { + compilation.hooks.optimizeChunksBasic.tap( + 'ServerlessPlugin', + chunks => { + chunks.forEach((chunk) => { + // If chunk is not an entry point skip them + if (chunk.hasEntryModule()) { + const dynamicChunks = chunk.getAllAsyncChunks() + if (dynamicChunks.size !== 0) { + for (const dynamicChunk of dynamicChunks) { + for (const module of dynamicChunk.modulesIterable) { + GraphHelpers.connectChunkAndModule(chunk, module) + } + } + } + } + }) + } + ) + }) + } +} diff --git a/packages/next/package.json b/packages/next/package.json index 80f66688..f3690faa 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -48,6 +48,8 @@ "@babel/runtime": "7.1.2", "@babel/runtime-corejs2": "7.1.2", "@babel/template": "7.1.2", + "@types/node-fetch": "2.1.4", + "@types/rimraf": "2.0.2", "ansi-html": "0.0.7", "arg": "3.0.0", "async-sema": "^2.1.4", @@ -108,6 +110,8 @@ "@types/cross-spawn": "6.0.0", "@types/etag": "1.8.0", "@types/fresh": "0.5.0", + "@types/glob": "7.1.1", + "@types/mkdirp": "0.5.2", "@types/nanoid": "1.2.0", "@types/node-fetch": "2.1.4", "@types/webpack": "4.4.22", diff --git a/packages/next/server/on-demand-entry-handler.js b/packages/next/server/on-demand-entry-handler.js index d2d178dd..58168d2e 100644 --- a/packages/next/server/on-demand-entry-handler.js +++ b/packages/next/server/on-demand-entry-handler.js @@ -4,7 +4,8 @@ import { join } from 'path' import fs from 'fs' import promisify from '../lib/promisify' import globModule from 'glob' -import {normalizePagePath, pageNotFoundError} from 'next-server/dist/server/require' +import {pageNotFoundError} from 'next-server/dist/server/require' +import {normalizePagePath} from 'next-server/dist/server/normalize-page-path' import {createEntry} from '../build/webpack/utils' import { ROUTE_NAME_REGEX, IS_BUNDLED_PAGE_REGEX } from 'next-server/constants' diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index 1e2bda41..5edcc518 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -1,6 +1,8 @@ declare module '@babel/plugin-transform-modules-commonjs'; declare module 'next-server/next-config'; declare module 'next-server/constants'; +declare module 'webpack/lib/GraphHelpers'; + declare module 'arg' { function arg(spec: T, options?: {argv?: string[], permissive?: boolean}): arg.Result; diff --git a/test/integration/serverless/components/hello.js b/test/integration/serverless/components/hello.js new file mode 100644 index 00000000..0a350ec1 --- /dev/null +++ b/test/integration/serverless/components/hello.js @@ -0,0 +1 @@ +export default () =>

Hello!

diff --git a/test/integration/lambdas/next.config.js b/test/integration/serverless/next.config.js similarity index 86% rename from test/integration/lambdas/next.config.js rename to test/integration/serverless/next.config.js index a4676bfc..c4b675ed 100644 --- a/test/integration/lambdas/next.config.js +++ b/test/integration/serverless/next.config.js @@ -1,4 +1,5 @@ module.exports = { + target: 'serverless', onDemandEntries: { // Make sure entries are not getting disposed. maxInactiveAge: 1000 * 60 * 60 diff --git a/test/integration/serverless/pages/abc.js b/test/integration/serverless/pages/abc.js new file mode 100644 index 00000000..7d148e90 --- /dev/null +++ b/test/integration/serverless/pages/abc.js @@ -0,0 +1 @@ +export default () =>
test
diff --git a/test/integration/serverless/pages/dynamic-two.js b/test/integration/serverless/pages/dynamic-two.js new file mode 100644 index 00000000..2750f3cb --- /dev/null +++ b/test/integration/serverless/pages/dynamic-two.js @@ -0,0 +1,7 @@ +import dynamic from 'next/dynamic' + +const Hello = dynamic(() => import('../components/hello')) + +export default () =>
+ +
diff --git a/test/integration/serverless/pages/dynamic.js b/test/integration/serverless/pages/dynamic.js new file mode 100644 index 00000000..2750f3cb --- /dev/null +++ b/test/integration/serverless/pages/dynamic.js @@ -0,0 +1,7 @@ +import dynamic from 'next/dynamic' + +const Hello = dynamic(() => import('../components/hello')) + +export default () =>
+ +
diff --git a/test/integration/lambdas/pages/fetch.js b/test/integration/serverless/pages/fetch.js similarity index 100% rename from test/integration/lambdas/pages/fetch.js rename to test/integration/serverless/pages/fetch.js diff --git a/test/integration/lambdas/pages/index.js b/test/integration/serverless/pages/index.js similarity index 100% rename from test/integration/lambdas/pages/index.js rename to test/integration/serverless/pages/index.js diff --git a/test/integration/serverless/run-server.js b/test/integration/serverless/run-server.js new file mode 100644 index 00000000..27ae48b5 --- /dev/null +++ b/test/integration/serverless/run-server.js @@ -0,0 +1,2 @@ +const start = require('./server') +start(3000).then(() => console.log('http://localhost:3000')) diff --git a/test/integration/serverless/server.js b/test/integration/serverless/server.js new file mode 100644 index 00000000..9e9342d6 --- /dev/null +++ b/test/integration/serverless/server.js @@ -0,0 +1,38 @@ +const express = require('express') +const http = require('http') +const path = require('path') + +module.exports = function start (port = 0) { + return new Promise((resolve, reject) => { + const app = express() + const nextStaticDir = path.join(__dirname, '.next', 'static') + app.use('/_next/static', express.static(nextStaticDir)) + app.get('/', (req, res) => { + require('./.next/serverless/pages/index.js').default(req, res) + }) + app.get('/abc', (req, res) => { + require('./.next/serverless/pages/abc.js').default(req, res) + }) + app.get('/fetch', (req, res) => { + require('./.next/serverless/pages/fetch.js').default(req, res) + }) + app.get('/dynamic', (req, res) => { + require('./.next/serverless/pages/dynamic.js').default(req, res) + }) + app.get('/dynamic-two', (req, res) => { + require('./.next/serverless/pages/dynamic-two.js').default(req, res) + }) + app.get('/404', (req, res) => { + require('./.next/serverless/pages/_error.js').default(req, res) + }) + const server = new http.Server(app) + + server.listen(port, (err) => { + if (err) { + return reject(err) + } + + resolve(server) + }) + }) +} diff --git a/test/integration/lambdas/test/index.test.js b/test/integration/serverless/test/index.test.js similarity index 51% rename from test/integration/lambdas/test/index.test.js rename to test/integration/serverless/test/index.test.js index 3fd38d6d..5e4e08ab 100644 --- a/test/integration/lambdas/test/index.test.js +++ b/test/integration/serverless/test/index.test.js @@ -2,34 +2,24 @@ /* global jasmine, test */ import { join } from 'path' import { - nextServer, nextBuild, - startApp, stopApp, renderViaHTTP } from 'next-test-utils' +import startServer from '../server' import webdriver from 'next-webdriver' import fetch from 'node-fetch' const appDir = join(__dirname, '../') let appPort let server -let app jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5 -const context = {} - -describe('Lambdas', () => { +describe('Serverless', () => { beforeAll(async () => { await nextBuild(appDir) - app = nextServer({ - dir: join(__dirname, '../'), - dev: false, - quiet: true - }) - - server = await startApp(app) - context.appPort = appPort = server.address().port + server = await startServer() + appPort = server.address().port }) afterAll(() => stopApp(server)) @@ -38,6 +28,21 @@ describe('Lambdas', () => { expect(html).toMatch(/Hello World/) }) + it('should render the page with dynamic import', async () => { + const html = await renderViaHTTP(appPort, '/dynamic') + expect(html).toMatch(/Hello!/) + }) + + it('should render the page with same dynamic import', async () => { + const html = await renderViaHTTP(appPort, '/dynamic-two') + expect(html).toMatch(/Hello!/) + }) + + it('should render 404', async () => { + const html = await renderViaHTTP(appPort, '/404') + expect(html).toMatch(/This page could not be found/) + }) + it('should render correctly when importing isomorphic-unfetch', async () => { const url = `http://localhost:${appPort}/fetch` const res = await fetch(url) @@ -59,4 +64,21 @@ describe('Lambdas', () => { browser.close() } }) + + describe('With basic usage', () => { + it('should allow etag header support', async () => { + const url = `http://localhost:${appPort}/` + const etag = (await fetch(url)).headers.get('ETag') + + const headers = { 'If-None-Match': etag } + const res2 = await fetch(url, { headers }) + expect(res2.status).toBe(304) + }) + + it('should set Content-Length header', async () => { + const url = `http://localhost:${appPort}` + const res = await fetch(url) + expect(res.headers.get('Content-Length')).toBeDefined() + }) + }) }) diff --git a/test/isolated/_resolvedata/invalid-target/next.config.js b/test/isolated/_resolvedata/invalid-target/next.config.js new file mode 100644 index 00000000..5bc42d6f --- /dev/null +++ b/test/isolated/_resolvedata/invalid-target/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + target: 'nonexistent' +} diff --git a/test/isolated/_resolvedata/valid-target/next.config.js b/test/isolated/_resolvedata/valid-target/next.config.js new file mode 100644 index 00000000..0fbd0e53 --- /dev/null +++ b/test/isolated/_resolvedata/valid-target/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + target: 'serverless' +} diff --git a/test/isolated/config.test.js b/test/isolated/config.test.js index 6d1fb190..9afa4ef2 100644 --- a/test/isolated/config.test.js +++ b/test/isolated/config.test.js @@ -32,4 +32,19 @@ describe('config', () => { const config = loadConfig(PHASE_DEVELOPMENT_SERVER, null, null) expect(config.webpack).toBe(null) }) + + it('Should throw when an invalid target is provided', () => { + try { + loadConfig(PHASE_DEVELOPMENT_SERVER, join(__dirname, '_resolvedata', 'invalid-target')) + // makes sure we don't just pass if the loadConfig passes while it should fail + throw new Error('failed') + } catch (err) { + expect(err.message).toMatch(/Specified target is invalid/) + } + }) + + it('Should pass when a valid target is provided', () => { + const config = loadConfig(PHASE_DEVELOPMENT_SERVER, join(__dirname, '_resolvedata', 'valid-target')) + expect(config.target).toBe('serverless') + }) }) diff --git a/test/isolated/require-page.test.js b/test/isolated/require-page.test.js index 09430639..87c6de75 100644 --- a/test/isolated/require-page.test.js +++ b/test/isolated/require-page.test.js @@ -2,7 +2,8 @@ import { join } from 'path' import {SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH} from 'next-server/constants' -import {requirePage, getPagePath, normalizePagePath, pageNotFoundError} from 'next-server/dist/server/require' +import {requirePage, getPagePath, pageNotFoundError} from 'next-server/dist/server/require' +import {normalizePagePath} from 'next-server/dist/server/normalize-page-path' const sep = '/' const distDir = join(__dirname, '_resolvedata') diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index ce0fff4a..aae2f976 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -126,8 +126,8 @@ export function launchApp (dir, port) { return runNextCommandDev([dir, '-p', port]) } -export function nextBuild (dir) { - return runNextCommand(['build', dir]) +export function nextBuild (dir, args = []) { + return runNextCommand(['build', dir, ...args]) } export function nextExport (dir, {outdir}) { diff --git a/yarn.lock b/yarn.lock index 75aa9e44..ea71bccf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1392,16 +1392,42 @@ dependencies: "@types/node" "*" +"@types/events@*": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" + integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA== + "@types/fresh@0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@types/fresh/-/fresh-0.5.0.tgz#4d09231027d69c4369cfb01a9af5ef083d0d285f" integrity sha512-eGPzuyc6wZM3sSHJdF7NM2jW6B/xsB014Rqg/iDa6xY02mlfy1w/TE2sYhR8vbHxkzJOXiGo6NuIk3xk35vsgQ== +"@types/glob@*", "@types/glob@7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + "@types/mime@*": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" integrity sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA== +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/mkdirp@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f" + integrity sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg== + dependencies: + "@types/node" "*" + "@types/nanoid@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/nanoid/-/nanoid-1.2.0.tgz#c0141cfec5b4818016355389b0bd539097d872ff" @@ -1448,6 +1474,14 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/rimraf@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e" + integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ== + dependencies: + "@types/glob" "*" + "@types/node" "*" + "@types/send@0.14.4": version "0.14.4" resolved "https://registry.yarnpkg.com/@types/send/-/send-0.14.4.tgz#d70458b030305999db619a7b057f7105058bd0ff"