Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serverless Next.js #5927

Merged
merged 33 commits into from
Dec 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2c37259
Remove distDir from renderOpts
timneutkens Dec 20, 2018
737f5ed
Add types for used modules
timneutkens Dec 20, 2018
80ee2ac
Add next build —target=serverless
timneutkens Dec 20, 2018
73f00bc
Disable manifests that are not used in serverless environment
timneutkens Dec 20, 2018
686cf62
Clean up temporary directory
timneutkens Dec 20, 2018
5eb78f8
Remove distDir from renderOpts
timneutkens Dec 21, 2018
ba9d22e
Remove lambdas option
timneutkens Dec 21, 2018
eb24266
Add rimraf types
timneutkens Dec 21, 2018
f4c1882
Implement file generation in loader
timneutkens Dec 21, 2018
e6970fe
Add target in next.config.js support
timneutkens Dec 21, 2018
7484b12
Add assetPrefix support
timneutkens Dec 21, 2018
5b7e0f9
Use the correct fallback mechanism
timneutkens Dec 21, 2018
740c1b0
Add custom _app/_error/_document support
timneutkens Dec 21, 2018
cdf6ace
Add integration tests for serverless target
timneutkens Dec 22, 2018
62d75cb
Default args
timneutkens Dec 22, 2018
8f8207a
Use alias for pages directory instead of absolute path
timneutkens Dec 22, 2018
8756fa8
Fix webpack warning
timneutkens Dec 22, 2018
afc1b97
Bring back node-fetch types
timneutkens Dec 22, 2018
1bc6414
Fix unit test
timneutkens Dec 23, 2018
fd83aa7
Make sure distDir is not an absolute path
timneutkens Dec 23, 2018
298e7ef
Don’t use underscores as they might throw of path joining in
timneutkens Dec 23, 2018
40682ad
Replace path so that it encodes correctly
timneutkens Dec 23, 2018
3f4944c
Make all paths use forward slashes
timneutkens Dec 23, 2018
f8a3147
Remove target flag, only allow target in next.config.js
timneutkens Dec 24, 2018
522d413
Add tests for target in next.config.js
timneutkens Dec 24, 2018
5d6356a
Add target to integration test next.config.js
timneutkens Dec 25, 2018
fd613ad
Remove target from test build
timneutkens Dec 25, 2018
87afaee
Bundle dynamic imports into serverless bundle
timneutkens Dec 26, 2018
051a7b6
Use serverless loader option types before stringify
timneutkens Dec 27, 2018
9239631
Merge branch 'canary' of github.com:zeit/next.js into add/serverless
timneutkens Dec 27, 2018
579a50e
Fix multiple dynamic imports
timneutkens Dec 27, 2018
64eb76c
Hook into the right lifecycle
timneutkens Dec 27, 2018
0ebbe42
Add support for 404 rendering
timneutkens Dec 28, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions packages/next-server/server/config.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/next-server/server/get-page-files.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {normalizePagePath} from './require'
import { normalizePagePath } from './normalize-page-path'

export type BuildManifest = {
devFiles: string[],
Expand Down
4 changes: 1 addition & 3 deletions packages/next-server/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -32,7 +32,6 @@ export default class Server {
buildId: string
renderOpts: {
staticMarkup: boolean,
distDir: string,
buildId: string,
generateEtags: boolean,
runtimeConfig?: {[key: string]: any},
Expand All @@ -54,7 +53,6 @@ export default class Server {
this.buildId = this.readBuildId()
this.renderOpts = {
staticMarkup,
distDir: this.distDir,
buildId: this.buildId,
generateEtags
}
Expand Down
17 changes: 17 additions & 0 deletions packages/next-server/server/normalize-page-path.ts
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 0 additions & 1 deletion packages/next-server/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ function render(renderElementToString: (element: React.ReactElement<any>) => str

type RenderOpts = {
staticMarkup: boolean,
distDir: string,
buildId: string,
runtimeConfig?: {[key: string]: any},
assetPrefix?: string,
Expand Down
23 changes: 2 additions & 21 deletions packages/next-server/server/require.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,13 @@
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}`)
err.code = 'ENOENT'
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))
Expand Down
14 changes: 6 additions & 8 deletions packages/next/bin/next-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']) {
Expand All @@ -30,23 +27,24 @@ 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?')
}

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)
})
76 changes: 68 additions & 8 deletions packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const glob = promisify(globModule)

function collectPages (directory: string, pageExtensions: string[]): Promise<string[]> {
return glob(`**/*.+(${pageExtensions.join('|')})`, {cwd: directory})
}

export default async function build (dir: string, conf = null): Promise<void> {
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]}
Expand All @@ -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)
Expand Down
Loading