Last active
March 15, 2023 04:10
-
-
Save rehanvdm/42d6ffa1d0dbd0283352b80182b28199 to your computer and use it in GitHub Desktop.
EsbuildFunction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
# The `EsbuildFunction` CDK component | |
Located at `stacks/constructs/EsbuildFunction.ts`. This component is using the | |
["unsafe"](https://joecreager.com/5-reasons-to-avoid-deasync-for-node-js/) `desync` [npm library](https://www.npmjs.com/package/deasync). | |
The TL;DR is that it is using an option in the V8 that is not really supported. That said this library has been around | |
from early Node versions and seems to have continued support. | |
We accept the risk for now, this is okay as we really want to the ability to run async code within the Bundling | |
function the CDK exposes for assets. The benefit of this is approach is that we no longer have to maintain a separate | |
build step before calling the CDK commands. | |
The alternative to this would be to place a build.ts file next to each lambda handler so by creating `src/backend/api-front/build.ts` | |
that does the building for each lambda function. Then from within the CDK bundler callback, call this build script | |
synchronously. Also consider that IF this breaks, it breaks at compile time, and there are workarounds like the one | |
mentioned above. We are never using `deasync` at runtime which would have been a big red flag for me. With all of this | |
in mind and with a backup strategy, the risk is minimized. | |
There are also some alternatives but none without drawbacks as well: | |
- https://www.npmjs.com/package/synchronous-promise | |
- https://github.com/sindresorhus/make-synchronous | |
- https://github.com/giuseppeg/styled-jsx-plugin-postcss/commit/52a30d9c12bdc39379e75f473860f7e92ce94c1b | |
*/ | |
import * as lambda from "aws-cdk-lib/aws-lambda"; | |
import path from "path"; | |
import * as cdk from "aws-cdk-lib"; | |
import {Construct} from "constructs"; | |
import * as esbuild from "esbuild"; | |
import {BundlingOptions} from "aws-cdk-lib"; | |
import fs from "fs"; | |
import {Plugin} from "esbuild"; | |
import deasync from "deasync"; | |
import {visualizer, TemplateType} from "esbuild-visualizer"; | |
import opn from "open"; | |
function getFiles(source: string) | |
{ | |
return fs.readdirSync(source, { withFileTypes: true }) | |
.filter(dirent => dirent.isFile()) | |
.map(dirent => dirent.name); | |
} | |
function esBuildPluginShrinkSourceMap(): Plugin | |
{ | |
//https://github.com/evanw/esbuild/issues/1685#issuecomment-944916409 | |
return { | |
name: 'excludeVendorFromSourceMap', | |
setup(build) { | |
build.onLoad({ filter: /node_modules/ }, args => { | |
if (args.path.endsWith('.js') && !args.path.endsWith('.json')) | |
return { | |
contents: fs.readFileSync(args.path, 'utf8') | |
+ '\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==', | |
loader: 'default', | |
} | |
else | |
return | |
}) | |
}, | |
} | |
} | |
export type EsbuildFunctionBundlerOptions = { | |
/** | |
* Defaults to cdk.out/esbuild-visualizer | |
*/ | |
outputDir?: string, | |
/** | |
* Defaults to "treemap" | |
*/ | |
template?: TemplateType, | |
/** | |
* Open the HTML file after bundling | |
*/ | |
open?: boolean | |
} | |
export type EsbuildFunctionProps = Omit<lambda.FunctionProps,"code"> & { | |
/** | |
* Path to TS file ending in .ts | |
*/ | |
entry: string, | |
/** | |
* The name of the exported handler function in the entry file | |
*/ | |
handler: string, | |
esbuildOptions: Omit<esbuild.BuildOptions, "entryPoints" | "outfile">, | |
bundleAnalyzer?: EsbuildFunctionBundlerOptions | |
}; | |
export class EsbuildFunction extends lambda.Function | |
{ | |
constructor(scope: Construct, id: string, props: EsbuildFunctionProps) | |
{ | |
const srcDir = path.dirname(props.entry); | |
const fileNameNoExtension = path.basename(props.entry, path.extname(props.entry)); | |
const newLambdaProps: lambda.FunctionProps = { | |
...props, | |
handler: `${fileNameNoExtension}.${props.handler}`, | |
code: lambda.Code.fromAsset(srcDir, { | |
assetHashType: cdk.AssetHashType.OUTPUT, | |
bundling: { | |
image: cdk.DockerImage.fromRegistry('local'),/* Does not exist will always use local below */ | |
local: { | |
tryBundle(outputDir: string, options: BundlingOptions): boolean | |
{ | |
let pathTs = props.entry; | |
let pathJs = path.join(outputDir, fileNameNoExtension +".js"); | |
let bundleAnalysisPath; | |
const timingLabel = " Bundled in"; | |
console.time(timingLabel); | |
let done = false; | |
let result: any; | |
esbuild.build({ | |
platform: 'node', | |
target: ["es2020"], | |
minify: true, | |
bundle: true, | |
keepNames: true, | |
sourcemap: 'linked', | |
entryPoints: [pathTs], | |
outfile: pathJs, | |
external: ["aws-sdk"], | |
logLevel: "warning", | |
metafile: true, | |
...props.esbuildOptions, | |
plugins: [ | |
...(props.esbuildOptions?.plugins || []), | |
esBuildPluginShrinkSourceMap(), | |
], | |
}).then(async resp => | |
{ | |
result = resp; | |
const bundlerDefaults: EsbuildFunctionBundlerOptions = { | |
outputDir: "cdk.out/esbuild-visualizer", | |
template: "treemap" | |
}; | |
/* Analyze Bundle */ | |
// fs.writeFileSync('meta.json', JSON.stringify(result.metafile)); | |
// let text = await esbuild.analyzeMetafile(result.metafile, {verbose: true, color: true}); | |
// console.log(text); | |
const htmlContent = await visualizer(result.metafile, { | |
title: props.functionName, | |
template: props.bundleAnalyzer?.template || bundlerDefaults.template!, | |
}); | |
const outputDir = props.bundleAnalyzer?.outputDir || bundlerDefaults.outputDir!; | |
const outputFile = path.join(outputDir, props.functionName! + ".html") | |
if(!fs.existsSync(outputDir)) | |
fs.mkdirSync(outputDir); | |
fs.writeFileSync(outputFile, htmlContent); | |
bundleAnalysisPath = path.resolve(outputFile) | |
if(props.bundleAnalyzer?.open) | |
await opn(outputFile); | |
}).catch(err => { | |
result = err; | |
}).finally(() => done = true); | |
deasync.loopWhile(() => !done); | |
// console.log(result); | |
if(result instanceof Error) | |
throw result; | |
const fileNames = getFiles(outputDir); | |
for(let file of fileNames) | |
{ | |
const stats = fs.statSync(path.join(outputDir, file)); | |
const fileSizeInMegabytes = (stats.size / (1024*1024)).toFixed(2); | |
console.log(" - "+file, fileSizeInMegabytes+"mb"); | |
} | |
console.timeEnd(timingLabel); | |
console.log(" Bundle analysis: " + bundleAnalysisPath); | |
return true; | |
} | |
} | |
} | |
}), | |
}; | |
super(scope, id, newLambdaProps); | |
} | |
} | |
=== USAGE === | |
const apiFrontLambda = new EsbuildFunction(this, name("lambda-api-front"), { | |
functionName: name("api-front"), | |
entry: path.join(__dirname, './src/backend/api-front/index.ts'), | |
handler: 'handler', | |
//pass anything here, even plugins, or omit | |
esbuildOptions: { | |
}, | |
//optional | |
bundleAnalyzer: { | |
open: true | |
}, | |
runtime: lambda.Runtime.NODEJS_16_X, | |
...defaultNodeJsFuncOpt, | |
memorySize: 1024, | |
environment: { | |
...defaultEnv, | |
}, | |
reservedConcurrentExecutions: 100, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment