rsnext/packages/next/build/webpack/plugins/serverless-plugin.ts
Joe Haddad b31c296730
Experimental: Serverless Trace target (#8246)
* Experimental: Serverless Trace target
The Serverless Trace target produces Serverless-handler wrapped entrypoints, but does not bundle all of `node_modules`.

This behavior increases bundling performance to be more akin to `target: 'server'`.

This mode is expected to be used with smart platforms (like [ZEIT Now](https://zeit.co/now) that can trace a program to its minimum dependencies.

* Use more generic variables

* Add asset relocator for production mode of serverless trace

* Verify Firebase compatiblity

* Revert "Add asset relocator for production mode of serverless trace"

This reverts commit 8404f1dcf28b60edab41a56c94b38dcd3fddec20.

* Add serverless trace tests

* Add _isLikeServerless helper

* Make constants

* Fix export

* Update packages/next-server/server/config.ts

Co-Authored-By: JJ Kasper <jj@jjsweb.site>

* Use a global helper for is like serverless

* Update import for isTargetLikeServerless

* Update packages/next/build/index.ts

Co-Authored-By: JJ Kasper <jj@jjsweb.site>
2019-08-05 18:26:20 -04:00

120 lines
3.4 KiB
TypeScript

import { Compiler } from 'webpack'
import { connectChunkAndModule } 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
*/
const NEXT_REPLACE_BUILD_ID = '__NEXT_REPLACE__BUILD_ID__'
function replaceInBuffer(buffer: Buffer, from: string, to: string) {
const target = Buffer.from(from, 'utf8')
const replacement = Buffer.from(to, 'utf8')
function bufferTee(source: Buffer): Buffer {
const index = source.indexOf(target)
if (index === -1) {
// Escape recursion loop
return source
}
const b1 = source.slice(0, index)
const b2 = source.slice(index + target.length)
const nextBuffer = bufferTee(b2)
return Buffer.concat(
[b1, replacement, nextBuffer],
index + replacement.length + nextBuffer.length
)
}
return bufferTee(buffer)
}
function interceptFileWrites(
compiler: Compiler,
contentFn: (input: Buffer) => Buffer
) {
compiler.outputFileSystem = new Proxy(compiler.outputFileSystem, {
get(target, propKey) {
const orig = (target as any)[propKey]
if (propKey !== 'writeFile') {
return orig
}
return function(targetPath: string, content: Buffer, ...args: any[]) {
return orig.call(target, targetPath, contentFn(content), ...args)
}
},
})
}
export class ServerlessPlugin {
private buildId: string
private isServer: boolean
private isTrace: boolean
private isFlyingShuttle: boolean
constructor(
buildId: string,
{
isServer,
isTrace,
isFlyingShuttle,
}: { isServer: boolean; isTrace: boolean; isFlyingShuttle: boolean }
) {
this.buildId = buildId
this.isServer = isServer
this.isTrace = isTrace
this.isFlyingShuttle = isFlyingShuttle
}
apply(compiler: Compiler) {
if (!this.isServer) {
if (this.isFlyingShuttle) {
compiler.hooks.emit.tap('ServerlessPlugin', compilation => {
const assetNames = Object.keys(compilation.assets).filter(f =>
f.includes(this.buildId)
)
for (const name of assetNames) {
compilation.assets[
name
.replace(new RegExp(`${this.buildId}[\\/\\\\]`), 'client/')
.replace(/[.]js$/, `.${this.buildId}.js`)
] = compilation.assets[name]
}
})
}
return
}
interceptFileWrites(compiler, content =>
replaceInBuffer(content, NEXT_REPLACE_BUILD_ID, this.buildId)
)
if (!this.isTrace) {
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) {
connectChunkAndModule(chunk, module)
}
}
}
}
})
}
)
})
}
}
}