import { build , BuildResult , BuildOptions , Plugin } from 'esbuild' ; import * as fs from 'fs-extra' ; import * as globby from 'globby' ; import * as path from 'path' ; import * as pMap from 'p-map' ; import { concat , always , memoizeWith , mergeRight } from 'ramda' ; import * as Serverless from 'serverless' ; import * as ServerlessPlugin from 'serverless/classes/Plugin' ; import * as chokidar from 'chokidar' ; import { extractFileNames , providerRuntimeMatcher } from './helper' ; import { packExternalModules } from './pack-externals' ; import { pack } from './pack' ; import { preOffline } from './pre-offline' ; import { preLocal } from './pre-local' ; import { trimExtension } from './utils' ; import { BUILD_FOLDER , ONLY_PREFIX , SERVERLESS_FOLDER , WORK_FOLDER } from './constants' ; type Plugins = Plugin [ ] ; type ReturnPluginsFn = ( sls : Serverless ) => Plugins ; interface OptionsExtended extends Serverless . Options { verbose ? : boolean ; } export interface WatchConfiguration { pattern ? : string [ ] | string ; ignore ? : string [ ] | string ; } export interface PackagerOptions { scripts ? : string [ ] | string ; } export interface Configuration extends Omit < BuildOptions , 'nativeZip' | 'watch' | 'plugins' > { concurrency ? : number ; packager : 'npm' | 'yarn' ; packagePath : string ; exclude : '*' | string [ ] ; nativeZip : boolean ; watch : WatchConfiguration ; installExtraArgs : string [ ] ; plugins ? : string ; keepOutputDirectory ? : boolean ; packagerOptions ? : PackagerOptions ; disableIncremental ? : boolean ; } export interface FunctionBuildResult { result : BuildResult ; bundlePath : string ; func : Serverless . FunctionDefinitionHandler ; functionAlias : string ; } const DEFAULT_BUILD_OPTIONS = { bundle : true , target : 'node10' , external : [ ] , exclude : [ 'aws-sdk' ] , nativeZip : false , packager : 'npm' , installExtraArgs : [ ] , watch : { pattern : './**/*.(js|ts)' , ignore : [ WORK_FOLDER , 'dist' , 'node_modules' , SERVERLESS_FOLDER ] , } , keepOutputDirectory : false , packagerOptions : { } , } ; export class EsbuildServerlessPlugin implements ServerlessPlugin { serviceDirPath : string ; workDirPath : string ; buildDirPath : string ; serverless : Serverless ; options : OptionsExtended ; hooks : ServerlessPlugin . Hooks ; buildResults : FunctionBuildResult [ ] ; packExternalModules : ( ) => Promise < void > ; pack : ( ) => Promise < void > ; preOffline : ( ) => Promise < void > ; preLocal : ( ) => void ; constructor ( serverless , options ) { this . serverless = serverless ; this . options = options ; this . packExternalModules = packExternalModules . bind ( this ) ; this . pack = pack . bind ( this ) ; this . preOffline = preOffline . bind ( this ) ; this . preLocal = preLocal . bind ( this ) ; this . serviceDirPath = this . serverless . config . serviceDir || this . serverless . config . servicePath ; this . workDirPath = path . join ( this . serviceDirPath , WORK_FOLDER ) ; this . buildDirPath = path . join ( this . workDirPath , BUILD_FOLDER ) ; this . hooks = { 'before:run:run' : async ( ) => { await this . bundle ( ) ; await this . packExternalModules ( ) ; await this . copyExtras ( ) ; } , 'before:offline:start' : async ( ) => { await this . bundle ( true ) ; await this . packExternalModules ( ) ; await this . copyExtras ( ) ; await this . preOffline ( ) ; this . watch ( ) ; } , 'before:offline:start:init' : async ( ) => { await this . bundle ( true ) ; await this . packExternalModules ( ) ; await this . copyExtras ( ) ; await this . preOffline ( ) ; this . watch ( ) ; } , 'before:package:createDeploymentArtifacts' : async ( ) => { await this . bundle ( ) ; await this . packExternalModules ( ) ; await this . copyExtras ( ) ; await this . pack ( ) ; } , 'after:package:createDeploymentArtifacts' : async ( ) => { await this . cleanup ( ) ; } , 'before:deploy:function:packageFunction' : async ( ) => { await this . bundle ( ) ; await this . packExternalModules ( ) ; await this . copyExtras ( ) ; await this . pack ( ) ; } , 'after:deploy:function:packageFunction' : async ( ) => { await this . cleanup ( ) ; } , 'before:invoke:local:invoke' : async ( ) => { await this . bundle ( ) ; await this . packExternalModules ( ) ; await this . copyExtras ( ) ; await this . preLocal ( ) ; } , } ; } param { Serverless . FunctionDefinitionHandler } func returns { boolean } private isNodeFunction ( func ) { const runtime = func . runtime || this . serverless . service . provider . runtime ; const runtimeMatcher = providerRuntimeMatcher [ this . serverless . service . provider . name ] ; return Boolean ( runtimeMatcher ?. [ runtime ] ) ; } param { Serverless . FunctionDefinitionHandler | Serverless . FunctionDefinitionImage } func returns { boolean } private isFunctionDefinitionHandler ( func ) { return Boolean ( ( func as Serverless . FunctionDefinitionHandler ) ?. handler ) ; } get functions ( ) : Record < string , Serverless . FunctionDefinitionHandler > { const functions = this . options . function ? { [ this . options . function ] : this . serverless . service . getFunction ( this . options . function ) , } : this . serverless . service . functions ; const nodeFunctions = { } ; for ( const [ functionAlias , fn ] of Object . entries ( functions ) ) { if ( this . isFunctionDefinitionHandler ( fn ) && this . isNodeFunction ( fn ) ) { nodeFunctions [ functionAlias ] = fn ; } } return nodeFunctions ; } get plugins ( ) : Plugins { if ( ! this . buildOptions . plugins ) return ; const plugins = require ( path . join ( this . serviceDirPath , this . buildOptions . plugins ) ) ; if ( typeof plugins === 'function' ) { return plugins ( this . serverless ) ; } return plugins ; } private getCachedOptions = memoizeWith ( always ( 'cache' ) , ( ) => { const runtimeMatcher = providerRuntimeMatcher [ this . serverless . service . provider . name ] ; const target = runtimeMatcher ?. [ this . serverless . service . provider . runtime ] ; const resolvedOptions = { ... ( target ? { target } : { } ) , } ; const withDefaultOptions = mergeRight ( DEFAULT_BUILD_OPTIONS ) ; const withResolvedOptions = mergeRight ( withDefaultOptions ( resolvedOptions ) ) ; return withResolvedOptions < Configuration > ( this . serverless . service . custom ?. esbuild ?? { } ) ; } ) ; get buildOptions ( ) { return this . getCachedOptions ( ) ; } get rootFileNames ( ) { return extractFileNames ( this . serviceDirPath , this . serverless . service . provider . name , this . functions ) ; } async watch ( ) { const options = { ignored : this . buildOptions . watch . ignore , awaitWriteFinish : true , ignoreInitial : true , } ; chokidar . watch ( this . buildOptions . watch . pattern , options ) . on ( 'all' , ( ) => this . bundle ( true ) . then ( ( ) => this . serverless . cli . log ( 'Watching files for changes...' ) ) . catch ( ( ) => this . serverless . cli . log ( 'Bundle error, waiting for a file change to reload...' ) ) ) ; } prepare ( ) { fs . mkdirpSync ( this . buildDirPath ) ; fs . mkdirpSync ( path . join ( this . workDirPath , SERVERLESS_FOLDER ) ) ; this . serverless . service . package = { ... ( this . serverless . service . package || { } ) , patterns : [ ... new Set ( [ ... ( this . serverless . service . package ?. include || [ ] ) , ... ( this . serverless . service . package ?. exclude || [ ] ) . map ( concat ( '!' ) ) , ... ( this . serverless . service . package ?. patterns || [ ] ) , '!node_modules/serverless-esbuild' , ] ) , ] , } ; for ( const fn of Object . values ( this . functions ) ) { fn . package = { ... ( fn . package || { } ) , patterns : [ ... new Set ( [ ... ( fn . package ?. include || [ ] ) , ... ( fn . package ?. exclude || [ ] ) . map ( concat ( '!' ) ) , ... ( fn . package ?. patterns || [ ] ) , ] ) , ] , } ; } } async bundle ( incremental = false ) { this . prepare ( ) ; this . serverless . cli . log ( `Compiling to ${ this . buildOptions . target } bundle with esbuild...` ) ; if ( this . buildOptions . disableIncremental === true ) { incremental = false ; } const bundleMapper = async ( bundleInfo ) => { const { entry , func , functionAlias } = bundleInfo ; const config = { ... this . buildOptions , external : [ ... this . buildOptions . external , ... ( this . buildOptions . exclude === '*' || this . buildOptions . exclude . includes ( '*' ) ? [ ] : this . buildOptions . exclude ) , ] , entryPoints : [ entry ] , outdir : path . join ( this . buildDirPath , path . dirname ( entry ) ) , platform : 'node' , incremental , plugins : this . plugins , } ; delete config [ 'concurrency' ] ; delete config [ 'exclude' ] ; delete config [ 'nativeZip' ] ; delete config [ 'packager' ] ; delete config [ 'packagePath' ] ; delete config [ 'watch' ] ; delete config [ 'keepOutputDirectory' ] ; delete config [ 'packagerOptions' ] ; delete config [ 'installExtraArgs' ] ; delete config [ 'disableIncremental' ] ; const bundlePath = entry . substr ( 0 , entry . lastIndexOf ( '.' ) ) + '.js' ; if ( this . buildResults ) { const { result } = this . buildResults . find ( ( { func : fn } ) => fn . name === func . name ) ; if ( result . rebuild ) { await result . rebuild ( ) ; return { result , bundlePath , func , functionAlias } ; } } const result = await build ( config ) ; if ( config . metafile ) { fs . writeFileSync ( path . join ( this . buildDirPath , `${ trimExtension ( entry ) }-meta.json` ) , JSON . stringify ( result . metafile , null , 2 ) ) ; } return { result , bundlePath , func , functionAlias } ; } ; this . serverless . cli . log ( `Compiling with concurrency: ${ this . buildOptions . concurrency ?? 'Infinity' }` ) ; this . buildResults = await pMap ( this . rootFileNames , bundleMapper , { concurrency : this . buildOptions . concurrency , } ) ; this . serverless . cli . log ( 'Compiling completed.' ) ; return this . buildResults . map ( ( r ) => r . result ) ; } /** Link or copy extras such as node_modules or package.patterns definitions */ async copyExtras ( ) { const { service } = this . serverless ; if ( service . package . patterns . length > 0 ) { const files = await globby ( service . package . patterns ) ; for ( const filename of files ) { const destFileName = path . resolve ( path . join ( this . buildDirPath , filename ) ) ; const dirname = path . dirname ( destFileName ) ; if ( ! fs . existsSync ( dirname ) ) { fs . mkdirpSync ( dirname ) ; } if ( ! fs . existsSync ( destFileName ) ) { fs . copySync ( path . resolve ( filename ) , destFileName ) ; } } } for ( const [ functionAlias , fn ] of Object . entries ( this . functions ) ) { if ( fn . package . patterns . length === 0 ) { continue ; } const files = await globby ( fn . package . patterns ) ; for ( const filename of files ) { const destFileName = path . resolve ( path . join ( this . buildDirPath , `${ ONLY_PREFIX }${ functionAlias }` , filename ) ) ; const dirname = path . dirname ( destFileName ) ; if ( ! fs . existsSync ( dirname ) ) { fs . mkdirpSync ( dirname ) ; } if ( ! fs . existsSync ( destFileName ) ) { fs . copySync ( path . resolve ( filename ) , destFileName ) ; } } } } /**
     * Move built code to the serverless folder, taking into account individual
     * packaging preferences.
     */ async moveArtifacts ( ) { const { service } = this . serverless ; await fs . copy ( path . join ( this . workDirPath , SERVERLESS_FOLDER ) , path . join ( this . serviceDirPath , SERVERLESS_FOLDER ) ) ; if ( this . options . function ) { const fn = service . getFunction ( this . options . function ) ; fn . package . artifact = path . join ( this . serviceDirPath , SERVERLESS_FOLDER , path . basename ( fn . package . artifact ) ) ; return ; } if ( service . package . individually ) { const functionNames = service . getAllFunctions ( ) ; functionNames . forEach ( ( name ) => { service . getFunction ( name ) . package . artifact = path . join ( this . serviceDirPath , SERVERLESS_FOLDER , path . basename ( service . getFunction ( name ) . package . artifact ) ) ; } ) ; return ; } service . package . artifact = path . join ( this . serviceDirPath , SERVERLESS_FOLDER , path . basename ( service . package . artifact ) ) ; } async cleanup ( ) { await this . moveArtifacts ( ) ; if ( ! this . buildOptions . keepOutputDirectory ) { fs . removeSync ( path . join ( this . workDirPath ) ) ; } } } module . exports = EsbuildServerlessPlugin ;