@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
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
/**
|
|
13
|
+
* Create a Bun plugin that wraps default Elysia exports with a Lambda handler
|
|
14
|
+
*/
|
|
15
|
+
export function createHandlerWrapperPlugin() {
|
|
16
|
+
return {
|
|
17
|
+
name: "elysia-apigw-handler-wrapper",
|
|
18
|
+
setup(build) {
|
|
19
|
+
// We'll handle this at the output level instead
|
|
20
|
+
// by appending handler export after bundling
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Transform source code to add handler export
|
|
26
|
+
* This is called after the initial bundle to add the handler wrapper
|
|
27
|
+
*/
|
|
28
|
+
export function wrapWithHandler(code) {
|
|
29
|
+
// Check if there's already a handler export
|
|
30
|
+
if (/export\s+(const|let|var|function)\s+handler\b/.test(code)) {
|
|
31
|
+
return code;
|
|
32
|
+
}
|
|
33
|
+
// Check for default export
|
|
34
|
+
const hasDefaultExport = /export\s+default\s+/.test(code);
|
|
35
|
+
if (!hasDefaultExport) {
|
|
36
|
+
return code;
|
|
37
|
+
}
|
|
38
|
+
// Add the handler wrapper import and export at the end
|
|
39
|
+
const handlerCode = `
|
|
40
|
+
// Auto-injected by elysia-apigw
|
|
41
|
+
import { Hono } from "hono/tiny";
|
|
42
|
+
import { handle } from "hono/aws-lambda";
|
|
43
|
+
|
|
44
|
+
const __defaultExport = await (async () => {
|
|
45
|
+
const mod = await import.meta.require?.("./index.mjs") ?? { default: null };
|
|
46
|
+
return mod.default;
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
function __createHandler(route) {
|
|
50
|
+
const api = new Hono().mount("/", route.fetch);
|
|
51
|
+
return handle(api);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const handler = __createHandler(__defaultExport);
|
|
55
|
+
`;
|
|
56
|
+
return code + handlerCode;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Alternative approach: Transform the bundle output directly
|
|
60
|
+
* This rewrites the code to capture the default export and wrap it
|
|
61
|
+
*/
|
|
62
|
+
export function transformBundleForLambda(code, lambdaName) {
|
|
63
|
+
// If handler already exists, return as-is
|
|
64
|
+
if (/export\s+(const|let|var|function)\s+handler\b/.test(code)) {
|
|
65
|
+
return code;
|
|
66
|
+
}
|
|
67
|
+
// Find and capture default export, wrap with handler
|
|
68
|
+
// We use a simple regex-based approach for common patterns
|
|
69
|
+
// Pattern 1: export default <identifier>;
|
|
70
|
+
// Pattern 2: export default createLambda()...;
|
|
71
|
+
// Pattern 3: export { something as default };
|
|
72
|
+
// The safest approach is to append a wrapper that re-imports the module
|
|
73
|
+
// But for a single-file bundle, we need a different strategy
|
|
74
|
+
// For bundled output, we'll use a post-processing approach:
|
|
75
|
+
// 1. Replace "export default X" with "const __defaultRoute__ = X; export default __defaultRoute__"
|
|
76
|
+
// 2. Append handler creation
|
|
77
|
+
// Match various default export patterns
|
|
78
|
+
const patterns = [
|
|
79
|
+
// export default identifier;
|
|
80
|
+
/export\s+default\s+(\w+)\s*;/,
|
|
81
|
+
// export default expression (function call, object, etc)
|
|
82
|
+
/export\s+default\s+/,
|
|
83
|
+
];
|
|
84
|
+
let hasDefault = false;
|
|
85
|
+
let modifiedCode = code;
|
|
86
|
+
// Check if code has default export
|
|
87
|
+
if (/export\s+default\s+/.test(code)) {
|
|
88
|
+
hasDefault = true;
|
|
89
|
+
}
|
|
90
|
+
if (!hasDefault) {
|
|
91
|
+
// No default export, check for named route exports and use the first one
|
|
92
|
+
const namedExportMatch = code.match(/export\s+(?:const|let|var)\s+(\w+(?:Route|Lambda|App))\s*=/);
|
|
93
|
+
if (namedExportMatch) {
|
|
94
|
+
// Add default export for the route
|
|
95
|
+
modifiedCode += `\nexport default ${namedExportMatch[1]};\n`;
|
|
96
|
+
hasDefault = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!hasDefault) {
|
|
100
|
+
console.warn(`Warning: No default export or route found in ${lambdaName}. Handler not injected.`);
|
|
101
|
+
return code;
|
|
102
|
+
}
|
|
103
|
+
// Append the handler wrapper
|
|
104
|
+
const handlerWrapper = `
|
|
105
|
+
// ============================================
|
|
106
|
+
// Auto-injected Lambda handler by elysia-apigw
|
|
107
|
+
// ============================================
|
|
108
|
+
import { Hono as __Hono } from "hono/tiny";
|
|
109
|
+
import { handle as __handle } from "hono/aws-lambda";
|
|
110
|
+
|
|
111
|
+
// Re-export with handler wrapper
|
|
112
|
+
// The default export should be an Elysia instance
|
|
113
|
+
const __route = (await import("./index.mjs")).default;
|
|
114
|
+
|
|
115
|
+
function __createElysiaHandler(route) {
|
|
116
|
+
if (!route || typeof route.fetch !== "function") {
|
|
117
|
+
throw new Error("Default export must be an Elysia instance with .fetch method");
|
|
118
|
+
}
|
|
119
|
+
const api = new __Hono().mount("/", route.fetch);
|
|
120
|
+
return __handle(api);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const handler = __createElysiaHandler(__route);
|
|
124
|
+
`;
|
|
125
|
+
return modifiedCode + handlerWrapper;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Create a wrapper entry file that imports and re-exports with handler
|
|
129
|
+
*/
|
|
130
|
+
export function createWrapperEntry(originalPath) {
|
|
131
|
+
return `
|
|
132
|
+
import route from "${originalPath}";
|
|
133
|
+
import { Hono } from "hono/tiny";
|
|
134
|
+
import { handle } from "hono/aws-lambda";
|
|
135
|
+
|
|
136
|
+
// Re-export the route as default for introspection
|
|
137
|
+
export default route;
|
|
138
|
+
|
|
139
|
+
// Create and export the Lambda handler
|
|
140
|
+
const api = new Hono().mount("/", route.fetch);
|
|
141
|
+
export const handler = handle(api);
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route manifest generation through introspection of built lambdas
|
|
3
|
+
*/
|
|
4
|
+
export interface RouteInfo {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
pathParameters: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface LambdaManifest {
|
|
10
|
+
name: string;
|
|
11
|
+
routes: RouteInfo[];
|
|
12
|
+
}
|
|
13
|
+
export interface ApiRoute {
|
|
14
|
+
method: string;
|
|
15
|
+
path: string;
|
|
16
|
+
lambda: string;
|
|
17
|
+
pathParameters: string[];
|
|
18
|
+
apiGatewayPath: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ApiManifest {
|
|
21
|
+
lambdas: LambdaManifest[];
|
|
22
|
+
routes: ApiRoute[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Convert Elysia path params (:id) to API Gateway format ({id})
|
|
26
|
+
*/
|
|
27
|
+
export declare function convertToApiGatewayPath(path: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Extract path parameters from a route path
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractPathParameters(path: string): string[];
|
|
32
|
+
/**
|
|
33
|
+
* Generate a valid Terraform resource name from route info
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateRouteName(lambda: string, method: string, path: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Generate manifest by introspecting built lambda modules
|
|
38
|
+
*/
|
|
39
|
+
export declare function generateManifest(lambdaFiles: string[], outputDir: string, openapiEnabled?: boolean, apiName?: string): Promise<ApiManifest>;
|
|
40
|
+
/**
|
|
41
|
+
* Write manifest to JSON file
|
|
42
|
+
*/
|
|
43
|
+
export declare function writeManifest(manifest: ApiManifest, outputPath: string): Promise<void>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route manifest generation through introspection of built lambdas
|
|
3
|
+
*/
|
|
4
|
+
import { join, isAbsolute } from "path";
|
|
5
|
+
import { getLambdaBundleName } from "./naming";
|
|
6
|
+
/**
|
|
7
|
+
* Convert Elysia path params (:id) to API Gateway format ({id})
|
|
8
|
+
*/
|
|
9
|
+
export function convertToApiGatewayPath(path) {
|
|
10
|
+
return path.replace(/:([^/]+)/g, "{$1}");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Extract path parameters from a route path
|
|
14
|
+
*/
|
|
15
|
+
export function extractPathParameters(path) {
|
|
16
|
+
const matches = path.match(/:([^/]+)/g);
|
|
17
|
+
return matches ? matches.map((m) => m.slice(1)) : [];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate a valid Terraform resource name from route info
|
|
21
|
+
*/
|
|
22
|
+
export function generateRouteName(lambda, method, path) {
|
|
23
|
+
const sanitizedPath = path
|
|
24
|
+
.replace(/^\//, "")
|
|
25
|
+
.replace(/\//g, "_")
|
|
26
|
+
.replace(/:/g, "")
|
|
27
|
+
.replace(/[{}]/g, "")
|
|
28
|
+
.replace(/_$/, "");
|
|
29
|
+
return `${lambda}_${method.toLowerCase()}_${sanitizedPath || "root"}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate manifest by introspecting built lambda modules
|
|
33
|
+
*/
|
|
34
|
+
export async function generateManifest(lambdaFiles, outputDir, openapiEnabled = true, apiName = "") {
|
|
35
|
+
const manifest = {
|
|
36
|
+
lambdas: [],
|
|
37
|
+
routes: [],
|
|
38
|
+
};
|
|
39
|
+
// Track routes with their source lambda for conflict detection
|
|
40
|
+
const routeOwners = new Map();
|
|
41
|
+
const conflicts = [];
|
|
42
|
+
const sortedFiles = [...lambdaFiles].sort();
|
|
43
|
+
for (const file of sortedFiles) {
|
|
44
|
+
const originalName = file.replace(/\.ts$/, "");
|
|
45
|
+
// Use prefixed bundle name for file lookup
|
|
46
|
+
const bundleName = apiName ? getLambdaBundleName(apiName, originalName) : originalName;
|
|
47
|
+
const modulePath = isAbsolute(outputDir)
|
|
48
|
+
? join(outputDir, `${bundleName}.js`)
|
|
49
|
+
: join(process.cwd(), outputDir, `${bundleName}.js`);
|
|
50
|
+
// Add cache-busting query param to force fresh import
|
|
51
|
+
const module = await import(`${modulePath}?t=${Date.now()}`);
|
|
52
|
+
const lambdaManifest = {
|
|
53
|
+
name: bundleName,
|
|
54
|
+
routes: [],
|
|
55
|
+
};
|
|
56
|
+
// Find all exported Elysia routes (objects with .routes array)
|
|
57
|
+
const exportedRoutes = Object.values(module).filter((exp) => exp !== null &&
|
|
58
|
+
typeof exp === "object" &&
|
|
59
|
+
"routes" in exp &&
|
|
60
|
+
Array.isArray(exp.routes));
|
|
61
|
+
for (const route of exportedRoutes) {
|
|
62
|
+
for (const r of route.routes) {
|
|
63
|
+
const method = r.method.toUpperCase();
|
|
64
|
+
const path = r.path;
|
|
65
|
+
const routeKey = `${method} ${path}`;
|
|
66
|
+
const pathParameters = extractPathParameters(path);
|
|
67
|
+
lambdaManifest.routes.push({
|
|
68
|
+
method,
|
|
69
|
+
path,
|
|
70
|
+
pathParameters,
|
|
71
|
+
});
|
|
72
|
+
// Determine which lambda should handle this route
|
|
73
|
+
let targetLambda = bundleName;
|
|
74
|
+
// OpenAPI routes always go to the __openapi__ lambda if enabled
|
|
75
|
+
if (openapiEnabled && path.startsWith("/openapi")) {
|
|
76
|
+
targetLambda = apiName ? getLambdaBundleName(apiName, "__openapi__") : "__openapi__";
|
|
77
|
+
}
|
|
78
|
+
else if (originalName === "__openapi__") {
|
|
79
|
+
// Skip non-openapi routes from the openapi aggregator lambda
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Check for route conflicts
|
|
83
|
+
const existingOwner = routeOwners.get(routeKey);
|
|
84
|
+
if (existingOwner && existingOwner !== targetLambda) {
|
|
85
|
+
const existingConflict = conflicts.find((c) => c.route === routeKey);
|
|
86
|
+
if (existingConflict) {
|
|
87
|
+
if (!existingConflict.lambdas.includes(targetLambda)) {
|
|
88
|
+
existingConflict.lambdas.push(targetLambda);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
conflicts.push({
|
|
93
|
+
route: routeKey,
|
|
94
|
+
lambdas: [existingOwner, targetLambda],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!routeOwners.has(routeKey)) {
|
|
100
|
+
routeOwners.set(routeKey, targetLambda);
|
|
101
|
+
manifest.routes.push({
|
|
102
|
+
method,
|
|
103
|
+
path,
|
|
104
|
+
lambda: targetLambda,
|
|
105
|
+
pathParameters,
|
|
106
|
+
apiGatewayPath: convertToApiGatewayPath(path),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
manifest.lambdas.push(lambdaManifest);
|
|
112
|
+
}
|
|
113
|
+
// Report conflicts and fail if any exist
|
|
114
|
+
if (conflicts.length > 0) {
|
|
115
|
+
const conflictMessages = conflicts
|
|
116
|
+
.map((c) => ` ${c.route} is defined in: ${c.lambdas.join(", ")}`)
|
|
117
|
+
.join("\n");
|
|
118
|
+
throw new Error(`Route conflicts detected:\n${conflictMessages}\n\nEach route must be handled by exactly one lambda.`);
|
|
119
|
+
}
|
|
120
|
+
return manifest;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Write manifest to JSON file
|
|
124
|
+
*/
|
|
125
|
+
export async function writeManifest(manifest, outputPath) {
|
|
126
|
+
await Bun.write(outputPath, JSON.stringify(manifest, null, 2));
|
|
127
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Naming utilities for lambda bundles
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Generate the prefixed lambda name for bundle files
|
|
6
|
+
* Format: {apiName}-{lambdaName}
|
|
7
|
+
*/
|
|
8
|
+
export declare function getLambdaBundleName(apiName: string, lambdaName: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Extract the original lambda name from a prefixed bundle name
|
|
11
|
+
*/
|
|
12
|
+
export declare function getOriginalLambdaName(apiName: string, bundleName: string): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Naming utilities for lambda bundles
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Generate the prefixed lambda name for bundle files
|
|
6
|
+
* Format: {apiName}-{lambdaName}
|
|
7
|
+
*/
|
|
8
|
+
export function getLambdaBundleName(apiName, lambdaName) {
|
|
9
|
+
return `${apiName}-${lambdaName}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extract the original lambda name from a prefixed bundle name
|
|
13
|
+
*/
|
|
14
|
+
export function getOriginalLambdaName(apiName, bundleName) {
|
|
15
|
+
const prefix = `${apiName}-`;
|
|
16
|
+
if (bundleName.startsWith(prefix)) {
|
|
17
|
+
return bundleName.slice(prefix.length);
|
|
18
|
+
}
|
|
19
|
+
return bundleName;
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI auto-aggregation
|
|
3
|
+
*
|
|
4
|
+
* Automatically generates an OpenAPI lambda that aggregates all routes
|
|
5
|
+
* from all other lambdas to provide a complete API specification.
|
|
6
|
+
*/
|
|
7
|
+
import type { ResolvedConfig } from "./config";
|
|
8
|
+
/**
|
|
9
|
+
* Generate the OpenAPI aggregator lambda source code
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateOpenApiLambdaSource(lambdaFiles: string[], config: ResolvedConfig): string;
|
|
12
|
+
/**
|
|
13
|
+
* Write the OpenAPI aggregator lambda to a temp location for bundling
|
|
14
|
+
*/
|
|
15
|
+
export declare function writeOpenApiLambda(lambdaFiles: string[], lambdasDir: string, config: ResolvedConfig): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Check if OpenAPI aggregator needs to be generated
|
|
18
|
+
*/
|
|
19
|
+
export declare function shouldGenerateOpenApi(config: ResolvedConfig): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Clean up the generated OpenAPI lambda file
|
|
22
|
+
*/
|
|
23
|
+
export declare function cleanupOpenApiLambda(lambdasDir: string): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI auto-aggregation
|
|
3
|
+
*
|
|
4
|
+
* Automatically generates an OpenAPI lambda that aggregates all routes
|
|
5
|
+
* from all other lambdas to provide a complete API specification.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
/**
|
|
9
|
+
* Generate the OpenAPI aggregator lambda source code
|
|
10
|
+
*/
|
|
11
|
+
export function generateOpenApiLambdaSource(lambdaFiles, config) {
|
|
12
|
+
// Filter out any existing openapi file to avoid circular imports
|
|
13
|
+
const routeFiles = lambdaFiles.filter((f) => !f.includes("openapi") && !f.startsWith("__"));
|
|
14
|
+
const imports = routeFiles
|
|
15
|
+
.map((file, index) => {
|
|
16
|
+
const name = file.replace(/\.ts$/, "");
|
|
17
|
+
return `import route${index} from "./${name}";`;
|
|
18
|
+
})
|
|
19
|
+
.join("\n");
|
|
20
|
+
const uses = routeFiles.map((_, index) => `.use(route${index})`).join("\n\t\t");
|
|
21
|
+
return `/**
|
|
22
|
+
* Auto-generated OpenAPI aggregator lambda
|
|
23
|
+
* Generated by elysia-apigw - DO NOT EDIT
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import Elysia from "elysia";
|
|
27
|
+
import { openapi } from "@elysiajs/openapi";
|
|
28
|
+
import { Hono } from "hono/tiny";
|
|
29
|
+
import { handle } from "hono/aws-lambda";
|
|
30
|
+
|
|
31
|
+
${imports}
|
|
32
|
+
|
|
33
|
+
const app = new Elysia()
|
|
34
|
+
.use(
|
|
35
|
+
openapi({
|
|
36
|
+
documentation: {
|
|
37
|
+
info: {
|
|
38
|
+
title: ${JSON.stringify(config.openapi.title)},
|
|
39
|
+
version: ${JSON.stringify(config.openapi.version)},
|
|
40
|
+
description: ${JSON.stringify(config.openapi.description)},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
${uses ? `\t${uses}` : ""};
|
|
46
|
+
|
|
47
|
+
export default app;
|
|
48
|
+
|
|
49
|
+
// Lambda handler
|
|
50
|
+
const api = new Hono().mount("/", app.fetch);
|
|
51
|
+
export const handler = handle(api);
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Write the OpenAPI aggregator lambda to a temp location for bundling
|
|
56
|
+
*/
|
|
57
|
+
export async function writeOpenApiLambda(lambdaFiles, lambdasDir, config) {
|
|
58
|
+
const source = generateOpenApiLambdaSource(lambdaFiles, config);
|
|
59
|
+
const outputPath = join(lambdasDir, "__openapi__.ts");
|
|
60
|
+
await Bun.write(outputPath, source);
|
|
61
|
+
return outputPath;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if OpenAPI aggregator needs to be generated
|
|
65
|
+
*/
|
|
66
|
+
export function shouldGenerateOpenApi(config) {
|
|
67
|
+
return config.openapi.enabled;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Clean up the generated OpenAPI lambda file
|
|
71
|
+
*/
|
|
72
|
+
export async function cleanupOpenApiLambda(lambdasDir) {
|
|
73
|
+
const openApiPath = join(lambdasDir, "__openapi__.ts");
|
|
74
|
+
try {
|
|
75
|
+
const file = Bun.file(openApiPath);
|
|
76
|
+
if (await file.exists()) {
|
|
77
|
+
await Bun.write(openApiPath, ""); // Clear content
|
|
78
|
+
const { unlinkSync } = await import("fs");
|
|
79
|
+
unlinkSync(openApiPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Ignore cleanup errors
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda packager - creates zip files for AWS Lambda deployment
|
|
3
|
+
*/
|
|
4
|
+
export interface PackageResult {
|
|
5
|
+
name: string;
|
|
6
|
+
zipPath: string;
|
|
7
|
+
success: boolean;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Package a bundled lambda into a zip file
|
|
12
|
+
*/
|
|
13
|
+
export declare function packageLambda(name: string, jsFilePath: string, outputDir: string): Promise<PackageResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Package all bundled lambdas
|
|
16
|
+
*/
|
|
17
|
+
export declare function packageAllLambdas(lambdaNames: string[], outputDir: string): Promise<PackageResult[]>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda packager - creates zip files for AWS Lambda deployment
|
|
3
|
+
*/
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { mkdirSync, rmSync, existsSync, copyFileSync } from "fs";
|
|
7
|
+
/**
|
|
8
|
+
* Package a bundled lambda into a zip file
|
|
9
|
+
*/
|
|
10
|
+
export async function packageLambda(name, jsFilePath, outputDir) {
|
|
11
|
+
const lambdaDir = join(outputDir, `__${name}_pkg__`);
|
|
12
|
+
const zipPath = join(outputDir, `${name}.zip`);
|
|
13
|
+
try {
|
|
14
|
+
// Create temp directory for packaging
|
|
15
|
+
mkdirSync(lambdaDir, { recursive: true });
|
|
16
|
+
// Copy JS file as index.mjs (Lambda expects this name)
|
|
17
|
+
copyFileSync(jsFilePath, join(lambdaDir, "index.mjs"));
|
|
18
|
+
// Create zip file
|
|
19
|
+
execSync(`zip -qj "${zipPath}" "${join(lambdaDir, "index.mjs")}"`, {
|
|
20
|
+
cwd: outputDir,
|
|
21
|
+
});
|
|
22
|
+
// Clean up temp directory
|
|
23
|
+
rmSync(lambdaDir, { recursive: true, force: true });
|
|
24
|
+
return {
|
|
25
|
+
name,
|
|
26
|
+
zipPath,
|
|
27
|
+
success: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
// Clean up on error
|
|
32
|
+
if (existsSync(lambdaDir)) {
|
|
33
|
+
rmSync(lambdaDir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
zipPath,
|
|
38
|
+
success: false,
|
|
39
|
+
error: error instanceof Error ? error.message : String(error),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Package all bundled lambdas
|
|
45
|
+
*/
|
|
46
|
+
export async function packageAllLambdas(lambdaNames, outputDir) {
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const name of lambdaNames) {
|
|
49
|
+
const jsFilePath = join(outputDir, `${name}.js`);
|
|
50
|
+
const result = await packageLambda(name, jsFilePath, outputDir);
|
|
51
|
+
results.push(result);
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terraform output generation
|
|
3
|
+
*/
|
|
4
|
+
import type { ApiManifest } from "./manifest";
|
|
5
|
+
import type { ResolvedConfig } from "./config";
|
|
6
|
+
/**
|
|
7
|
+
* Generate Terraform tfvars content from manifest
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateTerraformVars(manifest: ApiManifest, config: ResolvedConfig): string;
|
|
10
|
+
/**
|
|
11
|
+
* Write Terraform tfvars file
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeTerraformVars(manifest: ApiManifest, config: ResolvedConfig): Promise<string>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terraform output generation
|
|
3
|
+
*/
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { generateRouteName } from "./manifest";
|
|
6
|
+
/**
|
|
7
|
+
* Generate Terraform tfvars content from manifest
|
|
8
|
+
*/
|
|
9
|
+
export function generateTerraformVars(manifest, config) {
|
|
10
|
+
// Get unique lambda names from routes (excludes __openapi__ prefix for cleaner naming)
|
|
11
|
+
const lambdaNames = [...new Set(manifest.routes.map((r) => r.lambda))];
|
|
12
|
+
const routeEntries = manifest.routes.map((route) => {
|
|
13
|
+
const routeName = generateRouteName(route.lambda, route.method, route.path);
|
|
14
|
+
const apiGatewayRouteKey = `${route.method} ${route.apiGatewayPath}`;
|
|
15
|
+
return ` ${routeName} = {
|
|
16
|
+
lambda_key = "${route.lambda}"
|
|
17
|
+
route_key = "${apiGatewayRouteKey}"
|
|
18
|
+
path_parameters = ${JSON.stringify(route.pathParameters)}
|
|
19
|
+
}`;
|
|
20
|
+
});
|
|
21
|
+
return `# Auto-generated by elysian - DO NOT EDIT
|
|
22
|
+
# Generated at: ${new Date().toISOString()}
|
|
23
|
+
|
|
24
|
+
lambda_names = ${JSON.stringify(lambdaNames)}
|
|
25
|
+
|
|
26
|
+
api_routes = {
|
|
27
|
+
${routeEntries.join("\n")}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Lambda configuration defaults
|
|
31
|
+
lambda_runtime = "${config.lambda.runtime}"
|
|
32
|
+
lambda_memory_size = ${config.lambda.memorySize}
|
|
33
|
+
lambda_timeout = ${config.lambda.timeout}
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Write Terraform tfvars file
|
|
38
|
+
*/
|
|
39
|
+
export async function writeTerraformVars(manifest, config) {
|
|
40
|
+
const tfvarsContent = generateTerraformVars(manifest, config);
|
|
41
|
+
const outputPath = join(process.cwd(), config.terraform.outputDir, config.terraform.tfvarsFilename);
|
|
42
|
+
await Bun.write(outputPath, tfvarsContent);
|
|
43
|
+
return outputPath;
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package version - read from jsr.json or package.json
|
|
3
|
+
*/
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { join, dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
let version = "0.0.0";
|
|
8
|
+
try {
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
// Try jsr.json first (for JSR published package - src/core/version.ts -> ../../jsr.json)
|
|
12
|
+
try {
|
|
13
|
+
const jsr = require(join(__dirname, "../../jsr.json"));
|
|
14
|
+
version = jsr.version;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Try package.json (for npm/local - dist/core/version.js -> ../../package.json)
|
|
18
|
+
try {
|
|
19
|
+
const pkg = require(join(__dirname, "../../package.json"));
|
|
20
|
+
version = pkg.version;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Fallback already set to 0.0.0
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Fallback if nothing works
|
|
29
|
+
}
|
|
30
|
+
export { version };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* elysian - Automatic Lambda bundler for Elysia with API Gateway integration
|
|
3
|
+
*
|
|
4
|
+
* Main library exports for use in configuration and lambda files.
|
|
5
|
+
*/
|
|
6
|
+
import Elysia from "elysia";
|
|
7
|
+
export { defineConfig, type ElysianConfig, type ResolvedConfig } from "./core/config";
|
|
8
|
+
export { t } from "elysia";
|
|
9
|
+
export type { AnyElysia } from "elysia";
|
|
10
|
+
export { createHandler } from "./runtime/adapter";
|
|
11
|
+
/**
|
|
12
|
+
* Set the OpenAPI configuration (called by build process)
|
|
13
|
+
*/
|
|
14
|
+
export declare function setOpenApiConfig(config: {
|
|
15
|
+
title?: string;
|
|
16
|
+
version?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
}): void;
|
|
19
|
+
/**
|
|
20
|
+
* Create a new Elysia instance pre-configured for Lambda use
|
|
21
|
+
*
|
|
22
|
+
* This is the main entry point for defining Lambda routes.
|
|
23
|
+
* The returned Elysia instance has OpenAPI support built-in.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { createLambda, t } from "@actuallyjamez/elysian";
|
|
28
|
+
*
|
|
29
|
+
* export default createLambda()
|
|
30
|
+
* .get("/hello", () => "Hello World", {
|
|
31
|
+
* response: t.String(),
|
|
32
|
+
* })
|
|
33
|
+
* .get("/users/:id", ({ params }) => getUser(params.id), {
|
|
34
|
+
* params: t.Object({ id: t.String() }),
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function createLambda(): Elysia;
|