@actuallyjamez/elysian 0.7.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({
@@ -100,7 +100,10 @@ export const devCommand = defineCommand({
100
100
  async function buildSingleLambda(filename) {
101
101
  const lambdaName = filename.replace(/\.ts$/, "");
102
102
  const bundleName = getLambdaBundleName(name, lambdaName);
103
- 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);
104
107
  // Create wrapper entry
105
108
  const wrapperPath = join(tempDir, `${lambdaName}-wrapper.ts`);
106
109
  const wrapperContent = createWrapperEntry(inputPath);
@@ -108,17 +111,17 @@ export const devCommand = defineCommand({
108
111
  // Bundle with prefixed name
109
112
  const buildResult = await bundleLambda(bundleName, wrapperPath, outputDir, config);
110
113
  if (!buildResult.success) {
111
- return false;
114
+ return { success: false, error: buildResult.error };
112
115
  }
113
116
  // Package if not disabled
114
117
  if (!args["no-package"]) {
115
118
  const jsPath = join(outputDir, `${bundleName}.js`);
116
119
  const packageResult = await packageLambda(bundleName, jsPath, outputDir);
117
120
  if (!packageResult.success) {
118
- return false;
121
+ return { success: false, error: packageResult.error };
119
122
  }
120
123
  }
121
- return true;
124
+ return { success: true };
122
125
  }
123
126
  // Build all lambdas (including OpenAPI if enabled)
124
127
  async function buildAll() {
@@ -127,20 +130,17 @@ export const devCommand = defineCommand({
127
130
  const filesToBuild = [...lambdaFiles];
128
131
  // Generate OpenAPI aggregator if enabled
129
132
  if (shouldGenerateOpenApi(config)) {
130
- await writeOpenApiLambda(lambdaFiles, lambdasDir, config);
133
+ await writeOpenApiLambda(lambdaFiles, lambdasDir, config, tempDir);
131
134
  filesToBuild.push("__openapi__.ts");
132
135
  }
133
136
  // Build all lambdas
134
137
  for (const file of filesToBuild) {
135
- const success = await buildSingleLambda(file);
136
- if (!success) {
137
- return { success: false, count: 0, error: `Failed to build ${file}` };
138
+ const result = await buildSingleLambda(file);
139
+ if (!result.success) {
140
+ return { success: false, count: 0, error: `${file}: ${result.error || "Unknown error"}` };
138
141
  }
139
142
  }
140
- // Cleanup OpenAPI source file
141
- if (shouldGenerateOpenApi(config)) {
142
- await cleanupOpenApiLambda(lambdasDir);
143
- }
143
+ // No need to cleanup OpenAPI - it's in tempDir which persists during dev
144
144
  return { success: true, count: filesToBuild.length };
145
145
  }
146
146
  // Generate manifest and terraform vars
@@ -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...");
@@ -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.7.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",