Skip to content

Commit affe0c0

Browse files
committed
chore: add initial support for standalone runner
fix: improve S3 request parsers
1 parent 067e019 commit affe0c0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2183
-1178
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jobs:
1414
registry-url: "https://registry.npmjs.org"
1515
- run: yarn install
1616
- run: yarn build
17+
- run: yarn test run
1718
- run: yarn publish
1819
env:
1920
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ yarn-error.log
1010
/express
1111
/router.d.ts
1212
/router.js
13-
TODO
13+
TODO
14+
/.aws_lambda

build.mjs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,31 @@ const compileDeclarations = () => {
1111
}
1212
};
1313
const external = ["esbuild", "archiver", "serve-static", "@smithy/eventstream-codec", "local-aws-sqs", "@aws-sdk/client-sqs", "ajv", "ajv-formats", "fast-xml-parser"];
14+
15+
/**
16+
* @type {import("esbuild").Plugin}
17+
*/
1418
const watchPlugin = {
1519
name: "watch-plugin",
1620
setup: (build) => {
21+
build.onResolve({ filter: /^\.\/standalone$/ }, (args) => {
22+
if (args.with.external == "true") {
23+
return {
24+
external: true,
25+
path: `${args.path}.mjs`,
26+
};
27+
}
28+
});
29+
1730
const format = build.initialOptions.format;
1831
build.onEnd(async (result) => {
1932
console.log("Build", format, new Date().toLocaleString());
33+
2034
compileDeclarations();
35+
36+
if (build.initialOptions.format == "esm") {
37+
execSync("chmod +x dist/cli.mjs");
38+
}
2139
});
2240
},
2341
};
@@ -38,7 +56,6 @@ const bundle = shouldWatch ? esbuild.context : esbuild.build;
3856
const buildIndex = bundle.bind(null, {
3957
...esBuildConfig,
4058
entryPoints: [
41-
"./src/server.ts",
4259
"./src/defineConfig.ts",
4360
"./src/lib/runtime/runners/node/index.ts",
4461
"./src/lambda/router.ts",
@@ -54,7 +71,8 @@ const buildRouterESM = bundle.bind(null, {
5471
...esBuildConfig,
5572
entryPoints: [
5673
"./src/index.ts",
57-
"./src/server.ts",
74+
"./src/standalone.ts",
75+
"./src/cli.ts",
5876
"./src/defineConfig.ts",
5977
"./src/lambda/router.ts",
6078
"./src/plugins/sns/index.ts",

package.json

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "serverless-aws-lambda",
3-
"version": "5.0.1",
4-
"description": "AWS Application Load Balancer and API Gateway - Lambda dev tool for Serverless. Allows Express synthax in handlers. Supports packaging, local invoking and offline ALB, APG, S3, SNS, SQS, DynamoDB Stream server mocking.",
3+
"version": "6.0.0-beta.1",
4+
"description": "AWS Application Load Balancer and API Gateway - Lambda dev tool. Supports packaging, local invoking and offline ALB, APG, S3, SNS, SQS, DynamoDB Stream server mocking.",
55
"author": "Inqnuam",
66
"license": "MIT",
77
"homepage": "https://github.com/inqnuam/serverless-aws-lambda",
@@ -37,11 +37,10 @@
3737
"import": "./dist/lambda/body-parser.mjs",
3838
"default": "./dist/lambda/body-parser.mjs"
3939
},
40-
"./server": {
41-
"types": "./dist/server.d.ts",
42-
"require": "./dist/server.js",
43-
"import": "./dist/server.mjs",
44-
"default": "./dist/server.mjs"
40+
"./standalone": {
41+
"types": "./dist/standalone.d.ts",
42+
"import": "./dist/standalone.mjs",
43+
"default": "./dist/standalone.mjs"
4544
},
4645
"./sns": {
4746
"types": "./dist/plugins/sns/index.d.ts",
@@ -62,10 +61,13 @@
6261
"default": "./dist/plugins/s3/index.mjs"
6362
}
6463
},
64+
"bin": {
65+
"aws-lambda": "dist/cli.mjs"
66+
},
6567
"dependencies": {
66-
"@aws-sdk/client-sqs": "^3.726.1",
68+
"@aws-sdk/client-sqs": "^3.741.0",
6769
"@smithy/eventstream-codec": "^4.0.1",
68-
"@types/serverless": "^3.12.22",
70+
"@types/serverless": "^3.12.26",
6971
"ajv": "^8.17.1",
7072
"ajv-formats": "^3.0.1",
7173
"archiver": "^5.3.1",
@@ -75,11 +77,14 @@
7577
"serve-static": "^1.16.2"
7678
},
7779
"devDependencies": {
80+
"@aws-sdk/client-lambda": "^3.741.0",
81+
"@aws-sdk/client-s3": "^3.741.0",
7882
"@types/archiver": "^5.3.2",
79-
"@types/node": "^14.14.31",
83+
"@types/node": "^22.13.1",
8084
"@types/serve-static": "^1.15.5",
85+
"prettier": "^3.4.2",
8186
"typescript": "^5.7.3",
82-
"vitest": "^2.1.8"
87+
"vitest": "^3.0.5"
8388
},
8489
"keywords": [
8590
"aws",

src/cli.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
#!/usr/bin/env node
2+
3+
import { parseArgs } from "node:util";
4+
import { readFile } from "node:fs/promises";
5+
import path from "node:path";
6+
import { pathToFileURL } from "node:url";
7+
import { run, type ILambdaFunction } from "./standalone" with { external: "true" };
8+
import { log } from "./lib/utils/colorize";
9+
10+
function printHelpAndExit() {
11+
log.setDebug(true);
12+
log.GREY("Usage example:");
13+
14+
console.log(`aws-lambda -p 3000 --debug --functions "src/lambdas/**/*.ts"\n`);
15+
16+
log.BR_BLUE("Options:");
17+
18+
for (const [optionName, value] of Object.entries(options)) {
19+
let printableName = optionName;
20+
21+
if (value.short) {
22+
printableName += `, -${value.short}`;
23+
}
24+
25+
let content = `\t\ttype: ${value.type}`;
26+
if (value.description) {
27+
content += `\n\t\tdescription: ${value.description}`;
28+
}
29+
30+
if ("default" in value) {
31+
content += `\n\t\tdefault: ${value.default}`;
32+
}
33+
if (value.example) {
34+
content += `\n\t\texample: ${value.example}`;
35+
}
36+
37+
content += "\n";
38+
39+
log.CYAN(`\t --${printableName}`);
40+
log.GREY(content);
41+
}
42+
43+
process.exit(0);
44+
}
45+
46+
function getNumberOrDefault(value: any, defaultValue: number) {
47+
if (!value || isNaN(value)) {
48+
return defaultValue;
49+
}
50+
51+
return Number(value);
52+
}
53+
54+
async function getFunctionsDefinitionFromFile(filePath?: string) {
55+
if (!filePath) {
56+
return;
57+
}
58+
59+
if (filePath.endsWith(".js")) {
60+
throw new Error("Only .json, .mjs and .cjs are supported for --definitions option.");
61+
}
62+
63+
if (filePath.endsWith(".json")) {
64+
const defs = JSON.parse(await readFile(filePath, "utf-8"));
65+
return defs.functions;
66+
}
67+
68+
if (filePath.endsWith(".mjs") || filePath.endsWith(".cjs")) {
69+
const modulePath = pathToFileURL(path.resolve(process.cwd(), filePath)).href;
70+
const mod = await import(modulePath);
71+
72+
return mod.functions;
73+
}
74+
}
75+
76+
async function getFromGlob(excludePattern: RegExp, handlerName: string, matchPattern?: string[]) {
77+
if (!matchPattern) {
78+
return;
79+
}
80+
81+
const majorNodeVersion = Number(process.versions.node.slice(0, process.versions.node.indexOf(".")));
82+
83+
if (majorNodeVersion < 22) {
84+
throw new Error("--functions option is only supported on Node22 and higher.");
85+
}
86+
87+
const { glob } = await import("node:fs/promises");
88+
89+
const handlers: Map<string, ILambdaFunction> = new Map();
90+
91+
for await (const entry of glob(matchPattern)) {
92+
if (entry.match(excludePattern)) {
93+
continue;
94+
}
95+
96+
const parent = path.basename(path.dirname(entry));
97+
const parsedPath = path.parse(entry);
98+
99+
let funcName: string;
100+
101+
if (parsedPath.name == "index") {
102+
if (!handlers.has(parent)) {
103+
funcName = parent;
104+
} else {
105+
funcName = entry.replaceAll(path.sep, "_");
106+
}
107+
} else {
108+
if (!handlers.has(parsedPath.name)) {
109+
funcName = parsedPath.name;
110+
} else if (!handlers.has(`${parent}_${parsedPath.name}`)) {
111+
funcName = `${parent}_${parsedPath.name}`;
112+
} else {
113+
funcName = entry.replaceAll(path.sep, "_");
114+
}
115+
}
116+
117+
handlers.set(funcName, {
118+
name: funcName,
119+
// @ts-ignore
120+
handler: entry.replace(parsedPath.ext, `.${handlerName}`),
121+
// @ts-ignore
122+
runtime: parsedPath.ext == ".py" ? "python3.7" : parsedPath.ext == ".rb" ? "ruby2.7" : `nodejs${majorNodeVersion}.x`,
123+
});
124+
}
125+
126+
return Array.from(handlers.values());
127+
}
128+
129+
function getDefaultEnvs(env: string[]) {
130+
const environment: Record<string, string> = {};
131+
132+
for (const s of env) {
133+
const [key, ...rawValue] = s.split("=");
134+
135+
environment[key] = rawValue.join("=");
136+
}
137+
138+
return environment;
139+
}
140+
141+
interface ICliOptions {
142+
type: "string" | "boolean";
143+
multiple?: boolean | undefined;
144+
short?: string | undefined;
145+
default?: string | boolean | string[] | boolean[] | undefined;
146+
description?: string;
147+
example?: string;
148+
}
149+
150+
const options: Record<string, ICliOptions> = {
151+
port: { type: "string", short: "p", default: "0", description: "Set server port." },
152+
debug: { type: "boolean", default: false, description: "Enable debug mode. When enabled aws-lambda will print usefull informations." },
153+
config: { type: "string", short: "c", description: "Path to 'defineConfig' file." },
154+
runtime: { type: "string", short: "r", description: "Set default runtime (ex: nodejs22.x, python3.7, ruby2.7 etc.)." },
155+
timeout: { type: "string", short: "t", default: "3", description: "Set default timeout." },
156+
definitions: { type: "string", short: "d", description: "Path to .json, .mjs, .cjs file with Lambda function definitions." },
157+
functions: { type: "string", short: "f", multiple: true, description: "Glob pattern to automatically find and define Lambda handlers." },
158+
exclude: { type: "string", short: "x", default: "\.(test|spec)\.", description: "RegExp string to exclude found enteries from --functions." },
159+
handlerName: { type: "string", default: "handler", description: "Handler function name. To be used with --functions." },
160+
env: {
161+
type: "string",
162+
short: "e",
163+
multiple: true,
164+
default: [],
165+
description: "Environment variables to be injected into Lambdas. All existing AWS_* are automatically injected.",
166+
example: "-e API_KEY=supersecret -e API_URL=https://website.com",
167+
},
168+
help: { type: "boolean", short: "h" },
169+
};
170+
171+
const { values } = parseArgs({
172+
strict: false as true,
173+
options,
174+
});
175+
176+
const { port, config, debug, help, runtime, definitions, timeout, functions, handlerName, exclude, env } = values;
177+
178+
if (help) {
179+
printHelpAndExit();
180+
}
181+
182+
if (definitions && functions) {
183+
throw new Error("Can not use --definitions (-d) and --functions (-f) together.");
184+
}
185+
186+
// @ts-ignore
187+
const functionDefs = functions ? await getFromGlob(new RegExp(exclude), handlerName, functions as string[]) : await getFunctionsDefinitionFromFile(definitions as string);
188+
189+
run({
190+
// @ts-ignore
191+
debug,
192+
// @ts-ignore
193+
configPath: config,
194+
port: getNumberOrDefault(port, 0),
195+
functions: functionDefs,
196+
defaults: {
197+
// @ts-ignore
198+
environment: getDefaultEnvs(env),
199+
// @ts-ignore
200+
runtime,
201+
timeout: getNumberOrDefault(timeout, 3),
202+
},
203+
});

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface Config {
2121
buildCallback?: (result: BuildResult, isRebuild: boolean) => Promise<void> | void;
2222
afterDeployCallbacks?: (() => Promise<void> | void)[];
2323
afterPackageCallbacks?: (() => Promise<void> | void)[];
24+
onKill?: (() => Promise<void> | void)[];
2425
}
2526

2627
export interface ServerConfig {

0 commit comments

Comments
 (0)