serverless-llrt-analyzer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/index.js +150 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# serverless-llrt-analyzer
|
|
2
|
+
|
|
3
|
+
Serverless Framework plugin to flip Lambda functions to [AWS LLRT](https://github.com/awslabs/llrt)
|
|
4
|
+
(QuickJS, ~10x faster cold starts) with **one line per function** — plus a
|
|
5
|
+
deploy-time compatibility guard so you can't ship an incompatible function.
|
|
6
|
+
|
|
7
|
+
Thin Serverless adapter over the [`@davidwells/llrt-analyzer`](../llrt-analyzer) core.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i -D serverless-llrt-analyzer # pulls in @davidwells/llrt-analyzer
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
plugins:
|
|
19
|
+
- serverless-esbuild # (your bundler, if any)
|
|
20
|
+
- serverless-llrt-analyzer
|
|
21
|
+
|
|
22
|
+
custom:
|
|
23
|
+
llrt:
|
|
24
|
+
layerArn: arn:aws:lambda:us-east-1:xxxx:layer:llrt-arm64:1 # required
|
|
25
|
+
verify: true # default true — fail the deploy on a real blocker
|
|
26
|
+
|
|
27
|
+
functions:
|
|
28
|
+
api: { handler: src/app.handler, llrt: true } # -> provided.al2023 + arm64 + LLRT layer
|
|
29
|
+
warmer: { handler: src/warmer.warm, llrt: true }
|
|
30
|
+
processImg: { handler: src/img.handler } # stays nodejs (mixed-runtime is fine)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Reverting is deleting the `llrt: true` line.
|
|
34
|
+
|
|
35
|
+
## What it does at package time
|
|
36
|
+
|
|
37
|
+
For each `llrt: true` function:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
api -> runtime: provided.al2023 architecture: arm64 layers: [ <llrt layer> ]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Unflagged functions are untouched.
|
|
44
|
+
|
|
45
|
+
## The guard (`custom.llrt.verify`, default on)
|
|
46
|
+
|
|
47
|
+
Before packaging, it runs `llrt-analyzer`'s **static tier** against each flagged
|
|
48
|
+
function and aborts the deploy on a fatal blocker:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Error: [llrt] function "api" is NOT LLRT-compatible:
|
|
52
|
+
- node:crypto.timingSafeEqual: LLRT's node:crypto does not export
|
|
53
|
+
"timingSafeEqual" — use a supported alternative or keep this on Node.
|
|
54
|
+
Remove `llrt: true` or fix the blocker(s). (set custom.llrt.verify:false to bypass)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For the definitive verdict (actually runs the handler under the LLRT binary and
|
|
58
|
+
diffs behavior), run the core in CI: `llrt-analyzer <service> --local`.
|
|
59
|
+
|
|
60
|
+
## Getting the layer ARN (one command)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
llrt-analyzer publish-layer --arch arm64 --region us-east-1 \
|
|
64
|
+
--ssm /my-svc/prod/llrt-layer-arn
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This downloads the pinned LLRT lambda bootstrap, publishes it as a layer, prints
|
|
68
|
+
the ARN, and (with `--ssm`) writes it to SSM — then reference it as
|
|
69
|
+
`custom.llrt.layerArn: ${ssm:/my-svc/${self:provider.stage}/llrt-layer-arn}`.
|
|
70
|
+
`llrt-analyzer init` prints the whole snippet to paste.
|
|
71
|
+
|
|
72
|
+
## Bundler wiring (automatic)
|
|
73
|
+
|
|
74
|
+
If you use `serverless-esbuild`, the plugin **auto-wires it** for LLRT: it marks
|
|
75
|
+
`@aws-sdk/*` / `@smithy/*` / `@aws-crypto/*` **external** (LLRT ships its own SDK)
|
|
76
|
+
and sets `format: esm`. These are safe for the Node functions too (the Lambda
|
|
77
|
+
Node runtimes also provide `@aws-sdk`), so `llrt: true` really is the whole
|
|
78
|
+
change. Opt out with `custom.llrt.autoEsbuild: false`.
|
package/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
/**
|
|
4
|
+
* serverless-llrt-analyzer — Serverless Framework plugin (Consumer 1 of the
|
|
5
|
+
* generic `@davidwells/llrt-analyzer` core).
|
|
6
|
+
*
|
|
7
|
+
* Declarative per-function opt-in: mark a function `llrt: true` and this plugin
|
|
8
|
+
* swaps it to the LLRT custom runtime at package time (provided.al2023 + arm64 +
|
|
9
|
+
* the LLRT bootstrap layer), leaving unflagged functions on Node (mixed-runtime
|
|
10
|
+
* services are first-class). Reverting is one line (remove the flag).
|
|
11
|
+
*
|
|
12
|
+
* It ALSO guards: when custom.llrt.verify is on, it runs the analyzer's static
|
|
13
|
+
* tier against each flagged function and fails the deploy if a fatal blocker is
|
|
14
|
+
* found (e.g. node:crypto.timingSafeEqual) — you can't accidentally ship an
|
|
15
|
+
* incompatible function to LLRT.
|
|
16
|
+
*
|
|
17
|
+
* plugins:
|
|
18
|
+
* - serverless-llrt-analyzer
|
|
19
|
+
* custom:
|
|
20
|
+
* llrt:
|
|
21
|
+
* layerArn: arn:aws:lambda:us-east-1:xxxx:layer:llrt-arm64:1 # required
|
|
22
|
+
* verify: true # optional: analyzer guard (default true)
|
|
23
|
+
* functions:
|
|
24
|
+
* api: { handler: src/app.handler, llrt: true }
|
|
25
|
+
* warmer: { handler: src/warmer.warm, llrt: true }
|
|
26
|
+
* processImg: { handler: src/img.handler } # stays node
|
|
27
|
+
*
|
|
28
|
+
* The heavy analysis lives in the `llrt-analyzer` package (a peer of this one);
|
|
29
|
+
* this plugin is a thin Serverless adapter over it.
|
|
30
|
+
*/
|
|
31
|
+
const path = require('path')
|
|
32
|
+
const fs = require('fs')
|
|
33
|
+
|
|
34
|
+
class LlrtServerlessPlugin {
|
|
35
|
+
/**
|
|
36
|
+
* @param {any} serverless
|
|
37
|
+
* @param {any} [options]
|
|
38
|
+
* @param {any} [io]
|
|
39
|
+
* @param {{ analyzeFunction?: Function }} [deps] test seam — inject the core
|
|
40
|
+
* analyzer so the guard is unit-testable without resolving/installing core.
|
|
41
|
+
*/
|
|
42
|
+
constructor(serverless, options, io, deps) {
|
|
43
|
+
this.serverless = serverless
|
|
44
|
+
this.options = options || {}
|
|
45
|
+
this.log = (io && io.log && io.log.notice) || ((m) => serverless.cli && serverless.cli.log(`[llrt] ${m}`))
|
|
46
|
+
this.injectedAnalyze = (deps && deps.analyzeFunction) || null
|
|
47
|
+
this.hooks = {
|
|
48
|
+
'before:package:createDeploymentArtifacts': () => this.apply(),
|
|
49
|
+
'before:deploy:function:packageFunction': () => this.apply(),
|
|
50
|
+
'before:package:function:package': () => this.apply(),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Resolve the core analyzer's analyzeFunction (injected in tests). */
|
|
55
|
+
analyzer() {
|
|
56
|
+
if (this.injectedAnalyze) return this.injectedAnalyze
|
|
57
|
+
// eslint-disable-next-line global-require
|
|
58
|
+
return require('@davidwells/llrt-analyzer').analyzeFunction
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
llrtConfig() {
|
|
62
|
+
const custom = (this.serverless.service && this.serverless.service.custom) || {}
|
|
63
|
+
return custom.llrt || {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
flaggedFunctions() {
|
|
67
|
+
const fns = (this.serverless.service && this.serverless.service.functions) || {}
|
|
68
|
+
return Object.entries(fns).filter(([, def]) => def && def.llrt === true)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async apply() {
|
|
72
|
+
if (this.__applied) return
|
|
73
|
+
this.__applied = true
|
|
74
|
+
const flagged = this.flaggedFunctions()
|
|
75
|
+
if (flagged.length === 0) return
|
|
76
|
+
const cfg = this.llrtConfig()
|
|
77
|
+
|
|
78
|
+
if (cfg.verify !== false) await this.guard(flagged)
|
|
79
|
+
|
|
80
|
+
for (const [name, def] of flagged) {
|
|
81
|
+
def.runtime = 'provided.al2023'
|
|
82
|
+
def.architecture = def.architecture || 'arm64'
|
|
83
|
+
if (cfg.layerArn) def.layers = uniq([...(def.layers || []), cfg.layerArn])
|
|
84
|
+
else this.log(`WARNING: function "${name}" is llrt:true but custom.llrt.layerArn is unset — package the LLRT bootstrap yourself.`)
|
|
85
|
+
this.log(`function "${name}" -> LLRT (provided.al2023, ${def.architecture})`)
|
|
86
|
+
}
|
|
87
|
+
// Signal the esbuild step (if any) that LLRT bundling rules apply.
|
|
88
|
+
const service = this.serverless.service
|
|
89
|
+
service.custom = service.custom || {}
|
|
90
|
+
service.custom.__llrtFunctions = flagged.map(([n]) => n)
|
|
91
|
+
|
|
92
|
+
if (cfg.autoEsbuild !== false) this.autoWireEsbuild()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Auto-wire serverless-esbuild for LLRT (idea #4): LLRT needs its handlers
|
|
97
|
+
* bundled as ESM with the AWS SDK left EXTERNAL (LLRT ships its own SDK), so
|
|
98
|
+
* the flip is genuinely one line. serverless-esbuild config is global (not
|
|
99
|
+
* per-function), but these settings are SAFE for the Node functions too — the
|
|
100
|
+
* Lambda Node runtimes also provide @aws-sdk, and ESM output is fine there.
|
|
101
|
+
* Escape hatch: custom.llrt.autoEsbuild: false.
|
|
102
|
+
*/
|
|
103
|
+
autoWireEsbuild() {
|
|
104
|
+
const custom = this.serverless.service.custom || {}
|
|
105
|
+
const eb = custom.esbuild
|
|
106
|
+
if (!eb || typeof eb !== 'object') return // serverless-esbuild not in use — nothing to wire
|
|
107
|
+
const wantExternal = ['@aws-sdk/*', '@smithy/*', '@aws-crypto/*']
|
|
108
|
+
const existing = Array.isArray(eb.external) ? eb.external : []
|
|
109
|
+
const added = wantExternal.filter((e) => !existing.includes(e))
|
|
110
|
+
if (added.length) eb.external = [...existing, ...added]
|
|
111
|
+
if (!eb.format) eb.format = 'esm'
|
|
112
|
+
if (added.length || eb.format === 'esm') {
|
|
113
|
+
this.log(`serverless-esbuild wired for LLRT (external += ${added.join(', ') || '(none)'}, format=${eb.format})`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Analyzer guard: fail the deploy on a fatal LLRT blocker. */
|
|
118
|
+
async guard(flagged) {
|
|
119
|
+
const analyzeFunction = this.analyzer()
|
|
120
|
+
const servicePath = this.serverless.config && this.serverless.config.servicePath ? this.serverless.config.servicePath : process.cwd()
|
|
121
|
+
for (const [name, def] of flagged) {
|
|
122
|
+
const entryPoint = this.resolveHandlerFile(servicePath, def.handler)
|
|
123
|
+
if (!entryPoint) continue
|
|
124
|
+
// eslint-disable-next-line no-await-in-loop
|
|
125
|
+
const v = await analyzeFunction({ entryPoint, projectDir: servicePath, fn: name, tiers: ['static'] })
|
|
126
|
+
const fatal = (v.blockers || []).filter((b) => b.fatal)
|
|
127
|
+
if (fatal.length) {
|
|
128
|
+
const lines = fatal.map((b) => ` - ${b.api}: ${b.fix}`).join('\n')
|
|
129
|
+
throw new Error(`[llrt] function "${name}" is NOT LLRT-compatible:\n${lines}\n Remove \`llrt: true\` or fix the blocker(s). (set custom.llrt.verify:false to bypass)`)
|
|
130
|
+
}
|
|
131
|
+
this.log(`function "${name}" passed the LLRT static guard`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
resolveHandlerFile(servicePath, handler) {
|
|
136
|
+
if (!handler || typeof handler !== 'string') return null
|
|
137
|
+
const fileRel = handler.slice(0, handler.lastIndexOf('.'))
|
|
138
|
+
for (const e of ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs']) {
|
|
139
|
+
const p = path.join(servicePath, fileRel + e)
|
|
140
|
+
if (fs.existsSync(p)) return p
|
|
141
|
+
}
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function uniq(arr) {
|
|
147
|
+
return [...new Set(arr)]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = LlrtServerlessPlugin
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "serverless-llrt-analyzer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Serverless Framework plugin to flip Lambda functions to AWS LLRT (llrt: true) for ~10x faster cold starts, with a deploy-time compatibility guard powered by llrt-analyzer.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "uvu . \".*\\.test\\.js$\""
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"serverless",
|
|
15
|
+
"serverless-plugin",
|
|
16
|
+
"llrt",
|
|
17
|
+
"aws-lambda",
|
|
18
|
+
"cold-start",
|
|
19
|
+
"provided.al2023"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/DavidWells/smart-ci.git",
|
|
24
|
+
"directory": "packages/serverless-llrt-analyzer"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/DavidWells/smart-ci/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/DavidWells/smart-ci/tree/master/packages/serverless-llrt-analyzer#readme",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"license": "ISC",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@davidwells/llrt-analyzer": "^0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"uvu": "^0.5.6"
|
|
42
|
+
}
|
|
43
|
+
}
|