@actuallyjamez/elysian 0.2.1

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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Init command - Initialize a new elysian project
3
+ */
4
+ import { defineCommand } from "citty";
5
+ import consola from "consola";
6
+ import { existsSync, mkdirSync } from "fs";
7
+ import { join } from "path";
8
+ export const initCommand = defineCommand({
9
+ meta: {
10
+ name: "init",
11
+ description: "Initialize a new elysian project",
12
+ },
13
+ args: {
14
+ name: {
15
+ type: "string",
16
+ description: "API name",
17
+ default: "my-api",
18
+ },
19
+ force: {
20
+ type: "boolean",
21
+ description: "Overwrite existing files",
22
+ default: false,
23
+ },
24
+ },
25
+ async run({ args }) {
26
+ const cwd = process.cwd();
27
+ const apiName = args.name;
28
+ consola.start(`Initializing elysian project: ${apiName}`);
29
+ // Check for existing config
30
+ const configPath = join(cwd, "elysian.config.ts");
31
+ if (existsSync(configPath) && !args.force) {
32
+ consola.error("elysian.config.ts already exists. Use --force to overwrite.");
33
+ process.exit(1);
34
+ }
35
+ // Create directories
36
+ const lambdasDir = join(cwd, "src/lambdas");
37
+ const terraformDir = join(cwd, "terraform");
38
+ mkdirSync(lambdasDir, { recursive: true });
39
+ mkdirSync(terraformDir, { recursive: true });
40
+ consola.success("Created src/lambdas/");
41
+ consola.success("Created terraform/");
42
+ // Write config file
43
+ const configContent = `import { defineConfig } from "@actuallyjamez/elysian";
44
+
45
+ export default defineConfig({
46
+ apiName: "${apiName}",
47
+
48
+ // Lambda source directory
49
+ lambdasDir: "src/lambdas",
50
+
51
+ // Build output directory
52
+ outputDir: "dist",
53
+
54
+ // OpenAPI configuration
55
+ openapi: {
56
+ enabled: true,
57
+ title: "${apiName}",
58
+ version: "1.0.0",
59
+ description: "API powered by Elysia and AWS Lambda",
60
+ },
61
+
62
+ // Terraform configuration
63
+ terraform: {
64
+ outputDir: "terraform",
65
+ tfvarsFilename: "api-routes.auto.tfvars",
66
+ },
67
+
68
+ // Lambda defaults
69
+ lambda: {
70
+ runtime: "nodejs20.x",
71
+ memorySize: 256,
72
+ timeout: 30,
73
+ },
74
+ });
75
+ `;
76
+ await Bun.write(configPath, configContent);
77
+ consola.success("Created elysian.config.ts");
78
+ // Write example lambda
79
+ const exampleLambdaPath = join(lambdasDir, "hello.ts");
80
+ if (!existsSync(exampleLambdaPath) || args.force) {
81
+ const exampleLambdaContent = `import { createLambda, t } from "@actuallyjamez/elysian";
82
+
83
+ /**
84
+ * Example Lambda - Hello World
85
+ *
86
+ * Routes defined here will be automatically:
87
+ * - Bundled into a Lambda function
88
+ * - Mapped to API Gateway routes
89
+ * - Included in OpenAPI documentation
90
+ */
91
+ export default createLambda()
92
+ .get("/hello", ({ query }) => {
93
+ return \`Hello, \${query.name ?? "World"}!\`;
94
+ }, {
95
+ response: t.String(),
96
+ query: t.Object({
97
+ name: t.Optional(t.String()),
98
+ }),
99
+ detail: {
100
+ summary: "Say hello",
101
+ description: "Returns a greeting message",
102
+ tags: ["Greeting"],
103
+ },
104
+ });
105
+ `;
106
+ await Bun.write(exampleLambdaPath, exampleLambdaContent);
107
+ consola.success("Created src/lambdas/hello.ts");
108
+ }
109
+ // Write Terraform main.tf template
110
+ const terraformMainPath = join(terraformDir, "main.tf");
111
+ if (!existsSync(terraformMainPath) || args.force) {
112
+ const terraformContent = `terraform {
113
+ required_providers {
114
+ aws = {
115
+ source = "hashicorp/aws"
116
+ version = "~> 5.0"
117
+ }
118
+ }
119
+ }
120
+
121
+ provider "aws" {
122
+ region = var.region
123
+ }
124
+
125
+ variable "region" {
126
+ type = string
127
+ default = "eu-west-2"
128
+ }
129
+
130
+ variable "lambda_names" {
131
+ type = list(string)
132
+ default = []
133
+ }
134
+
135
+ variable "api_routes" {
136
+ type = map(object({
137
+ lambda_key = string
138
+ route_key = string
139
+ path_parameters = list(string)
140
+ }))
141
+ default = {}
142
+ }
143
+
144
+ variable "lambda_runtime" {
145
+ type = string
146
+ default = "nodejs20.x"
147
+ }
148
+
149
+ variable "lambda_memory_size" {
150
+ type = number
151
+ default = 256
152
+ }
153
+
154
+ variable "lambda_timeout" {
155
+ type = number
156
+ default = 30
157
+ }
158
+
159
+ variable "api_name" {
160
+ type = string
161
+ default = "${apiName}"
162
+ }
163
+
164
+ variable "tags" {
165
+ type = map(string)
166
+ default = {}
167
+ }
168
+
169
+ locals {
170
+ lambda_functions = {
171
+ for name in var.lambda_names : name => {
172
+ filename = "\${path.module}/../dist/\${name}.zip"
173
+ handler = "index.handler"
174
+ source_code_hash = filebase64sha256("\${path.module}/../dist/\${name}.zip")
175
+ }
176
+ }
177
+ }
178
+
179
+ # API Gateway
180
+ resource "aws_apigatewayv2_api" "this" {
181
+ name = var.api_name
182
+ protocol_type = "HTTP"
183
+ description = "API Gateway for \${var.api_name}"
184
+ tags = var.tags
185
+ }
186
+
187
+ # IAM Role for Lambda
188
+ resource "aws_iam_role" "lambda" {
189
+ name = "\${var.api_name}-lambda-role"
190
+
191
+ assume_role_policy = jsonencode({
192
+ Version = "2012-10-17"
193
+ Statement = [{
194
+ Action = "sts:AssumeRole"
195
+ Effect = "Allow"
196
+ Principal = {
197
+ Service = "lambda.amazonaws.com"
198
+ }
199
+ }]
200
+ })
201
+
202
+ tags = var.tags
203
+ }
204
+
205
+ resource "aws_iam_role_policy_attachment" "lambda_basic" {
206
+ role = aws_iam_role.lambda.name
207
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
208
+ }
209
+
210
+ # Lambda Functions
211
+ resource "aws_lambda_function" "this" {
212
+ for_each = local.lambda_functions
213
+
214
+ filename = each.value.filename
215
+ function_name = "\${var.api_name}-\${each.key}"
216
+ role = aws_iam_role.lambda.arn
217
+ handler = each.value.handler
218
+ source_code_hash = each.value.source_code_hash
219
+ runtime = var.lambda_runtime
220
+ memory_size = var.lambda_memory_size
221
+ timeout = var.lambda_timeout
222
+
223
+ tags = var.tags
224
+ }
225
+
226
+ # API Gateway Integrations
227
+ resource "aws_apigatewayv2_integration" "this" {
228
+ for_each = local.lambda_functions
229
+
230
+ api_id = aws_apigatewayv2_api.this.id
231
+ integration_type = "AWS_PROXY"
232
+ integration_uri = aws_lambda_function.this[each.key].invoke_arn
233
+ integration_method = "POST"
234
+ payload_format_version = "2.0"
235
+ }
236
+
237
+ # Lambda Permissions
238
+ resource "aws_lambda_permission" "apigateway" {
239
+ for_each = local.lambda_functions
240
+
241
+ statement_id = "AllowExecutionFromAPIGateway-\${each.key}"
242
+ action = "lambda:InvokeFunction"
243
+ function_name = aws_lambda_function.this[each.key].function_name
244
+ principal = "apigateway.amazonaws.com"
245
+ source_arn = "\${aws_apigatewayv2_api.this.execution_arn}/*/*"
246
+ }
247
+
248
+ # API Gateway Routes
249
+ resource "aws_apigatewayv2_route" "this" {
250
+ for_each = var.api_routes
251
+
252
+ api_id = aws_apigatewayv2_api.this.id
253
+ route_key = each.value.route_key
254
+ target = "integrations/\${aws_apigatewayv2_integration.this[each.value.lambda_key].id}"
255
+ }
256
+
257
+ # API Gateway Stage
258
+ resource "aws_apigatewayv2_stage" "this" {
259
+ api_id = aws_apigatewayv2_api.this.id
260
+ name = "$default"
261
+ auto_deploy = true
262
+
263
+ tags = var.tags
264
+
265
+ depends_on = [
266
+ aws_apigatewayv2_route.this,
267
+ aws_lambda_permission.apigateway
268
+ ]
269
+ }
270
+
271
+ # Outputs
272
+ output "api_endpoint" {
273
+ value = aws_apigatewayv2_stage.this.invoke_url
274
+ }
275
+
276
+ output "lambda_functions" {
277
+ value = { for k, v in aws_lambda_function.this : k => v.arn }
278
+ }
279
+ `;
280
+ await Bun.write(terraformMainPath, terraformContent);
281
+ consola.success("Created terraform/main.tf");
282
+ }
283
+ // Print next steps
284
+ console.log("");
285
+ consola.box("Project initialized!\n\n" +
286
+ "Next steps:\n\n" +
287
+ "1. Install dependencies:\n" +
288
+ " bun add elysia @actuallyjamez/elysian\n\n" +
289
+ "2. Add more lambdas in src/lambdas/\n\n" +
290
+ "3. Build your lambdas:\n" +
291
+ " bunx elysian build\n\n" +
292
+ "4. Deploy with Terraform:\n" +
293
+ " cd terraform && terraform init && terraform apply");
294
+ },
295
+ });
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * elysian CLI entry point
4
+ */
5
+ export {};
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * elysian CLI entry point
4
+ */
5
+ import { defineCommand, runMain } from "citty";
6
+ import { buildCommand } from "./commands/build";
7
+ import { devCommand } from "./commands/dev";
8
+ import { initCommand } from "./commands/init";
9
+ import { generateIacCommand } from "./commands/generate-iac";
10
+ import { version } from "../core/version";
11
+ const main = defineCommand({
12
+ meta: {
13
+ name: "elysian",
14
+ version,
15
+ description: "Automatic Lambda bundler for Elysia with API Gateway integration",
16
+ },
17
+ subCommands: {
18
+ build: buildCommand,
19
+ dev: devCommand,
20
+ init: initCommand,
21
+ "generate-iac": generateIacCommand,
22
+ },
23
+ });
24
+ runMain(main);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Lambda bundler using Bun.build
3
+ */
4
+ import type { ResolvedConfig } from "./config";
5
+ export interface BundleResult {
6
+ name: string;
7
+ outputPath: string;
8
+ success: boolean;
9
+ error?: string;
10
+ }
11
+ /**
12
+ * Bundle a single lambda file
13
+ */
14
+ export declare function bundleLambda(name: string, inputPath: string, outputDir: string, config: ResolvedConfig): Promise<BundleResult>;
15
+ /**
16
+ * Bundle all lambda files in a directory
17
+ */
18
+ export declare function bundleAllLambdas(lambdaFiles: string[], lambdasDir: string, outputDir: string, config: ResolvedConfig): Promise<BundleResult[]>;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Lambda bundler using Bun.build
3
+ */
4
+ import { build } from "bun";
5
+ import { join } from "path";
6
+ import { createHandlerWrapperPlugin } from "./handler-wrapper";
7
+ /**
8
+ * Bundle a single lambda file
9
+ */
10
+ export async function bundleLambda(name, inputPath, outputDir, config) {
11
+ try {
12
+ const result = await build({
13
+ entrypoints: [inputPath],
14
+ outdir: outputDir,
15
+ naming: `${name}.js`,
16
+ format: "esm",
17
+ target: "bun",
18
+ external: config.build.external,
19
+ sourcemap: config.build.sourcemap ? "external" : "none",
20
+ minify: config.build.minify,
21
+ plugins: [createHandlerWrapperPlugin()],
22
+ });
23
+ if (!result.success) {
24
+ const errors = result.logs
25
+ .filter((log) => log.level === "error")
26
+ .map((log) => log.message)
27
+ .join("\n");
28
+ return {
29
+ name,
30
+ outputPath: join(outputDir, `${name}.js`),
31
+ success: false,
32
+ error: errors || "Unknown build error",
33
+ };
34
+ }
35
+ return {
36
+ name,
37
+ outputPath: join(outputDir, `${name}.js`),
38
+ success: true,
39
+ };
40
+ }
41
+ catch (error) {
42
+ return {
43
+ name,
44
+ outputPath: join(outputDir, `${name}.js`),
45
+ success: false,
46
+ error: error instanceof Error ? error.message : String(error),
47
+ };
48
+ }
49
+ }
50
+ /**
51
+ * Bundle all lambda files in a directory
52
+ */
53
+ export async function bundleAllLambdas(lambdaFiles, lambdasDir, outputDir, config) {
54
+ const results = [];
55
+ for (const file of lambdaFiles) {
56
+ const name = file.replace(/\.ts$/, "");
57
+ const inputPath = join(lambdasDir, file);
58
+ const result = await bundleLambda(name, inputPath, outputDir, config);
59
+ results.push(result);
60
+ }
61
+ return results;
62
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Configuration types and loader for elysian
3
+ */
4
+ export interface OpenAPIConfig {
5
+ /** Enable OpenAPI auto-aggregation (default: true) */
6
+ enabled?: boolean;
7
+ /** API title for OpenAPI spec */
8
+ title?: string;
9
+ /** API version for OpenAPI spec */
10
+ version?: string;
11
+ /** API description for OpenAPI spec */
12
+ description?: string;
13
+ }
14
+ export interface BuildConfig {
15
+ /** Minify output (default: true in production) */
16
+ minify?: boolean;
17
+ /** Generate sourcemaps (default: true in development) */
18
+ sourcemap?: boolean;
19
+ /** External packages to exclude from bundle (default: ["@aws-sdk/*"]) */
20
+ external?: string[];
21
+ }
22
+ export interface LambdaConfig {
23
+ /** Lambda runtime (default: "nodejs20.x") */
24
+ runtime?: string;
25
+ /** Lambda memory size in MB (default: 256) */
26
+ memorySize?: number;
27
+ /** Lambda timeout in seconds (default: 30) */
28
+ timeout?: number;
29
+ }
30
+ export interface TerraformConfig {
31
+ /** Output directory for Terraform files (default: "terraform") */
32
+ outputDir?: string;
33
+ /** Name for the generated tfvars file (default: "api-routes.auto.tfvars") */
34
+ tfvarsFilename?: string;
35
+ }
36
+ export interface ElysianConfig {
37
+ /** Name of the API (used for resource naming) */
38
+ apiName: string;
39
+ /** Directory containing lambda files (default: "src/lambdas") */
40
+ lambdasDir?: string;
41
+ /** Output directory for built lambdas (default: "dist") */
42
+ outputDir?: string;
43
+ /** OpenAPI configuration */
44
+ openapi?: OpenAPIConfig;
45
+ /** Build configuration */
46
+ build?: BuildConfig;
47
+ /** Lambda defaults */
48
+ lambda?: LambdaConfig;
49
+ /** Terraform configuration */
50
+ terraform?: TerraformConfig;
51
+ }
52
+ export interface ResolvedConfig {
53
+ apiName: string;
54
+ lambdasDir: string;
55
+ outputDir: string;
56
+ openapi: Required<OpenAPIConfig>;
57
+ build: Required<BuildConfig>;
58
+ lambda: Required<LambdaConfig>;
59
+ terraform: Required<TerraformConfig>;
60
+ }
61
+ /**
62
+ * Define configuration with type safety and defaults
63
+ */
64
+ export declare function defineConfig(config: ElysianConfig): ElysianConfig;
65
+ /**
66
+ * Resolve configuration with defaults applied
67
+ */
68
+ export declare function resolveConfig(config: ElysianConfig): ResolvedConfig;
69
+ /**
70
+ * Load configuration from elysian.config.ts
71
+ */
72
+ export declare function loadConfig(cwd?: string): Promise<ResolvedConfig>;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Configuration types and loader for elysian
3
+ */
4
+ const DEFAULT_CONFIG = {
5
+ lambdasDir: "src/lambdas",
6
+ outputDir: "dist",
7
+ openapi: {
8
+ enabled: true,
9
+ title: "API",
10
+ version: "1.0.0",
11
+ description: "",
12
+ },
13
+ build: {
14
+ minify: process.env.NODE_ENV === "production",
15
+ sourcemap: process.env.NODE_ENV !== "production",
16
+ external: ["@aws-sdk/*"],
17
+ },
18
+ lambda: {
19
+ runtime: "nodejs20.x",
20
+ memorySize: 256,
21
+ timeout: 30,
22
+ },
23
+ terraform: {
24
+ outputDir: "terraform",
25
+ tfvarsFilename: "api-routes.auto.tfvars",
26
+ },
27
+ };
28
+ /**
29
+ * Define configuration with type safety and defaults
30
+ */
31
+ export function defineConfig(config) {
32
+ return config;
33
+ }
34
+ /**
35
+ * Resolve configuration with defaults applied
36
+ */
37
+ export function resolveConfig(config) {
38
+ return {
39
+ apiName: config.apiName,
40
+ lambdasDir: config.lambdasDir ?? DEFAULT_CONFIG.lambdasDir,
41
+ outputDir: config.outputDir ?? DEFAULT_CONFIG.outputDir,
42
+ openapi: {
43
+ enabled: config.openapi?.enabled ?? DEFAULT_CONFIG.openapi.enabled,
44
+ title: config.openapi?.title ?? DEFAULT_CONFIG.openapi.title,
45
+ version: config.openapi?.version ?? DEFAULT_CONFIG.openapi.version,
46
+ description: config.openapi?.description ?? DEFAULT_CONFIG.openapi.description,
47
+ },
48
+ build: {
49
+ minify: config.build?.minify ?? DEFAULT_CONFIG.build.minify,
50
+ sourcemap: config.build?.sourcemap ?? DEFAULT_CONFIG.build.sourcemap,
51
+ external: config.build?.external ?? DEFAULT_CONFIG.build.external,
52
+ },
53
+ lambda: {
54
+ runtime: config.lambda?.runtime ?? DEFAULT_CONFIG.lambda.runtime,
55
+ memorySize: config.lambda?.memorySize ?? DEFAULT_CONFIG.lambda.memorySize,
56
+ timeout: config.lambda?.timeout ?? DEFAULT_CONFIG.lambda.timeout,
57
+ },
58
+ terraform: {
59
+ outputDir: config.terraform?.outputDir ?? DEFAULT_CONFIG.terraform.outputDir,
60
+ tfvarsFilename: config.terraform?.tfvarsFilename ??
61
+ DEFAULT_CONFIG.terraform.tfvarsFilename,
62
+ },
63
+ };
64
+ }
65
+ /**
66
+ * Load configuration from elysian.config.ts
67
+ */
68
+ export async function loadConfig(cwd = process.cwd()) {
69
+ const configPath = `${cwd}/elysian.config.ts`;
70
+ try {
71
+ const configModule = await import(configPath);
72
+ const config = configModule.default;
73
+ if (!config.apiName) {
74
+ throw new Error("apiName is required in elysian.config.ts");
75
+ }
76
+ return resolveConfig(config);
77
+ }
78
+ catch (error) {
79
+ if (error.code === "ERR_MODULE_NOT_FOUND") {
80
+ throw new Error(`Configuration file not found: ${configPath}\nRun 'elysian init' to create one.`);
81
+ }
82
+ throw error;
83
+ }
84
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Bun plugin to inject Lambda handler wrapper for default exports
3
+ *
4
+ * This transforms:
5
+ * export default createLambda().get("/hello", ...)
6
+ *
7
+ * Into:
8
+ * const __elysia_route__ = createLambda().get("/hello", ...)
9
+ * export default __elysia_route__;
10
+ * export const handler = __createHandler(__elysia_route__);
11
+ */
12
+ import type { BunPlugin } from "bun";
13
+ /**
14
+ * Create a Bun plugin that wraps default Elysia exports with a Lambda handler
15
+ */
16
+ export declare function createHandlerWrapperPlugin(): BunPlugin;
17
+ /**
18
+ * Transform source code to add handler export
19
+ * This is called after the initial bundle to add the handler wrapper
20
+ */
21
+ export declare function wrapWithHandler(code: string): string;
22
+ /**
23
+ * Alternative approach: Transform the bundle output directly
24
+ * This rewrites the code to capture the default export and wrap it
25
+ */
26
+ export declare function transformBundleForLambda(code: string, lambdaName: string): string;
27
+ /**
28
+ * Create a wrapper entry file that imports and re-exports with handler
29
+ */
30
+ export declare function createWrapperEntry(originalPath: string): string;