@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.
- package/README.md +225 -0
- package/dist/cli/commands/build.d.ts +10 -0
- package/dist/cli/commands/build.js +199 -0
- package/dist/cli/commands/dev.d.ts +10 -0
- package/dist/cli/commands/dev.js +120 -0
- package/dist/cli/commands/generate-iac.d.ts +4 -0
- package/dist/cli/commands/generate-iac.js +83 -0
- package/dist/cli/commands/init.d.ts +15 -0
- package/dist/cli/commands/init.js +295 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +24 -0
- package/dist/core/bundler.d.ts +18 -0
- package/dist/core/bundler.js +62 -0
- package/dist/core/config.d.ts +72 -0
- package/dist/core/config.js +84 -0
- package/dist/core/handler-wrapper.d.ts +30 -0
- package/dist/core/handler-wrapper.js +143 -0
- package/dist/core/manifest.d.ts +43 -0
- package/dist/core/manifest.js +127 -0
- package/dist/core/naming.d.ts +12 -0
- package/dist/core/naming.js +20 -0
- package/dist/core/openapi.d.ts +23 -0
- package/dist/core/openapi.js +85 -0
- package/dist/core/packager.d.ts +17 -0
- package/dist/core/packager.js +54 -0
- package/dist/core/terraform.d.ts +13 -0
- package/dist/core/terraform.js +44 -0
- package/dist/core/version.d.ts +5 -0
- package/dist/core/version.js +30 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +58 -0
- package/dist/runtime/adapter.d.ts +32 -0
- package/dist/runtime/adapter.js +29 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.js +7 -0
- package/dist/runtime/types.d.ts +5 -0
- package/dist/runtime/types.js +4 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# elysian
|
|
2
|
+
|
|
3
|
+
Automatic Lambda bundler for [Elysia](https://elysiajs.com/) with AWS API Gateway and Terraform integration.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Zero-config Lambda handlers** - Just export your Elysia routes as default, handlers are auto-generated
|
|
8
|
+
- **Automatic OpenAPI aggregation** - All routes are aggregated into a single OpenAPI spec endpoint
|
|
9
|
+
- **Terraform integration** - Generates `tfvars` files for seamless infrastructure deployment
|
|
10
|
+
- **Type-safe configuration** - Full TypeScript support with `defineConfig()`
|
|
11
|
+
- **Watch mode** - Fast rebuilds during development
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Configure GitHub registry for @actuallyjamez scope
|
|
17
|
+
echo "@actuallyjamez:registry=https://npm.pkg.github.com" >> .npmrc
|
|
18
|
+
|
|
19
|
+
# Install
|
|
20
|
+
bun add elysia @actuallyjamez/elysian
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Initialize your project
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bunx @actuallyjamez/elysian init --name my-api
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This creates:
|
|
32
|
+
- `elysian.config.ts` - Configuration file
|
|
33
|
+
- `src/lambdas/hello.ts` - Example lambda
|
|
34
|
+
- `terraform/main.tf` - Terraform infrastructure
|
|
35
|
+
|
|
36
|
+
### 2. Write your lambdas
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// src/lambdas/users.ts
|
|
40
|
+
import { createLambda, t } from "@actuallyjamez/elysian";
|
|
41
|
+
|
|
42
|
+
export default createLambda()
|
|
43
|
+
.get("/users", () => db.getUsers(), {
|
|
44
|
+
response: t.Array(t.Object({ id: t.String(), name: t.String() })),
|
|
45
|
+
detail: { summary: "List all users", tags: ["Users"] },
|
|
46
|
+
})
|
|
47
|
+
.get("/users/:id", ({ params }) => db.getUser(params.id), {
|
|
48
|
+
params: t.Object({ id: t.String() }),
|
|
49
|
+
detail: { summary: "Get user by ID", tags: ["Users"] },
|
|
50
|
+
})
|
|
51
|
+
.post("/users", ({ body }) => db.createUser(body), {
|
|
52
|
+
body: t.Object({ name: t.String(), email: t.String() }),
|
|
53
|
+
detail: { summary: "Create user", tags: ["Users"] },
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**That's it!** No need to export a handler - the bundler wraps your default export automatically.
|
|
58
|
+
|
|
59
|
+
### 3. Build
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Development build
|
|
63
|
+
bunx elysian build
|
|
64
|
+
|
|
65
|
+
# Production build (minified)
|
|
66
|
+
bunx elysian build --prod
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Deploy
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cd terraform
|
|
73
|
+
terraform init
|
|
74
|
+
terraform apply
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
Create `elysian.config.ts` in your project root:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { defineConfig } from "@actuallyjamez/elysian";
|
|
83
|
+
|
|
84
|
+
export default defineConfig({
|
|
85
|
+
// Required
|
|
86
|
+
apiName: "my-api",
|
|
87
|
+
|
|
88
|
+
// Optional (showing defaults)
|
|
89
|
+
lambdasDir: "src/lambdas",
|
|
90
|
+
outputDir: "dist",
|
|
91
|
+
|
|
92
|
+
// OpenAPI configuration
|
|
93
|
+
openapi: {
|
|
94
|
+
enabled: true,
|
|
95
|
+
title: "My API",
|
|
96
|
+
version: "1.0.0",
|
|
97
|
+
description: "API description",
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// Terraform output configuration
|
|
101
|
+
terraform: {
|
|
102
|
+
outputDir: "terraform",
|
|
103
|
+
tfvarsFilename: "api-routes.auto.tfvars", // Won't overwrite your existing tfvars
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Lambda defaults (used in generated tfvars)
|
|
107
|
+
lambda: {
|
|
108
|
+
runtime: "nodejs20.x",
|
|
109
|
+
memorySize: 256,
|
|
110
|
+
timeout: 30,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## CLI Commands
|
|
116
|
+
|
|
117
|
+
### `elysian build`
|
|
118
|
+
|
|
119
|
+
Build all lambdas for deployment.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
bunx elysian build # Development build
|
|
123
|
+
bunx elysian build --prod # Production build (minified)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Output:**
|
|
127
|
+
- `dist/*.js` - Bundled lambda code
|
|
128
|
+
- `dist/*.zip` - Lambda deployment packages
|
|
129
|
+
- `dist/manifest.json` - Route manifest (for debugging)
|
|
130
|
+
- `terraform/api-routes.auto.tfvars` - Terraform variables
|
|
131
|
+
|
|
132
|
+
### `elysian dev`
|
|
133
|
+
|
|
134
|
+
Watch mode for development - rebuilds on file changes.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bunx elysian dev # Watch with packaging
|
|
138
|
+
bunx elysian dev --no-package # Skip zip creation (faster)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `elysian init`
|
|
142
|
+
|
|
143
|
+
Initialize a new project with example files.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
bunx elysian init --name my-api
|
|
147
|
+
bunx elysian init --force # Overwrite existing files
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `elysian generate-iac`
|
|
151
|
+
|
|
152
|
+
Regenerate Terraform files without rebuilding lambdas.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bunx elysian generate-iac
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## How It Works
|
|
159
|
+
|
|
160
|
+
### 1. Route Discovery
|
|
161
|
+
|
|
162
|
+
The bundler scans your `lambdasDir` for `.ts` files. Each file becomes a separate Lambda function.
|
|
163
|
+
|
|
164
|
+
### 2. Handler Injection
|
|
165
|
+
|
|
166
|
+
When you export an Elysia app as default:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
export default createLambda().get("/hello", () => "Hello!");
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The bundler automatically wraps it with a Lambda handler:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Hono } from "hono/tiny";
|
|
176
|
+
import { handle } from "hono/aws-lambda";
|
|
177
|
+
|
|
178
|
+
const app = /* your exported app */;
|
|
179
|
+
export const handler = handle(new Hono().mount("/", app.fetch));
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 3. OpenAPI Aggregation
|
|
183
|
+
|
|
184
|
+
An `__openapi__` lambda is automatically generated that imports all your routes and exposes:
|
|
185
|
+
- `GET /openapi` - Swagger UI
|
|
186
|
+
- `GET /openapi/json` - OpenAPI JSON spec
|
|
187
|
+
|
|
188
|
+
### 4. Terraform Integration
|
|
189
|
+
|
|
190
|
+
The generated `tfvars` file contains:
|
|
191
|
+
- List of Lambda names
|
|
192
|
+
- Route-to-Lambda mappings (with API Gateway path format)
|
|
193
|
+
- Lambda configuration defaults
|
|
194
|
+
|
|
195
|
+
## Project Structure
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
my-api/
|
|
199
|
+
├── elysian.config.ts # Configuration
|
|
200
|
+
├── src/
|
|
201
|
+
│ └── lambdas/
|
|
202
|
+
│ ├── users.ts # → users.zip Lambda
|
|
203
|
+
│ ├── posts.ts # → posts.zip Lambda
|
|
204
|
+
│ └── auth.ts # → auth.zip Lambda
|
|
205
|
+
├── dist/ # Build output
|
|
206
|
+
│ ├── users.js
|
|
207
|
+
│ ├── users.zip
|
|
208
|
+
│ ├── posts.js
|
|
209
|
+
│ ├── posts.zip
|
|
210
|
+
│ ├── __openapi__.js # Auto-generated
|
|
211
|
+
│ ├── __openapi__.zip
|
|
212
|
+
│ └── manifest.json
|
|
213
|
+
└── terraform/
|
|
214
|
+
├── main.tf
|
|
215
|
+
└── api-routes.auto.tfvars # Auto-generated
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Requirements
|
|
219
|
+
|
|
220
|
+
- [Bun](https://bun.sh/) runtime
|
|
221
|
+
- [Elysia](https://elysiajs.com/) v1.0+
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build command - Production build of all lambdas
|
|
3
|
+
*/
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { readdirSync, mkdirSync, existsSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
import { loadConfig } from "../../core/config";
|
|
9
|
+
import { bundleLambda } from "../../core/bundler";
|
|
10
|
+
import { packageLambda } from "../../core/packager";
|
|
11
|
+
import { generateManifest, writeManifest } from "../../core/manifest";
|
|
12
|
+
import { writeTerraformVars } from "../../core/terraform";
|
|
13
|
+
import { shouldGenerateOpenApi, writeOpenApiLambda, cleanupOpenApiLambda, } from "../../core/openapi";
|
|
14
|
+
import { createWrapperEntry } from "../../core/handler-wrapper";
|
|
15
|
+
import { getLambdaBundleName } from "../../core/naming";
|
|
16
|
+
import { version } from "../../core/version";
|
|
17
|
+
function formatDuration(ms) {
|
|
18
|
+
if (ms < 1000)
|
|
19
|
+
return `${ms}ms`;
|
|
20
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
21
|
+
}
|
|
22
|
+
function formatSize(bytes) {
|
|
23
|
+
if (bytes < 1024)
|
|
24
|
+
return `${bytes} B`;
|
|
25
|
+
if (bytes < 1024 * 1024)
|
|
26
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
27
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
28
|
+
}
|
|
29
|
+
export const buildCommand = defineCommand({
|
|
30
|
+
meta: {
|
|
31
|
+
name: "build",
|
|
32
|
+
description: "Build all lambdas for production deployment",
|
|
33
|
+
},
|
|
34
|
+
args: {
|
|
35
|
+
prod: {
|
|
36
|
+
type: "boolean",
|
|
37
|
+
description: "Production build (minify, no sourcemaps)",
|
|
38
|
+
default: false,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
async run({ args }) {
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
// Set production mode if flag is set
|
|
44
|
+
if (args.prod) {
|
|
45
|
+
process.env.NODE_ENV = "production";
|
|
46
|
+
}
|
|
47
|
+
// Header
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(` ${pc.bold(pc.cyan("elysian"))} ${pc.dim(`v${version}`)} ${args.prod ? pc.yellow("production") : pc.dim("development")}`);
|
|
50
|
+
console.log();
|
|
51
|
+
// Load config
|
|
52
|
+
let config;
|
|
53
|
+
try {
|
|
54
|
+
config = await loadConfig();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.log(` ${pc.red("✗")} ${error instanceof Error ? error.message : error}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const apiName = config.apiName;
|
|
61
|
+
const lambdasDir = join(process.cwd(), config.lambdasDir);
|
|
62
|
+
const outputDir = join(process.cwd(), config.outputDir);
|
|
63
|
+
const terraformDir = join(process.cwd(), config.terraform.outputDir);
|
|
64
|
+
// Ensure directories exist
|
|
65
|
+
if (!existsSync(lambdasDir)) {
|
|
66
|
+
console.log(` ${pc.red("✗")} Lambdas directory not found: ${lambdasDir}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
mkdirSync(outputDir, { recursive: true });
|
|
70
|
+
mkdirSync(terraformDir, { recursive: true });
|
|
71
|
+
// Get lambda files
|
|
72
|
+
let lambdaFiles = readdirSync(lambdasDir).filter((f) => f.endsWith(".ts") && !f.startsWith("__"));
|
|
73
|
+
if (lambdaFiles.length === 0) {
|
|
74
|
+
console.log(` ${pc.yellow("!")} No lambda files found in ${config.lambdasDir}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Generate OpenAPI aggregator if enabled
|
|
78
|
+
if (shouldGenerateOpenApi(config)) {
|
|
79
|
+
await writeOpenApiLambda(lambdaFiles, lambdasDir, config);
|
|
80
|
+
lambdaFiles.push("__openapi__.ts");
|
|
81
|
+
}
|
|
82
|
+
// Build phase
|
|
83
|
+
console.log(` ${pc.green("✓")} Compiling ${lambdaFiles.length} lambdas...`);
|
|
84
|
+
const tempDir = join(outputDir, "__temp__");
|
|
85
|
+
mkdirSync(tempDir, { recursive: true });
|
|
86
|
+
const buildResults = [];
|
|
87
|
+
for (const file of lambdaFiles) {
|
|
88
|
+
const name = file.replace(/\.ts$/, "");
|
|
89
|
+
const bundleName = getLambdaBundleName(apiName, name);
|
|
90
|
+
const inputPath = join(lambdasDir, file);
|
|
91
|
+
// Create wrapper entry that imports the original and exports handler
|
|
92
|
+
const wrapperPath = join(tempDir, `${name}-wrapper.ts`);
|
|
93
|
+
const wrapperContent = createWrapperEntry(inputPath);
|
|
94
|
+
await Bun.write(wrapperPath, wrapperContent);
|
|
95
|
+
// Bundle the wrapper with prefixed name
|
|
96
|
+
const result = await bundleLambda(bundleName, wrapperPath, outputDir, config);
|
|
97
|
+
buildResults.push({ ...result, name, bundleName });
|
|
98
|
+
if (!result.success) {
|
|
99
|
+
console.log(` ${pc.red("✗")} Failed to build ${name}: ${result.error}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Clean up temp directory
|
|
104
|
+
const { rmSync } = await import("fs");
|
|
105
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
106
|
+
// Clean up generated OpenAPI file
|
|
107
|
+
if (shouldGenerateOpenApi(config)) {
|
|
108
|
+
await cleanupOpenApiLambda(lambdasDir);
|
|
109
|
+
}
|
|
110
|
+
// Package phase
|
|
111
|
+
console.log(` ${pc.green("✓")} Packaging lambdas...`);
|
|
112
|
+
const packageSizes = new Map();
|
|
113
|
+
for (const file of lambdaFiles) {
|
|
114
|
+
const name = file.replace(/\.ts$/, "");
|
|
115
|
+
const bundleName = getLambdaBundleName(apiName, name);
|
|
116
|
+
const jsPath = join(outputDir, `${bundleName}.js`);
|
|
117
|
+
const result = await packageLambda(bundleName, jsPath, outputDir);
|
|
118
|
+
if (!result.success) {
|
|
119
|
+
console.log(` ${pc.red("✗")} Failed to package ${name}: ${result.error}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
// Get zip size (store by original name for display)
|
|
123
|
+
const zipPath = join(outputDir, `${bundleName}.zip`);
|
|
124
|
+
const stat = await Bun.file(zipPath).stat();
|
|
125
|
+
if (stat) {
|
|
126
|
+
packageSizes.set(name, stat.size);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Generate manifest
|
|
130
|
+
console.log(` ${pc.green("✓")} Generating manifest...`);
|
|
131
|
+
try {
|
|
132
|
+
const manifest = await generateManifest(lambdaFiles, outputDir, config.openapi.enabled, apiName);
|
|
133
|
+
// Write JSON manifest
|
|
134
|
+
const manifestPath = join(outputDir, "manifest.json");
|
|
135
|
+
await writeManifest(manifest, manifestPath);
|
|
136
|
+
// Write Terraform variables
|
|
137
|
+
await writeTerraformVars(manifest, config);
|
|
138
|
+
// Duration
|
|
139
|
+
const duration = Date.now() - startTime;
|
|
140
|
+
// Route table header
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(` ${pc.bold("Routes")}`);
|
|
143
|
+
console.log();
|
|
144
|
+
// Group routes by lambda (use original name for display)
|
|
145
|
+
const routesByLambda = new Map();
|
|
146
|
+
for (const route of manifest.routes) {
|
|
147
|
+
// Extract display name (original name) from bundle name
|
|
148
|
+
const displayName = route.lambda.startsWith(`${apiName}-`)
|
|
149
|
+
? route.lambda.slice(apiName.length + 1)
|
|
150
|
+
: route.lambda;
|
|
151
|
+
const existing = routesByLambda.get(displayName) || [];
|
|
152
|
+
existing.push(route);
|
|
153
|
+
routesByLambda.set(displayName, existing);
|
|
154
|
+
}
|
|
155
|
+
// Method colors
|
|
156
|
+
const methodColor = (method) => {
|
|
157
|
+
switch (method) {
|
|
158
|
+
case "GET":
|
|
159
|
+
return pc.green;
|
|
160
|
+
case "POST":
|
|
161
|
+
return pc.blue;
|
|
162
|
+
case "PUT":
|
|
163
|
+
return pc.yellow;
|
|
164
|
+
case "DELETE":
|
|
165
|
+
return pc.red;
|
|
166
|
+
case "PATCH":
|
|
167
|
+
return pc.magenta;
|
|
168
|
+
default:
|
|
169
|
+
return pc.white;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
// Find longest path for alignment
|
|
173
|
+
const maxPathLen = Math.max(...manifest.routes.map((r) => r.path.length));
|
|
174
|
+
for (const [displayName, routes] of routesByLambda) {
|
|
175
|
+
const size = packageSizes.get(displayName);
|
|
176
|
+
const sizeStr = size ? pc.dim(` (${formatSize(size)})`) : "";
|
|
177
|
+
console.log(` ${pc.dim("λ")} ${pc.bold(displayName)}${sizeStr}`);
|
|
178
|
+
for (const route of routes) {
|
|
179
|
+
const method = methodColor(route.method)(route.method.padEnd(6));
|
|
180
|
+
const path = route.path.padEnd(maxPathLen + 2);
|
|
181
|
+
const params = route.pathParameters.length > 0
|
|
182
|
+
? pc.dim(` [${route.pathParameters.join(", ")}]`)
|
|
183
|
+
: "";
|
|
184
|
+
console.log(` ${method} ${path}${params}`);
|
|
185
|
+
}
|
|
186
|
+
console.log();
|
|
187
|
+
}
|
|
188
|
+
// Summary footer
|
|
189
|
+
console.log(pc.dim(" " + "─".repeat(40)));
|
|
190
|
+
console.log();
|
|
191
|
+
console.log(` ${pc.green("✓")} Compiled ${pc.bold(String(manifest.lambdas.length))} lambdas (${manifest.routes.length} routes) in ${pc.bold(formatDuration(duration))}`);
|
|
192
|
+
console.log();
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.log(` ${pc.red("✗")} ${error instanceof Error ? error.message : String(error)}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev command - Watch mode for development
|
|
3
|
+
*/
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
import { watch, readdirSync, mkdirSync, existsSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { loadConfig } from "../../core/config";
|
|
9
|
+
import { bundleLambda } from "../../core/bundler";
|
|
10
|
+
import { packageLambda } from "../../core/packager";
|
|
11
|
+
import { createWrapperEntry } from "../../core/handler-wrapper";
|
|
12
|
+
import { getLambdaBundleName } from "../../core/naming";
|
|
13
|
+
export const devCommand = defineCommand({
|
|
14
|
+
meta: {
|
|
15
|
+
name: "dev",
|
|
16
|
+
description: "Watch mode - rebuild lambdas on file changes",
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
"no-package": {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
description: "Skip creating zip files (faster rebuilds)",
|
|
22
|
+
default: false,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
async run({ args }) {
|
|
26
|
+
consola.start("Loading configuration...");
|
|
27
|
+
let config;
|
|
28
|
+
try {
|
|
29
|
+
config = await loadConfig();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
consola.error(error instanceof Error ? error.message : error);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const apiName = config.apiName;
|
|
36
|
+
const lambdasDir = join(process.cwd(), config.lambdasDir);
|
|
37
|
+
const outputDir = join(process.cwd(), config.outputDir);
|
|
38
|
+
const tempDir = join(outputDir, "__temp__");
|
|
39
|
+
// Ensure directories exist
|
|
40
|
+
if (!existsSync(lambdasDir)) {
|
|
41
|
+
consola.error(`Lambdas directory not found: ${lambdasDir}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
mkdirSync(outputDir, { recursive: true });
|
|
45
|
+
mkdirSync(tempDir, { recursive: true });
|
|
46
|
+
// Get initial lambda files
|
|
47
|
+
const lambdaFiles = readdirSync(lambdasDir).filter((f) => f.endsWith(".ts") && !f.startsWith("__"));
|
|
48
|
+
if (lambdaFiles.length === 0) {
|
|
49
|
+
consola.warn("No lambda files found in", config.lambdasDir);
|
|
50
|
+
}
|
|
51
|
+
// Build function for a single lambda
|
|
52
|
+
async function buildSingleLambda(filename) {
|
|
53
|
+
const name = filename.replace(/\.ts$/, "");
|
|
54
|
+
const bundleName = getLambdaBundleName(apiName, name);
|
|
55
|
+
const inputPath = join(lambdasDir, filename);
|
|
56
|
+
// Create wrapper entry
|
|
57
|
+
const wrapperPath = join(tempDir, `${name}-wrapper.ts`);
|
|
58
|
+
const wrapperContent = createWrapperEntry(inputPath);
|
|
59
|
+
await Bun.write(wrapperPath, wrapperContent);
|
|
60
|
+
// Bundle with prefixed name
|
|
61
|
+
const buildResult = await bundleLambda(bundleName, wrapperPath, outputDir, config);
|
|
62
|
+
if (!buildResult.success) {
|
|
63
|
+
consola.error(`Failed to build ${name}: ${buildResult.error}`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
consola.success(`Built ${bundleName}.js`);
|
|
67
|
+
// Package if not disabled
|
|
68
|
+
if (!args["no-package"]) {
|
|
69
|
+
const jsPath = join(outputDir, `${bundleName}.js`);
|
|
70
|
+
const packageResult = await packageLambda(bundleName, jsPath, outputDir);
|
|
71
|
+
if (!packageResult.success) {
|
|
72
|
+
consola.error(`Failed to package ${name}: ${packageResult.error}`);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
consola.success(`Packaged ${bundleName}.zip`);
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
// Initial build of all lambdas
|
|
80
|
+
consola.start("Initial build...");
|
|
81
|
+
for (const file of lambdaFiles) {
|
|
82
|
+
await buildSingleLambda(file);
|
|
83
|
+
}
|
|
84
|
+
consola.info("Initial build complete");
|
|
85
|
+
console.log("");
|
|
86
|
+
consola.box("Watch mode active\n\n" +
|
|
87
|
+
`Watching: ${config.lambdasDir}/\n` +
|
|
88
|
+
`Output: ${config.outputDir}/\n\n` +
|
|
89
|
+
"Press Ctrl+C to stop");
|
|
90
|
+
// Set up file watcher
|
|
91
|
+
const watcher = watch(lambdasDir, { recursive: false }, async (event, filename) => {
|
|
92
|
+
if (!filename || !filename.endsWith(".ts") || filename.startsWith("__")) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log("");
|
|
96
|
+
consola.info(`Change detected: ${filename}`);
|
|
97
|
+
const success = await buildSingleLambda(filename);
|
|
98
|
+
if (success) {
|
|
99
|
+
consola.ready("Rebuild complete");
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// Handle graceful shutdown
|
|
103
|
+
process.on("SIGINT", () => {
|
|
104
|
+
console.log("");
|
|
105
|
+
consola.info("Stopping watcher...");
|
|
106
|
+
watcher.close();
|
|
107
|
+
// Clean up temp directory
|
|
108
|
+
const { rmSync } = require("fs");
|
|
109
|
+
try {
|
|
110
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Ignore cleanup errors
|
|
114
|
+
}
|
|
115
|
+
process.exit(0);
|
|
116
|
+
});
|
|
117
|
+
// Keep process alive
|
|
118
|
+
await new Promise(() => { });
|
|
119
|
+
},
|
|
120
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate IAC command - Regenerate Terraform files without rebuilding
|
|
3
|
+
*/
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
import { readdirSync, existsSync, mkdirSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { loadConfig } from "../../core/config";
|
|
9
|
+
import { generateManifest, writeManifest } from "../../core/manifest";
|
|
10
|
+
import { writeTerraformVars } from "../../core/terraform";
|
|
11
|
+
import { getOriginalLambdaName } from "../../core/naming";
|
|
12
|
+
export const generateIacCommand = defineCommand({
|
|
13
|
+
meta: {
|
|
14
|
+
name: "generate-iac",
|
|
15
|
+
description: "Regenerate Terraform files from existing build artifacts",
|
|
16
|
+
},
|
|
17
|
+
args: {},
|
|
18
|
+
async run() {
|
|
19
|
+
consola.start("Loading configuration...");
|
|
20
|
+
let config;
|
|
21
|
+
try {
|
|
22
|
+
config = await loadConfig();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
consola.error(error instanceof Error ? error.message : error);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const apiName = config.apiName;
|
|
29
|
+
const outputDir = join(process.cwd(), config.outputDir);
|
|
30
|
+
const terraformDir = join(process.cwd(), config.terraform.outputDir);
|
|
31
|
+
// Check that build output exists
|
|
32
|
+
if (!existsSync(outputDir)) {
|
|
33
|
+
consola.error(`Build output directory not found: ${outputDir}\nRun 'elysian build' first.`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
// Ensure terraform directory exists
|
|
37
|
+
mkdirSync(terraformDir, { recursive: true });
|
|
38
|
+
// Get built lambda files (they have the apiName prefix)
|
|
39
|
+
const jsFiles = readdirSync(outputDir).filter((f) => f.endsWith(".js") && !f.startsWith("__temp__"));
|
|
40
|
+
if (jsFiles.length === 0) {
|
|
41
|
+
consola.error(`No built lambda files found in ${outputDir}\nRun 'elysian build' first.`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
// Convert bundle names back to .ts names for manifest generation
|
|
45
|
+
// The manifest generator will re-add the prefix
|
|
46
|
+
const lambdaFiles = jsFiles.map((f) => {
|
|
47
|
+
const bundleName = f.replace(/\.js$/, "");
|
|
48
|
+
const originalName = getOriginalLambdaName(apiName, bundleName);
|
|
49
|
+
return `${originalName}.ts`;
|
|
50
|
+
});
|
|
51
|
+
consola.info(`Found ${lambdaFiles.length} built lambda(s)`);
|
|
52
|
+
// Generate manifest
|
|
53
|
+
consola.start("Generating route manifest...");
|
|
54
|
+
try {
|
|
55
|
+
const manifest = await generateManifest(lambdaFiles, outputDir, config.openapi.enabled, apiName);
|
|
56
|
+
// Write JSON manifest
|
|
57
|
+
const manifestPath = join(outputDir, "manifest.json");
|
|
58
|
+
await writeManifest(manifest, manifestPath);
|
|
59
|
+
consola.success("Generated manifest.json");
|
|
60
|
+
// Write Terraform variables
|
|
61
|
+
const tfvarsPath = await writeTerraformVars(manifest, config);
|
|
62
|
+
consola.success(`Generated ${config.terraform.tfvarsFilename}`);
|
|
63
|
+
// Print summary
|
|
64
|
+
console.log("");
|
|
65
|
+
consola.box(`Infrastructure files generated\n\n` +
|
|
66
|
+
`Lambdas: ${manifest.lambdas.length}\n` +
|
|
67
|
+
`Routes: ${manifest.routes.length}\n\n` +
|
|
68
|
+
`Output: ${config.terraform.outputDir}/${config.terraform.tfvarsFilename}`);
|
|
69
|
+
// Print route summary
|
|
70
|
+
console.log("\nRoute Summary:");
|
|
71
|
+
for (const route of manifest.routes) {
|
|
72
|
+
const params = route.pathParameters.length > 0
|
|
73
|
+
? ` [${route.pathParameters.join(", ")}]`
|
|
74
|
+
: "";
|
|
75
|
+
console.log(` ${route.method.padEnd(6)} ${route.path.padEnd(30)} → ${route.lambda}${params}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
consola.error(error instanceof Error ? error.message : error);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command - Initialize a new elysian project
|
|
3
|
+
*/
|
|
4
|
+
export declare const initCommand: import("citty").CommandDef<{
|
|
5
|
+
name: {
|
|
6
|
+
type: "string";
|
|
7
|
+
description: string;
|
|
8
|
+
default: string;
|
|
9
|
+
};
|
|
10
|
+
force: {
|
|
11
|
+
type: "boolean";
|
|
12
|
+
description: string;
|
|
13
|
+
default: false;
|
|
14
|
+
};
|
|
15
|
+
}>;
|