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.
Files changed (3) hide show
  1. package/README.md +78 -0
  2. package/index.js +150 -0
  3. 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
+ }