feat: adds support for esbuild plugins (SC-2514) (#12662)

* feat: adds support for esbuild plugins

* chore: update docs

* chore: memoize build function
This commit is contained in:
Eslam λ Hefnawy 2024-07-10 07:35:04 -07:00 committed by GitHub
parent d2f09094d9
commit 2cb089e5c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 88 additions and 8 deletions

View File

@ -46,6 +46,49 @@ build:
setNodeOptions: true
```
You may also configure esbuild with a JavaScript file, which is useful if you want to use esbuild plugins. Here's an example:
```yml
build:
esbuild:
# Path to the esbuild config file relative to the `serverless.yml` file
configFile: ./esbuild.config.js
```
The JavaScript file must export a function that returns an esbuild configuration object. For your convenience, the **serverless** instance is passed to that function.
Here's an example of the `esbuild.config.js` file that uses the `esbuild-plugin-env` plugin:
**ESM:**
```js
/**
* don't forget to set the `"type": "module"` property in `package.json`
* and install the `esbuild-plugin-env` package
*/
import env from 'esbuild-plugin-env'
export default (serverless) => {
return {
external: ['@aws-sdk/client-s3'],
plugins: [env()],
}
}
```
**CommonJS:**
```js
const env = require('esbuild-plugin-env')
module.exports = (serverless) => {
return {
external: ['@aws-sdk/client-s3'],
plugins: [env()],
}
}
```
## Plugin Conflicts
Please note, plugins that build your code will not work unless you opt out of the default build experience. Some of the plugins affected are:

View File

@ -1,4 +1,5 @@
import path from 'path'
import { pathToFileURL } from 'url'
import { readFile, rm, writeFile, stat } from 'fs/promises'
import { createWriteStream, existsSync } from 'fs'
import * as esbuild from 'esbuild'
@ -19,6 +20,8 @@ class Esbuild {
this.options = options || {}
this._functions = undefined
this._buildProperties = _.memoize(this._buildProperties.bind(this))
this.hooks = {
'before:dev-build:build': async () => {
if (await this._shouldRun('originalHandler')) {
@ -273,17 +276,50 @@ class Esbuild {
return undefined
}
_buildProperties() {
async _buildProperties() {
const defaultConfig = { bundle: true, minify: false, sourcemap: true }
if (
this.serverless.service.build &&
this.serverless.service.build !== 'esbuild' &&
this.serverless.service.build.esbuild
) {
// For advanced use cases, users can provide a js file that exports a function that returns esbuild configuration options
// This is useful for when users want to use esbuild plugins (which require calling a function) or other advanced configurations
// That you can't really do in serverless.yml
let jsConfig = {}
if (this.serverless.service.build.esbuild.configFile) {
// Resolve the absolute path to the config file
const configFilePath = path.resolve(
this.serverless.config.serviceDir,
this.serverless.service.build.esbuild.configFile,
)
// This is a dynamic import because we want to support both CommonJS and ESM
const configFile = await import(pathToFileURL(configFilePath).href)
const configFunction = configFile.default || configFile
// Print a nice error message if the export is not a function
if (typeof configFunction !== 'function') {
throw new ServerlessError(
`Your build config "${path.basename(configFilePath)}" file must export a function that returns esbuild configuration options. For more details, please refer to the documentation: https://www.serverless.com/framework/docs/providers/aws/guide/building`,
'ESBUILD_CONFIG_ERROR',
)
}
// Passing the serverless instance can be useful
// Ref: https://github.com/floydspace/serverless-esbuild/issues/168
jsConfig = await configFunction(this.serverless)
}
// Users can use both serverless.yml and js file to configure esbuild
// The yml config will take precedence over js config
const mergedOptions = _.merge(
defaultConfig,
jsConfig,
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) {
@ -307,8 +343,8 @@ class Esbuild {
* @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()
async _getExternal(runtime) {
const buildProperties = await this._buildProperties()
let external = new Set(buildProperties.external || [])
let exclude = new Set(buildProperties.exclude || [])
if (buildProperties.excludes) {
@ -356,7 +392,7 @@ class Esbuild {
const updatedFunctionsToBuild = {}
const buildProperties = this._buildProperties()
const buildProperties = await this._buildProperties()
for (const [alias, functionObject] of Object.entries(functionsToBuild)) {
const functionName = path
@ -369,7 +405,7 @@ class Esbuild {
const runtime =
functionObject.runtime || this.serverless.service.provider.runtime
const external = Array.from(this._getExternal(runtime).external)
const external = Array.from((await this._getExternal(runtime)).external)
const extension = await this._extensionForFunction(
functionObject[handlerPropertyName],
@ -426,6 +462,7 @@ class Esbuild {
// Remove the following properties from the esbuildProps as they are not valid esbuild properties
delete esbuildProps.exclude
delete esbuildProps.buildConcurrency
delete esbuildProps.configFile
await esbuild.build(esbuildProps)
if (!this.serverless.builtFunctions) {
@ -474,7 +511,7 @@ class Esbuild {
*/
async _package(handlerPropertyName = 'handler') {
const functions = await this.functions(handlerPropertyName)
const buildProperties = this._buildProperties()
const buildProperties = await this._buildProperties()
if (Object.keys(functions).length === 0) {
log.debug('No functions to package')
@ -672,7 +709,7 @@ class Esbuild {
async _preparePackageJson() {
const runtime = this.serverless.service.provider.runtime || 'nodejs18.x'
const { external, exclude } = this._getExternal(runtime)
const { external, exclude } = await this._getExternal(runtime)
const packageJsonPath = path.join(
this.serverless.config.serviceDir,
@ -686,7 +723,7 @@ class Esbuild {
}
delete packageJsonNoDevDeps.devDependencies
const buildProperties = this._buildProperties()
const buildProperties = await this._buildProperties()
if (packageJson.dependencies) {
if (buildProperties.packages !== 'external') {