Skip to content

Commit

Permalink
chore: add initial support for standalone runner
Browse files Browse the repository at this point in the history
fix: improve S3 request parsers
  • Loading branch information
Inqnuam committed Feb 6, 2025
1 parent 067e019 commit affe0c0
Show file tree
Hide file tree
Showing 43 changed files with 2,183 additions and 1,178 deletions.
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
registry-url: "https://registry.npmjs.org"
- run: yarn install
- run: yarn build
- run: yarn test run
- run: yarn publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ yarn-error.log
/express
/router.d.ts
/router.js
TODO
TODO
/.aws_lambda
22 changes: 20 additions & 2 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,31 @@ const compileDeclarations = () => {
}
};
const external = ["esbuild", "archiver", "serve-static", "@smithy/eventstream-codec", "local-aws-sqs", "@aws-sdk/client-sqs", "ajv", "ajv-formats", "fast-xml-parser"];

/**
* @type {import("esbuild").Plugin}
*/
const watchPlugin = {
name: "watch-plugin",
setup: (build) => {
build.onResolve({ filter: /^\.\/standalone$/ }, (args) => {
if (args.with.external == "true") {
return {
external: true,
path: `${args.path}.mjs`,
};
}
});

const format = build.initialOptions.format;
build.onEnd(async (result) => {
console.log("Build", format, new Date().toLocaleString());

compileDeclarations();

if (build.initialOptions.format == "esm") {
execSync("chmod +x dist/cli.mjs");
}
});
},
};
Expand All @@ -38,7 +56,6 @@ const bundle = shouldWatch ? esbuild.context : esbuild.build;
const buildIndex = bundle.bind(null, {
...esBuildConfig,
entryPoints: [
"./src/server.ts",
"./src/defineConfig.ts",
"./src/lib/runtime/runners/node/index.ts",
"./src/lambda/router.ts",
Expand All @@ -54,7 +71,8 @@ const buildRouterESM = bundle.bind(null, {
...esBuildConfig,
entryPoints: [
"./src/index.ts",
"./src/server.ts",
"./src/standalone.ts",
"./src/cli.ts",
"./src/defineConfig.ts",
"./src/lambda/router.ts",
"./src/plugins/sns/index.ts",
Expand Down
27 changes: 16 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "serverless-aws-lambda",
"version": "5.0.1",
"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.",
"version": "6.0.0-beta.1",
"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.",
"author": "Inqnuam",
"license": "MIT",
"homepage": "https://github.com/inqnuam/serverless-aws-lambda",
Expand Down Expand Up @@ -37,11 +37,10 @@
"import": "./dist/lambda/body-parser.mjs",
"default": "./dist/lambda/body-parser.mjs"
},
"./server": {
"types": "./dist/server.d.ts",
"require": "./dist/server.js",
"import": "./dist/server.mjs",
"default": "./dist/server.mjs"
"./standalone": {
"types": "./dist/standalone.d.ts",
"import": "./dist/standalone.mjs",
"default": "./dist/standalone.mjs"
},
"./sns": {
"types": "./dist/plugins/sns/index.d.ts",
Expand All @@ -62,10 +61,13 @@
"default": "./dist/plugins/s3/index.mjs"
}
},
"bin": {
"aws-lambda": "dist/cli.mjs"
},
"dependencies": {
"@aws-sdk/client-sqs": "^3.726.1",
"@aws-sdk/client-sqs": "^3.741.0",
"@smithy/eventstream-codec": "^4.0.1",
"@types/serverless": "^3.12.22",
"@types/serverless": "^3.12.26",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"archiver": "^5.3.1",
Expand All @@ -75,11 +77,14 @@
"serve-static": "^1.16.2"
},
"devDependencies": {
"@aws-sdk/client-lambda": "^3.741.0",
"@aws-sdk/client-s3": "^3.741.0",
"@types/archiver": "^5.3.2",
"@types/node": "^14.14.31",
"@types/node": "^22.13.1",
"@types/serve-static": "^1.15.5",
"prettier": "^3.4.2",
"typescript": "^5.7.3",
"vitest": "^2.1.8"
"vitest": "^3.0.5"
},
"keywords": [
"aws",
Expand Down
203 changes: 203 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#!/usr/bin/env node

import { parseArgs } from "node:util";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { run, type ILambdaFunction } from "./standalone" with { external: "true" };
import { log } from "./lib/utils/colorize";

function printHelpAndExit() {
log.setDebug(true);
log.GREY("Usage example:");

console.log(`aws-lambda -p 3000 --debug --functions "src/lambdas/**/*.ts"\n`);

log.BR_BLUE("Options:");

for (const [optionName, value] of Object.entries(options)) {
let printableName = optionName;

if (value.short) {
printableName += `, -${value.short}`;
}

let content = `\t\ttype: ${value.type}`;
if (value.description) {
content += `\n\t\tdescription: ${value.description}`;
}

if ("default" in value) {
content += `\n\t\tdefault: ${value.default}`;
}
if (value.example) {
content += `\n\t\texample: ${value.example}`;
}

content += "\n";

log.CYAN(`\t --${printableName}`);
log.GREY(content);
}

process.exit(0);
}

function getNumberOrDefault(value: any, defaultValue: number) {
if (!value || isNaN(value)) {
return defaultValue;
}

return Number(value);
}

async function getFunctionsDefinitionFromFile(filePath?: string) {
if (!filePath) {
return;
}

if (filePath.endsWith(".js")) {
throw new Error("Only .json, .mjs and .cjs are supported for --definitions option.");
}

if (filePath.endsWith(".json")) {
const defs = JSON.parse(await readFile(filePath, "utf-8"));
return defs.functions;
}

if (filePath.endsWith(".mjs") || filePath.endsWith(".cjs")) {
const modulePath = pathToFileURL(path.resolve(process.cwd(), filePath)).href;
const mod = await import(modulePath);

return mod.functions;
}
}

async function getFromGlob(excludePattern: RegExp, handlerName: string, matchPattern?: string[]) {
if (!matchPattern) {
return;
}

const majorNodeVersion = Number(process.versions.node.slice(0, process.versions.node.indexOf(".")));

if (majorNodeVersion < 22) {
throw new Error("--functions option is only supported on Node22 and higher.");
}

const { glob } = await import("node:fs/promises");

const handlers: Map<string, ILambdaFunction> = new Map();

for await (const entry of glob(matchPattern)) {
if (entry.match(excludePattern)) {
continue;
}

const parent = path.basename(path.dirname(entry));
const parsedPath = path.parse(entry);

let funcName: string;

if (parsedPath.name == "index") {
if (!handlers.has(parent)) {
funcName = parent;
} else {
funcName = entry.replaceAll(path.sep, "_");
}
} else {
if (!handlers.has(parsedPath.name)) {
funcName = parsedPath.name;
} else if (!handlers.has(`${parent}_${parsedPath.name}`)) {
funcName = `${parent}_${parsedPath.name}`;
} else {
funcName = entry.replaceAll(path.sep, "_");
}
}

handlers.set(funcName, {
name: funcName,
// @ts-ignore
handler: entry.replace(parsedPath.ext, `.${handlerName}`),
// @ts-ignore
runtime: parsedPath.ext == ".py" ? "python3.7" : parsedPath.ext == ".rb" ? "ruby2.7" : `nodejs${majorNodeVersion}.x`,
});
}

return Array.from(handlers.values());
}

function getDefaultEnvs(env: string[]) {
const environment: Record<string, string> = {};

for (const s of env) {
const [key, ...rawValue] = s.split("=");

environment[key] = rawValue.join("=");
}

return environment;
}

interface ICliOptions {
type: "string" | "boolean";
multiple?: boolean | undefined;
short?: string | undefined;
default?: string | boolean | string[] | boolean[] | undefined;
description?: string;
example?: string;
}

const options: Record<string, ICliOptions> = {
port: { type: "string", short: "p", default: "0", description: "Set server port." },
debug: { type: "boolean", default: false, description: "Enable debug mode. When enabled aws-lambda will print usefull informations." },
config: { type: "string", short: "c", description: "Path to 'defineConfig' file." },
runtime: { type: "string", short: "r", description: "Set default runtime (ex: nodejs22.x, python3.7, ruby2.7 etc.)." },
timeout: { type: "string", short: "t", default: "3", description: "Set default timeout." },
definitions: { type: "string", short: "d", description: "Path to .json, .mjs, .cjs file with Lambda function definitions." },
functions: { type: "string", short: "f", multiple: true, description: "Glob pattern to automatically find and define Lambda handlers." },
exclude: { type: "string", short: "x", default: "\.(test|spec)\.", description: "RegExp string to exclude found enteries from --functions." },
handlerName: { type: "string", default: "handler", description: "Handler function name. To be used with --functions." },
env: {
type: "string",
short: "e",
multiple: true,
default: [],
description: "Environment variables to be injected into Lambdas. All existing AWS_* are automatically injected.",
example: "-e API_KEY=supersecret -e API_URL=https://website.com",
},
help: { type: "boolean", short: "h" },
};

const { values } = parseArgs({
strict: false as true,
options,
});

const { port, config, debug, help, runtime, definitions, timeout, functions, handlerName, exclude, env } = values;

if (help) {
printHelpAndExit();
}

if (definitions && functions) {
throw new Error("Can not use --definitions (-d) and --functions (-f) together.");
}

// @ts-ignore
const functionDefs = functions ? await getFromGlob(new RegExp(exclude), handlerName, functions as string[]) : await getFunctionsDefinitionFromFile(definitions as string);

run({
// @ts-ignore
debug,
// @ts-ignore
configPath: config,
port: getNumberOrDefault(port, 0),
functions: functionDefs,
defaults: {
// @ts-ignore
environment: getDefaultEnvs(env),
// @ts-ignore
runtime,
timeout: getNumberOrDefault(timeout, 3),
},
});
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface Config {
buildCallback?: (result: BuildResult, isRebuild: boolean) => Promise<void> | void;
afterDeployCallbacks?: (() => Promise<void> | void)[];
afterPackageCallbacks?: (() => Promise<void> | void)[];
onKill?: (() => Promise<void> | void)[];
}

export interface ServerConfig {
Expand Down
Loading

0 comments on commit affe0c0

Please sign in to comment.