@actuallyjamez/elysian 0.6.0 → 0.7.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.
@@ -9,7 +9,7 @@ import { bundleLambda } from "../../core/bundler";
9
9
  import { packageLambda } from "../../core/packager";
10
10
  import { generateManifest, writeManifest } from "../../core/manifest";
11
11
  import { writeTerraformVars } from "../../core/terraform";
12
- import { shouldGenerateOpenApi, writeOpenApiLambda, cleanupOpenApiLambda, } from "../../core/openapi";
12
+ import { shouldGenerateOpenApi, writeOpenApiLambda, } from "../../core/openapi";
13
13
  import { createWrapperEntry } from "../../core/handler-wrapper";
14
14
  import { getLambdaBundleName } from "../../core/naming";
15
15
  import { ui, pc, formatDuration, formatSize } from "../ui";
@@ -53,6 +53,9 @@ export const buildCommand = defineCommand({
53
53
  }
54
54
  mkdirSync(outputDir, { recursive: true });
55
55
  mkdirSync(terraformDir, { recursive: true });
56
+ // Create temp directory for generated files
57
+ const tempDir = join(outputDir, "__temp__");
58
+ mkdirSync(tempDir, { recursive: true });
56
59
  // Get lambda files
57
60
  let lambdaFiles = readdirSync(lambdasDir).filter((f) => f.endsWith(".ts") && !f.startsWith("__"));
58
61
  if (lambdaFiles.length === 0) {
@@ -61,18 +64,19 @@ export const buildCommand = defineCommand({
61
64
  }
62
65
  // Generate OpenAPI aggregator if enabled
63
66
  if (shouldGenerateOpenApi(config)) {
64
- await writeOpenApiLambda(lambdaFiles, lambdasDir, config);
67
+ await writeOpenApiLambda(lambdaFiles, lambdasDir, config, tempDir);
65
68
  lambdaFiles.push("__openapi__.ts");
66
69
  }
67
70
  // Build phase
68
71
  ui.success(`Compiling ${lambdaFiles.length} lambdas...`);
69
- const tempDir = join(outputDir, "__temp__");
70
- mkdirSync(tempDir, { recursive: true });
71
72
  const buildResults = [];
72
73
  for (const file of lambdaFiles) {
73
74
  const lambdaName = file.replace(/\.ts$/, "");
74
75
  const bundleName = getLambdaBundleName(name, lambdaName);
75
- const inputPath = join(lambdasDir, file);
76
+ // For OpenAPI, the source is in tempDir; for regular lambdas, it's in lambdasDir
77
+ const inputPath = file === "__openapi__.ts"
78
+ ? join(tempDir, file)
79
+ : join(lambdasDir, file);
76
80
  // Create wrapper entry that imports the original and exports handler
77
81
  const wrapperPath = join(tempDir, `${lambdaName}-wrapper.ts`);
78
82
  const wrapperContent = createWrapperEntry(inputPath);
@@ -87,10 +91,7 @@ export const buildCommand = defineCommand({
87
91
  }
88
92
  // Clean up temp directory
89
93
  rmSync(tempDir, { recursive: true, force: true });
90
- // Clean up generated OpenAPI file
91
- if (shouldGenerateOpenApi(config)) {
92
- await cleanupOpenApiLambda(lambdasDir);
93
- }
94
+ // No need to clean up OpenAPI file separately - it's in tempDir
94
95
  // Package phase
95
96
  ui.success("Packaging lambdas...");
96
97
  const packageSizes = new Map();
@@ -11,7 +11,7 @@ import { createWrapperEntry } from "../../core/handler-wrapper";
11
11
  import { getLambdaBundleName } from "../../core/naming";
12
12
  import { generateManifest, writeManifest } from "../../core/manifest";
13
13
  import { writeTerraformVars } from "../../core/terraform";
14
- import { shouldGenerateOpenApi, writeOpenApiLambda, cleanupOpenApiLambda, } from "../../core/openapi";
14
+ import { shouldGenerateOpenApi, writeOpenApiLambda, } from "../../core/openapi";
15
15
  import { detectLocalStack, isTerraformInitialized, runTfLocalInit, runTfLocalApply, getTerraformOutputs, } from "../../core/localstack";
16
16
  import { ui, pc, createSpinner, formatDuration } from "../ui";
17
17
  export const devCommand = defineCommand({
@@ -95,11 +95,15 @@ export const devCommand = defineCommand({
95
95
  // Track last terraform outputs and build info for display
96
96
  let lastOutputs = null;
97
97
  let lastBuildInfo = null;
98
+ let lastError = null;
98
99
  // Build function for a single lambda
99
100
  async function buildSingleLambda(filename) {
100
101
  const lambdaName = filename.replace(/\.ts$/, "");
101
102
  const bundleName = getLambdaBundleName(name, lambdaName);
102
- const inputPath = join(lambdasDir, filename);
103
+ // For OpenAPI, the source is in tempDir; for regular lambdas, it's in lambdasDir
104
+ const inputPath = filename === "__openapi__.ts"
105
+ ? join(tempDir, filename)
106
+ : join(lambdasDir, filename);
103
107
  // Create wrapper entry
104
108
  const wrapperPath = join(tempDir, `${lambdaName}-wrapper.ts`);
105
109
  const wrapperContent = createWrapperEntry(inputPath);
@@ -107,17 +111,17 @@ export const devCommand = defineCommand({
107
111
  // Bundle with prefixed name
108
112
  const buildResult = await bundleLambda(bundleName, wrapperPath, outputDir, config);
109
113
  if (!buildResult.success) {
110
- return false;
114
+ return { success: false, error: buildResult.error };
111
115
  }
112
116
  // Package if not disabled
113
117
  if (!args["no-package"]) {
114
118
  const jsPath = join(outputDir, `${bundleName}.js`);
115
119
  const packageResult = await packageLambda(bundleName, jsPath, outputDir);
116
120
  if (!packageResult.success) {
117
- return false;
121
+ return { success: false, error: packageResult.error };
118
122
  }
119
123
  }
120
- return true;
124
+ return { success: true };
121
125
  }
122
126
  // Build all lambdas (including OpenAPI if enabled)
123
127
  async function buildAll() {
@@ -126,20 +130,17 @@ export const devCommand = defineCommand({
126
130
  const filesToBuild = [...lambdaFiles];
127
131
  // Generate OpenAPI aggregator if enabled
128
132
  if (shouldGenerateOpenApi(config)) {
129
- await writeOpenApiLambda(lambdaFiles, lambdasDir, config);
133
+ await writeOpenApiLambda(lambdaFiles, lambdasDir, config, tempDir);
130
134
  filesToBuild.push("__openapi__.ts");
131
135
  }
132
136
  // Build all lambdas
133
137
  for (const file of filesToBuild) {
134
- const success = await buildSingleLambda(file);
135
- if (!success) {
136
- return { success: false, count: 0 };
138
+ const result = await buildSingleLambda(file);
139
+ if (!result.success) {
140
+ return { success: false, count: 0, error: `${file}: ${result.error || "Unknown error"}` };
137
141
  }
138
142
  }
139
- // Cleanup OpenAPI source file
140
- if (shouldGenerateOpenApi(config)) {
141
- await cleanupOpenApiLambda(lambdasDir);
142
- }
143
+ // No need to cleanup OpenAPI - it's in tempDir which persists during dev
143
144
  return { success: true, count: filesToBuild.length };
144
145
  }
145
146
  // Generate manifest and terraform vars
@@ -155,19 +156,19 @@ export const devCommand = defineCommand({
155
156
  await writeTerraformVars(manifest, config);
156
157
  return { success: true, routes: manifest.routes.length };
157
158
  }
158
- catch {
159
- return { success: false, routes: 0 };
159
+ catch (err) {
160
+ return { success: false, routes: 0, error: err instanceof Error ? err.message : String(err) };
160
161
  }
161
162
  }
162
163
  // Deploy to LocalStack
163
164
  async function deployToLocalStack() {
164
165
  const applyResult = await runTfLocalApply(terraformDir);
165
166
  if (!applyResult.success) {
166
- return false;
167
+ return { success: false, error: applyResult.error };
167
168
  }
168
169
  // Get and store outputs (transformed to LocalStack URLs)
169
170
  lastOutputs = await getTerraformOutputs(terraformDir, true);
170
- return true;
171
+ return { success: true };
171
172
  }
172
173
  // Show the final status screen (Vite-like)
173
174
  function showReadyScreen(trigger, failed) {
@@ -178,6 +179,18 @@ export const devCommand = defineCommand({
178
179
  if (trigger) {
179
180
  ui.info(`Triggered by: ${trigger}`);
180
181
  }
182
+ if (lastError) {
183
+ ui.blank();
184
+ console.log(pc.dim(" Error:"));
185
+ // Show first few lines of error
186
+ const errorLines = lastError.split("\n").slice(0, 10);
187
+ for (const line of errorLines) {
188
+ console.log(pc.red(` ${line}`));
189
+ }
190
+ if (lastError.split("\n").length > 10) {
191
+ console.log(pc.dim(" ... (truncated)"));
192
+ }
193
+ }
181
194
  }
182
195
  else if (lastBuildInfo) {
183
196
  ui.success(`Ready in ${pc.bold(formatDuration(lastBuildInfo.duration))}`);
@@ -211,6 +224,7 @@ export const devCommand = defineCommand({
211
224
  // Run the full build and deploy cycle
212
225
  async function runBuildCycle(trigger) {
213
226
  const cycleStart = Date.now();
227
+ lastError = null;
214
228
  // Show building status
215
229
  ui.clear();
216
230
  ui.header(pc.dim("dev"));
@@ -223,6 +237,7 @@ export const devCommand = defineCommand({
223
237
  const buildResult = await buildAll();
224
238
  if (!buildResult.success) {
225
239
  buildSpinner.fail("Build failed");
240
+ lastError = buildResult.error || "Unknown build error";
226
241
  showReadyScreen(trigger, true);
227
242
  return;
228
243
  }
@@ -232,6 +247,7 @@ export const devCommand = defineCommand({
232
247
  const manifestResult = await generateManifestFiles();
233
248
  if (!manifestResult.success) {
234
249
  manifestSpinner.fail("Manifest failed");
250
+ lastError = manifestResult.error || "Unknown manifest error";
235
251
  showReadyScreen(trigger, true);
236
252
  return;
237
253
  }
@@ -239,9 +255,10 @@ export const devCommand = defineCommand({
239
255
  // Deploy if LocalStack enabled
240
256
  if (localstackEnabled) {
241
257
  const deploySpinner = createSpinner("Deploying...").start();
242
- const deploySuccess = await deployToLocalStack();
243
- if (!deploySuccess) {
258
+ const deployResult = await deployToLocalStack();
259
+ if (!deployResult.success) {
244
260
  deploySpinner.fail("Deploy failed");
261
+ lastError = deployResult.error || "Unknown deploy error";
245
262
  showReadyScreen(trigger, true);
246
263
  return;
247
264
  }
@@ -260,14 +277,16 @@ export const devCommand = defineCommand({
260
277
  // Run terraform-only deploy
261
278
  async function runTerraformCycle(trigger) {
262
279
  const cycleStart = Date.now();
280
+ lastError = null;
263
281
  ui.clear();
264
282
  ui.header(pc.dim("dev"));
265
283
  ui.info(`Terraform: ${trigger}`);
266
284
  ui.blank();
267
285
  const deploySpinner = createSpinner("Deploying...").start();
268
- const deploySuccess = await deployToLocalStack();
269
- if (!deploySuccess) {
286
+ const deployResult = await deployToLocalStack();
287
+ if (!deployResult.success) {
270
288
  deploySpinner.fail("Deploy failed");
289
+ lastError = deployResult.error || "Unknown deploy error";
271
290
  showReadyScreen(trigger, true);
272
291
  return;
273
292
  }
@@ -322,12 +341,14 @@ export const devCommand = defineCommand({
322
341
  terraformWatcher = watch(terraformDir, { recursive: false }, (event, filename) => {
323
342
  if (!filename)
324
343
  return;
325
- // Skip auto-generated files
344
+ // Skip auto-generated files and tflocal override files
326
345
  if (filename === config.terraform.tfvarsFilename ||
327
346
  filename.endsWith(".auto.tfvars") ||
328
347
  filename.startsWith(".terraform") ||
329
348
  filename.endsWith(".tfstate") ||
330
- filename.endsWith(".tfstate.backup")) {
349
+ filename.endsWith(".tfstate.backup") ||
350
+ filename.includes("override") ||
351
+ filename.startsWith("localstack")) {
331
352
  return;
332
353
  }
333
354
  // Only watch .tf files
@@ -147,7 +147,7 @@ async function scaffoldTerraform(cwd, info, name, result) {
147
147
  */
148
148
  export async function installDependencies(cwd, packageManager) {
149
149
  const deps = ["elysia", "@actuallyjamez/elysian"];
150
- const devDeps = ["@types/bun", "typescript"];
150
+ const devDeps = ["@types/node", "typescript"];
151
151
  const addCmd = packageManager === "npm" ? "install" : "add";
152
152
  const devFlag = packageManager === "npm" ? "--save-dev" : "-D";
153
153
  ui.info("Installing dependencies...");
@@ -19,8 +19,8 @@ export function exampleLambdaTemplate() {
19
19
  return `import { createLambda, t } from "@actuallyjamez/elysian";
20
20
 
21
21
  export default createLambda()
22
- .get("/hello", ({ query }) => {
23
- return \`Hello, \${query.name ?? "World"}!\`;
22
+ .get("/", ({ query }) => {
23
+ return \`Hello, \${query.name ?? "Elysian"}!\`;
24
24
  }, {
25
25
  response: t.String(),
26
26
  query: t.Object({
@@ -91,7 +91,7 @@ export function tsconfigTemplate() {
91
91
  esModuleInterop: true,
92
92
  skipLibCheck: true,
93
93
  noEmit: true,
94
- types: ["bun-types"],
94
+ types: ["@types/node"],
95
95
  },
96
96
  include: ["src/**/*", "elysian.config.ts"],
97
97
  exclude: ["node_modules", "dist"],
@@ -21,15 +21,37 @@ export async function bundleLambda(name, inputPath, outputDir, config) {
21
21
  plugins: [createHandlerWrapperPlugin()],
22
22
  });
23
23
  if (!result.success) {
24
- const errors = result.logs
24
+ // Extract detailed error messages from build logs
25
+ const errorMessages = result.logs
25
26
  .filter((log) => log.level === "error")
26
- .map((log) => log.message)
27
- .join("\n");
27
+ .map((log) => {
28
+ // Include file position if available
29
+ const position = log.position;
30
+ if (position) {
31
+ return `${position.file}:${position.line}:${position.column}: ${log.message}`;
32
+ }
33
+ return log.message;
34
+ });
35
+ // Also include warnings that might be relevant
36
+ const warningMessages = result.logs
37
+ .filter((log) => log.level === "warning")
38
+ .map((log) => log.message);
39
+ const allMessages = [...errorMessages, ...warningMessages];
40
+ // If no messages captured, try to get string representation of all logs
41
+ if (allMessages.length === 0 && result.logs.length > 0) {
42
+ const allLogs = result.logs.map((log) => `[${log.level}] ${log.message}`);
43
+ return {
44
+ name,
45
+ outputPath: join(outputDir, `${name}.js`),
46
+ success: false,
47
+ error: allLogs.join("\n") || "Build failed with no error message",
48
+ };
49
+ }
28
50
  return {
29
51
  name,
30
52
  outputPath: join(outputDir, `${name}.js`),
31
53
  success: false,
32
- error: errors || "Unknown build error",
54
+ error: allMessages.length > 0 ? allMessages.join("\n") : "Build failed with no error message",
33
55
  };
34
56
  }
35
57
  return {
@@ -39,11 +61,36 @@ export async function bundleLambda(name, inputPath, outputDir, config) {
39
61
  };
40
62
  }
41
63
  catch (error) {
64
+ // Handle AggregateError from Bun bundler (contains detailed parse errors)
65
+ if (error instanceof AggregateError && error.errors?.length > 0) {
66
+ const errorMessages = error.errors.map((e) => {
67
+ // Use Bun.inspect for nicely formatted error output
68
+ if (typeof Bun !== "undefined" && Bun.inspect) {
69
+ return Bun.inspect(e);
70
+ }
71
+ // Fallback: try to extract message and position
72
+ const err = e;
73
+ if (err.position) {
74
+ return `${err.position.file}:${err.position.line}:${err.position.column}: ${err.message}`;
75
+ }
76
+ return String(e);
77
+ });
78
+ return {
79
+ name,
80
+ outputPath: join(outputDir, `${name}.js`),
81
+ success: false,
82
+ error: errorMessages.join("\n\n"),
83
+ };
84
+ }
85
+ // Capture full error details including stack trace
86
+ const errorMessage = error instanceof Error
87
+ ? `${error.message}${error.stack ? `\n${error.stack}` : ""}`
88
+ : String(error);
42
89
  return {
43
90
  name,
44
91
  outputPath: join(outputDir, `${name}.js`),
45
92
  success: false,
46
- error: error instanceof Error ? error.message : String(error),
93
+ error: errorMessage,
47
94
  };
48
95
  }
49
96
  }
@@ -124,14 +124,32 @@ export const handler = __createElysiaHandler(__route);
124
124
  `;
125
125
  return modifiedCode + handlerWrapper;
126
126
  }
127
+ /**
128
+ * Get the path to a module from elysian's node_modules
129
+ */
130
+ function resolveFromElysian(modulePath) {
131
+ // Use import.meta.resolve to get the absolute path
132
+ // This resolves relative to where this code is located (elysian package)
133
+ try {
134
+ const resolved = import.meta.resolve(modulePath);
135
+ // Convert file:// URL to path
136
+ return resolved.replace("file://", "");
137
+ }
138
+ catch {
139
+ // Fallback to relative import if resolution fails
140
+ return modulePath;
141
+ }
142
+ }
127
143
  /**
128
144
  * Create a wrapper entry file that imports and re-exports with handler
129
145
  */
130
146
  export function createWrapperEntry(originalPath) {
147
+ const honoTinyPath = resolveFromElysian("hono/tiny");
148
+ const honoLambdaPath = resolveFromElysian("hono/aws-lambda");
131
149
  return `
132
150
  import route from "${originalPath}";
133
- import { Hono } from "hono/tiny";
134
- import { handle } from "hono/aws-lambda";
151
+ import { Hono } from "${honoTinyPath}";
152
+ import { handle } from "${honoLambdaPath}";
135
153
 
136
154
  // Re-export the route as default for introspection
137
155
  export default route;
@@ -8,11 +8,11 @@ import type { ResolvedConfig } from "./config";
8
8
  /**
9
9
  * Generate the OpenAPI aggregator lambda source code
10
10
  */
11
- export declare function generateOpenApiLambdaSource(lambdaFiles: string[], config: ResolvedConfig): string;
11
+ export declare function generateOpenApiLambdaSource(lambdaFiles: string[], lambdasDir: string, config: ResolvedConfig): string;
12
12
  /**
13
13
  * Write the OpenAPI aggregator lambda to a temp location for bundling
14
14
  */
15
- export declare function writeOpenApiLambda(lambdaFiles: string[], lambdasDir: string, config: ResolvedConfig): Promise<string>;
15
+ export declare function writeOpenApiLambda(lambdaFiles: string[], lambdasDir: string, config: ResolvedConfig, tempDir: string): Promise<string>;
16
16
  /**
17
17
  * Check if OpenAPI aggregator needs to be generated
18
18
  */
@@ -20,4 +20,4 @@ export declare function shouldGenerateOpenApi(config: ResolvedConfig): boolean;
20
20
  /**
21
21
  * Clean up the generated OpenAPI lambda file
22
22
  */
23
- export declare function cleanupOpenApiLambda(lambdasDir: string): Promise<void>;
23
+ export declare function cleanupOpenApiLambda(tempDir: string): Promise<void>;
@@ -5,28 +5,46 @@
5
5
  * from all other lambdas to provide a complete API specification.
6
6
  */
7
7
  import { join } from "path";
8
+ /**
9
+ * Resolve a module path from elysian's node_modules
10
+ */
11
+ function resolveFromElysian(modulePath) {
12
+ try {
13
+ const resolved = import.meta.resolve(modulePath);
14
+ return resolved.replace("file://", "");
15
+ }
16
+ catch {
17
+ return modulePath;
18
+ }
19
+ }
8
20
  /**
9
21
  * Generate the OpenAPI aggregator lambda source code
10
22
  */
11
- export function generateOpenApiLambdaSource(lambdaFiles, config) {
23
+ export function generateOpenApiLambdaSource(lambdaFiles, lambdasDir, config) {
12
24
  // Filter out any existing openapi file to avoid circular imports
13
25
  const routeFiles = lambdaFiles.filter((f) => !f.includes("openapi") && !f.startsWith("__"));
14
26
  const imports = routeFiles
15
27
  .map((file, index) => {
16
28
  const name = file.replace(/\.ts$/, "");
17
- return `import route${index} from "./${name}";`;
29
+ // Use absolute path to the lambda file
30
+ const absolutePath = join(lambdasDir, name);
31
+ return `import route${index} from "${absolutePath}";`;
18
32
  })
19
33
  .join("\n");
20
34
  const uses = routeFiles.map((_, index) => `.use(route${index})`).join("\n\t\t");
35
+ // Resolve paths from elysian's dependencies
36
+ const honoTinyPath = resolveFromElysian("hono/tiny");
37
+ const honoLambdaPath = resolveFromElysian("hono/aws-lambda");
38
+ const openapiPath = resolveFromElysian("@elysiajs/openapi");
21
39
  return `/**
22
40
  * Auto-generated OpenAPI aggregator lambda
23
41
  * Generated by elysia-apigw - DO NOT EDIT
24
42
  */
25
43
 
26
44
  import Elysia from "elysia";
27
- import { openapi } from "@elysiajs/openapi";
28
- import { Hono } from "hono/tiny";
29
- import { handle } from "hono/aws-lambda";
45
+ import { openapi } from "${openapiPath}";
46
+ import { Hono } from "${honoTinyPath}";
47
+ import { handle } from "${honoLambdaPath}";
30
48
 
31
49
  ${imports}
32
50
 
@@ -54,9 +72,9 @@ export const handler = handle(api);
54
72
  /**
55
73
  * Write the OpenAPI aggregator lambda to a temp location for bundling
56
74
  */
57
- export async function writeOpenApiLambda(lambdaFiles, lambdasDir, config) {
58
- const source = generateOpenApiLambdaSource(lambdaFiles, config);
59
- const outputPath = join(lambdasDir, "__openapi__.ts");
75
+ export async function writeOpenApiLambda(lambdaFiles, lambdasDir, config, tempDir) {
76
+ const source = generateOpenApiLambdaSource(lambdaFiles, lambdasDir, config);
77
+ const outputPath = join(tempDir, "__openapi__.ts");
60
78
  await Bun.write(outputPath, source);
61
79
  return outputPath;
62
80
  }
@@ -69,8 +87,8 @@ export function shouldGenerateOpenApi(config) {
69
87
  /**
70
88
  * Clean up the generated OpenAPI lambda file
71
89
  */
72
- export async function cleanupOpenApiLambda(lambdasDir) {
73
- const openApiPath = join(lambdasDir, "__openapi__.ts");
90
+ export async function cleanupOpenApiLambda(tempDir) {
91
+ const openApiPath = join(tempDir, "__openapi__.ts");
74
92
  try {
75
93
  const file = Bun.file(openApiPath);
76
94
  if (await file.exists()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuallyjamez/elysian",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Automatic Lambda bundler for Elysia with API Gateway and Terraform integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,8 @@
20
20
  },
21
21
  "files": [
22
22
  "dist",
23
- "templates"
23
+ "templates",
24
+ "README.md"
24
25
  ],
25
26
  "repository": {
26
27
  "type": "git",