mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
792 lines
24 KiB
JavaScript
792 lines
24 KiB
JavaScript
import path from 'path'
|
|
import { readFile, rm, writeFile, stat } from 'fs/promises'
|
|
import { createWriteStream, existsSync } from 'fs'
|
|
import * as esbuild from 'esbuild'
|
|
import utils from '@serverlessinc/sf-core/src/utils.js'
|
|
import archiver from 'archiver'
|
|
import { spawn } from 'child_process'
|
|
import _ from 'lodash'
|
|
import pLimit from 'p-limit'
|
|
import globby from 'globby'
|
|
import ServerlessError from '../../serverless-error.js'
|
|
const { log } = utils
|
|
|
|
const nodeRuntimeRe = /nodejs(?<version>\d+).x/
|
|
|
|
class Esbuild {
|
|
constructor(serverless, options) {
|
|
this.serverless = serverless
|
|
this.options = options || {}
|
|
this._functions = undefined
|
|
|
|
this.hooks = {
|
|
'before:dev-build:build': async () => {
|
|
if (await this._shouldRun('originalHandler')) {
|
|
await this._build('originalHandler')
|
|
}
|
|
},
|
|
'after:dev-build:build': async () => {},
|
|
'before:invoke:local:invoke': async () => {
|
|
if (await this._shouldRun()) {
|
|
await this._build()
|
|
this._setConfigForInvokeLocal()
|
|
}
|
|
},
|
|
'before:package:createDeploymentArtifacts': async () => {
|
|
if (await this._shouldRun()) {
|
|
await this._build()
|
|
await this._preparePackageJson()
|
|
await this._package()
|
|
}
|
|
},
|
|
'before:deploy:function:packageFunction': async () => {
|
|
if (await this._shouldRun()) {
|
|
await this._build()
|
|
await this._preparePackageJson()
|
|
await this._package()
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
async asyncInit() {
|
|
this._defineSchema()
|
|
}
|
|
|
|
_defineSchema() {
|
|
this.serverless.configSchemaHandler.defineBuildProperty('esbuild', {
|
|
anyOf: [
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
// The node modules that should not be bundled
|
|
external: { type: 'array', items: { type: 'string' } },
|
|
// These are node modules that should not be bundled but also not included in the package.json
|
|
exclude: { type: 'array', items: { type: 'string' } },
|
|
// The packages config, this can be set to override the behavior of external
|
|
packages: { type: 'string', enum: ['external'] },
|
|
// The concurrency to use for building functions. By default it will be set to the number of functions to build.
|
|
// Meaning that all functions will be built concurrently.
|
|
buildConcurrency: { type: 'number' },
|
|
// Whether to bundle or not. Default is true
|
|
bundle: { type: 'boolean' },
|
|
// Whether to minify or not. Default is false
|
|
minify: { type: 'boolean' },
|
|
// If set to a boolean, true, then framework uses external sourcemaps and enables it on functions by default.
|
|
sourcemap: {
|
|
anyOf: [
|
|
{ type: 'boolean' },
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
type: {
|
|
type: 'string',
|
|
enum: ['inline', 'linked', 'external'],
|
|
},
|
|
setNodeOptions: { type: 'boolean' },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{ type: 'boolean' },
|
|
],
|
|
})
|
|
}
|
|
|
|
async _shouldRun(handlerPropertyName = 'handler') {
|
|
const functions = await this.functions(handlerPropertyName)
|
|
return Object.keys(functions).length > 0
|
|
}
|
|
|
|
/**
|
|
* Get a record of functions that should be built by esbuild
|
|
*/
|
|
async functions(handlerPropertyName = 'handler') {
|
|
if (this._functions) {
|
|
return this._functions
|
|
}
|
|
|
|
const functions = this.options.function
|
|
? {
|
|
[this.options.function]: this.serverless.service.getFunction(
|
|
this.options.function,
|
|
),
|
|
}
|
|
: this.serverless.service.functions
|
|
|
|
const functionsToBuild = {}
|
|
|
|
for (const [alias, functionObject] of Object.entries(functions)) {
|
|
const shouldBuild = await this._shouldBuildFunction(
|
|
functionObject,
|
|
handlerPropertyName,
|
|
)
|
|
if (shouldBuild) {
|
|
functionsToBuild[alias] = functionObject
|
|
}
|
|
}
|
|
|
|
this._functions = functionsToBuild
|
|
|
|
return functionsToBuild
|
|
}
|
|
|
|
static WillEsBuildRun(
|
|
configFile,
|
|
serviceDir,
|
|
handlerPropertyName = 'handler',
|
|
) {
|
|
if (!configFile || configFile?.build?.esbuild === false) {
|
|
return false
|
|
}
|
|
|
|
const functions = configFile.functions || {}
|
|
|
|
const willRun = Object.entries(functions).some(([, functionObject]) => {
|
|
const functionHandler = functionObject[handlerPropertyName]
|
|
if (!functionHandler) {
|
|
return false
|
|
}
|
|
|
|
const runtime = functionObject.runtime || configFile.provider.runtime
|
|
if (!runtime || !runtime.startsWith('nodejs')) {
|
|
return false
|
|
}
|
|
|
|
if (configFile.build?.esbuild) {
|
|
return true
|
|
}
|
|
|
|
const functionName = path.extname(functionHandler).slice(1)
|
|
const handlerPath = functionHandler.replace(`.${functionName}`, '')
|
|
let parsedExtension = undefined
|
|
for (const extension of [
|
|
'.js',
|
|
'.ts',
|
|
'.cjs',
|
|
'.mjs',
|
|
'.cts',
|
|
'.mts',
|
|
'.jsx',
|
|
'.tsx',
|
|
]) {
|
|
if (existsSync(path.join(serviceDir, handlerPath + extension))) {
|
|
parsedExtension = extension
|
|
break
|
|
}
|
|
}
|
|
|
|
if (
|
|
parsedExtension &&
|
|
['.ts', '.cts', '.mts', '.tsx'].includes(parsedExtension)
|
|
) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
})
|
|
return willRun
|
|
}
|
|
|
|
/**
|
|
* Take a Function Configuration and determine if it should be built by esbuild
|
|
* @param {Object} functionObject - A Framework Function Configuration Object
|
|
* @returns
|
|
*/
|
|
async _shouldBuildFunction(functionObject, handlerPropertyName = 'handler') {
|
|
if (this.serverless.service.build?.esbuild === false) {
|
|
return false
|
|
}
|
|
// If handler isn't set then it is a docker function so do not attempt to build
|
|
if (!functionObject[handlerPropertyName]) {
|
|
return false
|
|
}
|
|
const runtime =
|
|
functionObject.runtime || this.serverless.service.provider.runtime
|
|
const functionBuildParam = functionObject.build
|
|
const providerBuildParam = this.serverless.service.build
|
|
|
|
// If runtime is not node then should not build
|
|
if (!runtime || !runtime.startsWith('nodejs')) {
|
|
return false
|
|
}
|
|
|
|
// If the build property is not set then we use the zero-config checking which is simply
|
|
// if the handler is a typescript file
|
|
if (!functionBuildParam && !providerBuildParam) {
|
|
log.debug(
|
|
'Build property not set using default checking behavior for esbuild',
|
|
)
|
|
const extension = await this._extensionForFunction(
|
|
functionObject[handlerPropertyName],
|
|
)
|
|
if (extension && ['.ts', '.cts', '.mts', '.tsx'].includes(extension)) {
|
|
log.debug('Build property not set using esbuild since typescript')
|
|
return true
|
|
}
|
|
}
|
|
|
|
// If the build property on the function config is defined and is set to esbuild then
|
|
// framework should build the function, otherwise if the build property is defined
|
|
// but not set to esbuild then it should not be built
|
|
if (functionBuildParam && functionBuildParam === 'esbuild') {
|
|
return true
|
|
} else if (functionBuildParam) {
|
|
return false
|
|
}
|
|
|
|
// If the provider build property is set to esbuild then build by default
|
|
if (
|
|
providerBuildParam &&
|
|
(providerBuildParam === 'esbuild' || providerBuildParam.esbuild)
|
|
) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// This is all the possible extensions that the esbuild plugin can build for
|
|
async _extensionForFunction(functionHandler) {
|
|
const functionName = path.extname(functionHandler).slice(1)
|
|
const handlerPath = functionHandler.replace(`.${functionName}`, '')
|
|
for (const extension of [
|
|
'.js',
|
|
'.ts',
|
|
'.cjs',
|
|
'.mjs',
|
|
'.cts',
|
|
'.mts',
|
|
'.jsx',
|
|
'.tsx',
|
|
]) {
|
|
if (
|
|
existsSync(
|
|
path.join(this.serverless.config.serviceDir, handlerPath + extension),
|
|
)
|
|
) {
|
|
return extension
|
|
}
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
_buildProperties() {
|
|
const defaultConfig = { bundle: true, minify: false, sourcemap: true }
|
|
if (
|
|
this.serverless.service.build &&
|
|
this.serverless.service.build !== 'esbuild' &&
|
|
this.serverless.service.build.esbuild
|
|
) {
|
|
const mergedOptions = _.merge(
|
|
defaultConfig,
|
|
this.serverless.service.build.esbuild,
|
|
)
|
|
if (this.serverless.service.build.esbuild.sourcemap === true) {
|
|
mergedOptions.sourcemap = true
|
|
} else if (this.serverless.service.build.esbuild.sourcemap === false) {
|
|
delete mergedOptions.sourcemap
|
|
} else if (this.serverless.service.build.esbuild?.sourcemap?.type) {
|
|
if (this.serverless.service.build.esbuild.sourcemap.type === 'linked') {
|
|
mergedOptions.sourcemap = true
|
|
} else {
|
|
mergedOptions.sourcemap =
|
|
this.serverless.service.build.esbuild.sourcemap.type
|
|
}
|
|
}
|
|
return mergedOptions
|
|
}
|
|
|
|
return defaultConfig
|
|
}
|
|
|
|
/**
|
|
* Determine which modules to mark as external (i.e. added to the generated package.json) and which modules to be excluded all together
|
|
* @param {string} runtime - The provider.runtime or functionObject.runtime value used to determine which version of the AWS SDK to exclude
|
|
* @returns
|
|
*/
|
|
_getExternal(runtime) {
|
|
const buildProperties = this._buildProperties()
|
|
let external = new Set(buildProperties.external || [])
|
|
let exclude = new Set(buildProperties.exclude || [])
|
|
if (buildProperties.excludes) {
|
|
external = [...external, ...buildProperties.excludes]
|
|
} else {
|
|
const nodeRuntimeMatch = runtime.match(nodeRuntimeRe)
|
|
if (nodeRuntimeMatch) {
|
|
const version = parseInt(nodeRuntimeMatch.groups.version) || 18
|
|
// If node version is 18 or greater then we need to exclude all @aws-sdk/ packages
|
|
if (version >= 18) {
|
|
external.add('@aws-sdk/*')
|
|
exclude.add('@aws-sdk/*')
|
|
} else {
|
|
external.add('aws-sdk')
|
|
exclude.add('aws-sdk')
|
|
}
|
|
}
|
|
}
|
|
return { external, exclude }
|
|
}
|
|
|
|
/**
|
|
* When invoking locally we need to set the servicePath to the build directory so that invoke local correctly uses the built function and does not
|
|
* attempt to use the typescript file directly.
|
|
*/
|
|
_setConfigForInvokeLocal() {
|
|
this.serverless.config.servicePath = path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Take the current build context. Which could be service-wide or a given function and then build it
|
|
* @param {string} handlerPropertyName - The property name of the handler in the function object. In the case of dev mode this will be different, so we need to be able to set it.
|
|
*/
|
|
async _build(handlerPropertyName = 'handler') {
|
|
const functionsToBuild = await this.functions(handlerPropertyName)
|
|
|
|
if (Object.keys(functionsToBuild).length === 0) {
|
|
log.debug('No functions to build with esbuild')
|
|
return
|
|
}
|
|
|
|
const updatedFunctionsToBuild = {}
|
|
|
|
const buildProperties = this._buildProperties()
|
|
|
|
for (const [alias, functionObject] of Object.entries(functionsToBuild)) {
|
|
const functionName = path
|
|
.extname(functionObject[handlerPropertyName])
|
|
.slice(1)
|
|
const handlerPath = functionObject[handlerPropertyName].replace(
|
|
`.${functionName}`,
|
|
'',
|
|
)
|
|
const runtime =
|
|
functionObject.runtime || this.serverless.service.provider.runtime
|
|
|
|
const external = Array.from(this._getExternal(runtime).external)
|
|
|
|
const extension = await this._extensionForFunction(
|
|
functionObject[handlerPropertyName],
|
|
)
|
|
if (extension) {
|
|
// Enrich the functionObject with additional values we will need for building
|
|
updatedFunctionsToBuild[alias] = {
|
|
...functionObject,
|
|
handlerPath: path.join(
|
|
this.serverless.config.serviceDir,
|
|
handlerPath + extension,
|
|
),
|
|
extension,
|
|
esbuild: { external },
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine the concurrency to use for building functions, by default framework will attempt to build
|
|
// all functions concurrently, but this can be overridden by setting the buildConcurrency property.
|
|
const concurrency =
|
|
buildProperties.buildConcurrency ?? Object.keys(functionsToBuild).length
|
|
|
|
const limit = pLimit(concurrency)
|
|
|
|
try {
|
|
await Promise.all(
|
|
Object.entries(updatedFunctionsToBuild).map(
|
|
([alias, functionObject]) => {
|
|
return limit(async () => {
|
|
const functionName = path
|
|
.extname(functionObject[handlerPropertyName])
|
|
.slice(1)
|
|
const handlerPath = functionObject[handlerPropertyName].replace(
|
|
`.${functionName}`,
|
|
'',
|
|
)
|
|
await esbuild.build({
|
|
...buildProperties,
|
|
platform: 'node',
|
|
...(buildProperties.bundle === true
|
|
? { external: functionObject.esbuild.external }
|
|
: { external: [] }),
|
|
entryPoints: [functionObject.handlerPath],
|
|
outfile: path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
handlerPath + '.js',
|
|
),
|
|
logLevel: 'error',
|
|
})
|
|
if (!this.serverless.builtFunctions) {
|
|
this.serverless.builtFunctions = new Set()
|
|
}
|
|
this.serverless.builtFunctions.add(alias)
|
|
if (
|
|
this.serverless.service.build?.esbuild?.sourcemap ===
|
|
undefined ||
|
|
this.serverless.service.build?.esbuild?.sourcemap === true ||
|
|
this.serverless.service.build?.esbuild.sourcemap
|
|
?.setNodeOptions === true
|
|
) {
|
|
const functionObject =
|
|
this.serverless.service.getFunction(alias)
|
|
if (functionObject.environment?.NODE_OPTIONS) {
|
|
functionObject.environment.NODE_OPTIONS = `${functionObject.environment.NODE_OPTIONS} --enable-source-maps`
|
|
} else {
|
|
if (!functionObject.environment) {
|
|
functionObject.environment = {}
|
|
}
|
|
functionObject.environment.NODE_OPTIONS =
|
|
'--enable-source-maps'
|
|
}
|
|
}
|
|
})
|
|
},
|
|
),
|
|
)
|
|
} catch (err) {
|
|
if (this.serverless.devmodeEnabled === true) {
|
|
return
|
|
}
|
|
throw new ServerlessError(err.message, 'ESBULD_BUILD_ERROR')
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
/**
|
|
* Take the current build context. Which could be service-wide or a given function and then package it.
|
|
*
|
|
* This function takes package.individually into account and will either create a single zip file to use for all functions or a zip file per function otherwise.
|
|
*
|
|
* @param {string} handlerPropertyName - The property name of the handler in the function object. In the case of dev mode this will be different, so we need to be able to set it.
|
|
*/
|
|
async _package(handlerPropertyName = 'handler') {
|
|
const functions = await this.functions(handlerPropertyName)
|
|
const buildProperties = this._buildProperties()
|
|
|
|
if (Object.keys(functions).length === 0) {
|
|
log.debug('No functions to package')
|
|
return
|
|
}
|
|
|
|
// If not packaging individually then package all functions together into a single zip
|
|
if (!this.serverless?.service?.package?.individually) {
|
|
await this._packageAll(functions, handlerPropertyName)
|
|
return
|
|
}
|
|
|
|
const concurrency =
|
|
buildProperties.buildConcurrency ?? Object.keys(functions).length
|
|
|
|
const limit = pLimit(concurrency)
|
|
|
|
const packageIncludes = await globby(
|
|
this.serverless.service.package?.patterns ?? [],
|
|
)
|
|
|
|
const zipPromises = Object.entries(functions).map(
|
|
([functionAlias, functionObject]) => {
|
|
return limit(async () => {
|
|
const zipName = `${this.serverless.service.service}-${functionAlias}.zip`
|
|
const zipPath = path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
zipName,
|
|
)
|
|
|
|
const zip = archiver.create('zip')
|
|
const output = createWriteStream(zipPath)
|
|
|
|
const zipPromise = new Promise(async (resolve, reject) => {
|
|
output.on('close', () => resolve(zipPath))
|
|
output.on('error', (err) => reject(err))
|
|
|
|
output.on('open', async () => {
|
|
const functionIncludes = await globby(
|
|
functionObject.package?.patterns ?? [],
|
|
)
|
|
|
|
const includesToPackage = _.union(
|
|
packageIncludes,
|
|
functionIncludes,
|
|
)
|
|
|
|
zip.pipe(output)
|
|
const functionName = path
|
|
.extname(functionObject[handlerPropertyName])
|
|
.slice(1)
|
|
const handlerPath = functionObject[handlerPropertyName].replace(
|
|
`.${functionName}`,
|
|
'',
|
|
)
|
|
|
|
const handlerZipPath = path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
handlerPath + '.js',
|
|
)
|
|
|
|
zip.file(handlerZipPath, { name: `${handlerPath}.js` })
|
|
if (existsSync(`${handlerZipPath}.map`)) {
|
|
zip.file(`${handlerZipPath}.map`, {
|
|
name: `${handlerPath}.js.map`,
|
|
})
|
|
}
|
|
|
|
zip.directory(
|
|
path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
'node_modules',
|
|
),
|
|
'node_modules',
|
|
)
|
|
|
|
await Promise.all(
|
|
includesToPackage.map(async (filePath) => {
|
|
const stats = await stat(filePath)
|
|
if (stats.isDirectory()) {
|
|
zip.directory(filePath, filePath)
|
|
} else {
|
|
zip.file(filePath, { name: filePath })
|
|
}
|
|
}),
|
|
)
|
|
|
|
await zip.finalize()
|
|
functionObject.package = {
|
|
artifact: zipPath,
|
|
}
|
|
})
|
|
})
|
|
await zipPromise
|
|
})
|
|
},
|
|
)
|
|
|
|
try {
|
|
await Promise.all(zipPromises)
|
|
} catch (err) {
|
|
throw new ServerlessError(err.message, 'ESBULD_PACKAGE_ERROR')
|
|
}
|
|
}
|
|
|
|
async _packageAll(functions, handlerPropertyName = 'handler') {
|
|
const zipName = `${this.serverless.service.service}.zip`
|
|
const zipPath = path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
zipName,
|
|
)
|
|
|
|
const packageIncludes = await globby(
|
|
this.serverless.service.package.patterns ?? [],
|
|
)
|
|
|
|
const zip = archiver.create('zip')
|
|
const output = createWriteStream(zipPath)
|
|
|
|
const zipPromise = new Promise(async (resolve, reject) => {
|
|
output.on('close', () => resolve(zipPath))
|
|
output.on('error', (err) => reject(err))
|
|
|
|
output.on('open', async () => {
|
|
zip.pipe(output)
|
|
|
|
for (const [, functionObject] of Object.entries(functions)) {
|
|
const functionName = path
|
|
.extname(functionObject[handlerPropertyName])
|
|
.slice(1)
|
|
const handlerPath = functionObject[handlerPropertyName].replace(
|
|
`.${functionName}`,
|
|
'',
|
|
)
|
|
|
|
const handlerZipPath = path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
handlerPath + '.js',
|
|
)
|
|
|
|
zip.file(handlerZipPath, { name: `${handlerPath}.js` })
|
|
if (existsSync(`${handlerZipPath}.map`)) {
|
|
zip.file(`${handlerZipPath}.map`, {
|
|
name: `${handlerPath}.js.map`,
|
|
})
|
|
}
|
|
}
|
|
|
|
zip.directory(
|
|
path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
'node_modules',
|
|
),
|
|
'node_modules',
|
|
)
|
|
|
|
await Promise.all(
|
|
packageIncludes.map(async (filePath) => {
|
|
const stats = await stat(filePath)
|
|
if (stats.isDirectory()) {
|
|
zip.directory(filePath, filePath)
|
|
} else {
|
|
zip.file(filePath, { name: filePath })
|
|
}
|
|
}),
|
|
)
|
|
|
|
await zip.finalize()
|
|
this.serverless.service.package.artifact = zipPath
|
|
})
|
|
})
|
|
|
|
try {
|
|
await zipPromise
|
|
} catch (err) {
|
|
throw new ServerlessError(err.message, 'ESBULD_PACKAGE_ALL_ERROR')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Take the package.json and add an updated version with no dev dependencies and external and excluded node_modules taken care of, to the .serverless/build directory
|
|
*/
|
|
async _preparePackageJson() {
|
|
const runtime = this.serverless.service.provider.runtime || 'nodejs18.x'
|
|
|
|
const { external, exclude } = this._getExternal(runtime)
|
|
|
|
const packageJsonPath = path.join(
|
|
this.serverless.config.serviceDir,
|
|
'package.json',
|
|
)
|
|
const packageJsonStr = await readFile(packageJsonPath, 'utf-8')
|
|
const packageJson = JSON.parse(packageJsonStr)
|
|
|
|
const packageJsonNoDevDeps = {
|
|
...packageJson,
|
|
}
|
|
delete packageJsonNoDevDeps.devDependencies
|
|
|
|
const buildProperties = this._buildProperties()
|
|
|
|
if (packageJson.dependencies) {
|
|
if (buildProperties.packages !== 'external') {
|
|
packageJsonNoDevDeps.dependencies = {}
|
|
for (const key of external) {
|
|
if (packageJson.dependencies[key]) {
|
|
packageJsonNoDevDeps.dependencies[key] =
|
|
packageJson.dependencies[key]
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const key of exclude) {
|
|
if (key === '@aws-sdk/*') {
|
|
const awsSdkPackages = Object.keys(
|
|
packageJsonNoDevDeps.dependencies,
|
|
).filter((dep) => dep.startsWith('@aws-sdk/'))
|
|
for (const awsSdkPackage of awsSdkPackages) {
|
|
delete packageJsonNoDevDeps.dependencies[awsSdkPackage]
|
|
}
|
|
} else {
|
|
delete packageJsonNoDevDeps.dependencies[key]
|
|
}
|
|
}
|
|
}
|
|
|
|
const packageJsonNoDevDepsStr = JSON.stringify(
|
|
packageJsonNoDevDeps,
|
|
null,
|
|
2,
|
|
)
|
|
|
|
const packageJsonBuildPath = path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
'package.json',
|
|
)
|
|
|
|
await writeFile(packageJsonBuildPath, packageJsonNoDevDepsStr)
|
|
|
|
const packager = this._determinePackager()
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const child = spawn(packager, ['install'], {
|
|
cwd: path.join(
|
|
this.serverless.config.serviceDir,
|
|
'.serverless',
|
|
'build',
|
|
),
|
|
shell: true,
|
|
})
|
|
child.on('error', (error) => {
|
|
log.error('Error installing dependencies', error)
|
|
reject(error)
|
|
})
|
|
|
|
child.on('close', (code) => {
|
|
resolve(code)
|
|
})
|
|
})
|
|
}
|
|
|
|
_determinePackager() {
|
|
if (existsSync(path.join(this.serverless.config.serviceDir, 'yarn.lock'))) {
|
|
return 'yarn'
|
|
} else if (
|
|
existsSync(path.join(this.serverless.config.serviceDir, 'pnpm-lock.yaml'))
|
|
) {
|
|
return 'pnpm'
|
|
} else {
|
|
return 'npm'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup, mainly removing build files and directories
|
|
*/
|
|
async _cleanUp() {
|
|
try {
|
|
await rm(
|
|
path.join(this.serverless.config.serviceDir, '.serverless', 'build'),
|
|
{
|
|
recursive: true,
|
|
force: true,
|
|
},
|
|
)
|
|
} catch (err) {
|
|
// empty error
|
|
}
|
|
}
|
|
|
|
async _useLocalEsbuild() {
|
|
const packageJsonPath = path.join(
|
|
this.serverless.serviceDir,
|
|
'package.json',
|
|
)
|
|
if (existsSync(packageJsonPath)) {
|
|
const packageJsonStr = await readFile(packageJsonPath, 'utf-8')
|
|
const packageJson = JSON.parse(packageJsonStr)
|
|
return Object.keys(packageJson?.devDependencies || {}).includes('esbuild')
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
export default Esbuild
|