@function-bay/nodejs 1.0.0

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/dist/index.cjs ADDED
@@ -0,0 +1,326 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ build: () => build
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var import_build2 = require("@function-bay/build");
37
+ var import_validation = require("@lowerdeck/validation");
38
+ var import_bun = require("bun");
39
+ var import_promises2 = __toESM(require("fs/promises"), 1);
40
+ var import_path2 = __toESM(require("path"), 1);
41
+
42
+ // src/launcher.ts
43
+ var import_build = require("@function-bay/build");
44
+ var getMetorialLauncher = async (opts) => {
45
+ if (import_build.Runtime.provider == "aws.lambda") {
46
+ return {
47
+ handler: "metorial_launcher.handler",
48
+ entrypoint: "metorial_launcher.js",
49
+ files: [
50
+ {
51
+ filename: "metorial_launcher.js",
52
+ content: `// Auto-generated by Metorial Function Bay
53
+ import * as handlerModule from './${opts.bundledEntrypoint}';
54
+
55
+ let handler = handlerModule;
56
+
57
+ exports.handler = async (event) => {
58
+ // Load the entrypoint module
59
+ if (typeof handler == 'object') {
60
+ if (typeof handler.default === 'function') {
61
+ handler = handler.default;
62
+ } else if (typeof handler.handler === 'function') {
63
+ handler = handler.handler;
64
+ }
65
+ }
66
+
67
+ if (typeof handler !== 'function') {
68
+ return {
69
+ statusCode: 500,
70
+ body: JSON.stringify({ error: { code: 'function_bay.launcher', message: "Entrypoint does not export a valid handler function" } })
71
+ };
72
+ }
73
+
74
+ let rawPayload = event.payload;
75
+ if (!rawPayload) {
76
+ return {
77
+ statusCode: 400,
78
+ body: JSON.stringify({ error: { code: 'function_bay.launcher', message: "Missing payload field" } })
79
+ };
80
+ }
81
+
82
+ let data;
83
+ try {
84
+ data = JSON.parse(rawPayload);
85
+ } catch (err) {
86
+ return {
87
+ statusCode: 400,
88
+ body: JSON.stringify({ error: { code: 'function_bay.launcher', message: "Invalid JSON payload" } })
89
+ };
90
+ }
91
+
92
+ try {
93
+ let result = await handler(data);
94
+
95
+ return {
96
+ statusCode: 200,
97
+ body: JSON.stringify({
98
+ message: "Payload received",
99
+ received: data
100
+ })
101
+ };
102
+ } catch (err) {
103
+ if (typeof err == 'object' && err.__function_bay_error) {
104
+ let result = err.toResponse();
105
+ return {
106
+ statusCode: result.statusCode || 500,
107
+ body: JSON.stringify(result)
108
+ };
109
+ }
110
+
111
+ let fullMessage = '';
112
+ if (typeof err == 'object' && err !== null) {
113
+ if ('name' in err && typeof err.name === 'string') {
114
+ fullMessage += \`[\${err.name}]: \\n\`;
115
+ }
116
+
117
+ if ('message' in err && typeof err.message === 'string') {
118
+ fullMessage += err.message;
119
+ }
120
+
121
+ if ('stack' in err && typeof err.stack === 'string') {
122
+ fullMessage += '\\n' + err.stack;
123
+ }
124
+ } else {
125
+ try {
126
+ fullMessage = JSON.stringify(err);
127
+ } catch {
128
+ fullMessage = String(err || 'Unknown error');
129
+ }
130
+ }
131
+
132
+ return {
133
+ statusCode: 500,
134
+ body: JSON.stringify({ error: { code: 'function_bay.handler_error', message: fullMessage } })
135
+ };
136
+ }
137
+ };
138
+ `
139
+ },
140
+ {
141
+ filename: "tsconfig.json",
142
+ content: JSON.stringify(
143
+ {
144
+ compilerOptions: {
145
+ lib: ["ESNext"],
146
+ target: "es2020",
147
+ jsx: "react-jsx",
148
+ allowJs: true,
149
+ moduleResolution: "bundler",
150
+ verbatimModuleSyntax: false,
151
+ strict: false,
152
+ skipLibCheck: true,
153
+ noFallthroughCasesInSwitch: false,
154
+ noUncheckedIndexedAccess: false,
155
+ noImplicitOverride: false,
156
+ noUnusedLocals: false,
157
+ noUnusedParameters: false,
158
+ noPropertyAccessFromIndexSignature: false
159
+ }
160
+ },
161
+ null,
162
+ 2
163
+ )
164
+ }
165
+ ]
166
+ };
167
+ }
168
+ throw new Error(`Unsupported provider for Node.js runtime: ${import_build.Runtime.provider}`);
169
+ };
170
+
171
+ // src/lib/cleanup.ts
172
+ var cleanup = (arr) => arr.filter((item) => item != null);
173
+
174
+ // src/lib/fs.ts
175
+ var import_fs = __toESM(require("fs"), 1);
176
+ var import_promises = __toESM(require("fs/promises"), 1);
177
+ var import_path = __toESM(require("path"), 1);
178
+ var readJsonFile = async (inPath) => {
179
+ let content = await import_promises.default.readFile(import_path.default.join(process.cwd(), inPath), "utf-8");
180
+ return JSON.parse(content);
181
+ };
182
+ var readJsonFileOptional = async (inPath) => {
183
+ if (!await fileExists(inPath)) {
184
+ return null;
185
+ }
186
+ return readJsonFile(inPath);
187
+ };
188
+ var fileExists = async (inPath) => {
189
+ try {
190
+ await import_promises.default.access(import_path.default.join(process.cwd(), inPath));
191
+ return true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ };
196
+ var fileExistsSync = (inPath) => {
197
+ try {
198
+ import_fs.default.accessSync(import_path.default.join(process.cwd(), inPath));
199
+ return true;
200
+ } catch {
201
+ return false;
202
+ }
203
+ };
204
+
205
+ // src/index.ts
206
+ var $ = (...args) => {
207
+ let commandString = "";
208
+ let [templateStrings, ...values] = args;
209
+ for (let i = 0; i < templateStrings.length; i++) {
210
+ commandString += templateStrings[i];
211
+ if (i < values.length) {
212
+ commandString += values[i];
213
+ }
214
+ }
215
+ let bashPrefix = 'bash -c "';
216
+ if (commandString.startsWith(bashPrefix)) {
217
+ commandString = commandString.slice(bashPrefix.length, -1);
218
+ }
219
+ console.log(`
220
+ $ ${commandString}`);
221
+ return (0, import_bun.$)(...args);
222
+ };
223
+ var spec = import_validation.v.object({
224
+ entrypoint: import_validation.v.optional(import_validation.v.string()),
225
+ scripts: import_validation.v.optional(
226
+ import_validation.v.object({
227
+ build: import_validation.v.optional(import_validation.v.string())
228
+ })
229
+ ),
230
+ nodeJsVersion: import_validation.v.optional(import_validation.v.string())
231
+ });
232
+ var differentJsTypes = [".ts", ".js", ".cjs", ".mjs"];
233
+ var tryExtensions = (base) => differentJsTypes.map((ext) => base + ext);
234
+ var potentialDirs = ["", "dist/", "build/", "output/", "out/"];
235
+ var tryDirs = (filenames) => filenames.flatMap((name) => potentialDirs.map((dir) => dir + name));
236
+ var build = async () => {
237
+ console.log("Building Node.js function...");
238
+ await $`bun i -g @vercel/ncc typescript`;
239
+ await $`curl https://get.volta.sh | bash`;
240
+ console.log("Setting up Node.js build environment...");
241
+ let packageJson = await readJsonFile(
242
+ "package.json"
243
+ );
244
+ let functionBayFile = await readJsonFileOptional("function-bay.json") ?? await readJsonFileOptional("metorial.json");
245
+ let nodeJsVersionIdentifierRaw = functionBayFile?.nodeJsVersion ?? process.env.NODE_VERSION ?? "24.x";
246
+ let nodeJsVersionIdentifier = nodeJsVersionIdentifierRaw.split(".")[0] + ".x";
247
+ console.log(`Using Node.js version identifier: ${nodeJsVersionIdentifier}`);
248
+ let env = { PATH: `${process.env.HOME}/.volta/bin:${process.env.PATH}` };
249
+ await $`bash -c "volta install node@${nodeJsVersionIdentifier}"`.env(env);
250
+ if (fileExistsSync("yarn.lock")) {
251
+ console.log("Detected yarn.lock, installing dependencies with Yarn...");
252
+ await $`bash -c "volta install yarn@1"`.env(env);
253
+ await $`bash -c "yarn install"`.env(env);
254
+ } else if (fileExistsSync("pnpm-lock.yaml")) {
255
+ console.log("Detected pnpm-lock.yaml, installing dependencies with pnpm...");
256
+ await $`bash -c "volta install pnpm"`.env(env);
257
+ await $`bash -c "pnpm install"`.env(env);
258
+ } else if (fileExistsSync("bun.lock")) {
259
+ console.log("Detected bun.lock, installing dependencies with Bun...");
260
+ await $`bash -c "volta install bun"`.env(env);
261
+ await $`bash -c "bun install"`.env(env);
262
+ } else {
263
+ console.log("Installing dependencies with npm...");
264
+ await $`bash -c "npm install"`.env(env);
265
+ }
266
+ if (functionBayFile?.scripts?.build) {
267
+ let buildScript = functionBayFile.scripts.build;
268
+ console.log(`Detected build script: "${buildScript}"`);
269
+ await $`bash -c ${buildScript}`.env(env);
270
+ } else if (packageJson.scripts?.build) {
271
+ console.log(`Detected build script in package.json: "${packageJson.scripts.build}"`);
272
+ await $`bash -c "npm run build"`.env(env);
273
+ } else {
274
+ console.log("No build script detected, skipping build step.");
275
+ }
276
+ let potentialEntrypoints = cleanup([
277
+ functionBayFile?.entrypoint,
278
+ packageJson.main,
279
+ ...tryDirs(tryExtensions("index")),
280
+ ...tryDirs(tryExtensions("main")),
281
+ ...tryDirs(tryExtensions("server")),
282
+ ...tryDirs(tryExtensions("function"))
283
+ ]).filter(fileExistsSync);
284
+ if (potentialEntrypoints.length === 0) {
285
+ throw new Error(
286
+ "Could not find entrypoint for function. Please specify one in function-bay.json"
287
+ );
288
+ }
289
+ let entrypoint = potentialEntrypoints[0];
290
+ console.log(`Detected entrypoint: ${entrypoint}`);
291
+ if (!functionBayFile?.entrypoint) {
292
+ console.log(
293
+ "You can specify this entrypoint in metorial.json to skip this detection step in the future."
294
+ );
295
+ }
296
+ console.log("Bundling function to Metorial Function Bay format...");
297
+ let launcher = await getMetorialLauncher({
298
+ bundledEntrypoint: entrypoint
299
+ });
300
+ for (let file of launcher.files) {
301
+ await import_promises2.default.writeFile(import_path2.default.join(process.cwd(), file.filename), file.content, "utf-8");
302
+ }
303
+ let outputTempDir = await (0, import_build2.tempDir)();
304
+ await $`ncc build ${launcher.entrypoint} -o ${outputTempDir} --minify --source-map --debug --target es2020`.env(
305
+ env
306
+ );
307
+ console.log("\nCreating function package...");
308
+ await import_build2.Function.create({
309
+ runtime: {
310
+ identifier: "@function-bay/nodejs",
311
+ layer: import_build2.Runtime.layer,
312
+ handler: launcher.handler,
313
+ runtime: {
314
+ identifier: "nodejs",
315
+ version: nodeJsVersionIdentifier
316
+ }
317
+ },
318
+ directory: outputTempDir
319
+ });
320
+ console.log("Function package created successfully.");
321
+ };
322
+ // Annotate the CommonJS export names for ESM import in node:
323
+ 0 && (module.exports = {
324
+ build
325
+ });
326
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/launcher.ts","../src/lib/cleanup.ts","../src/lib/fs.ts"],"sourcesContent":["import { Function, Runtime, tempDir } from '@function-bay/build';\nimport { v, ValidationTypeValue } from '@lowerdeck/validation';\nimport { $ as _$ } from 'bun';\nimport fs from 'fs/promises';\nimport path from 'path';\nimport { getMetorialLauncher } from './launcher';\nimport { cleanup } from './lib/cleanup';\nimport { fileExistsSync, readJsonFile, readJsonFileOptional } from './lib/fs';\n\nlet $ = (...args: Parameters<typeof _$>) => {\n let commandString = '';\n let [templateStrings, ...values] = args;\n for (let i = 0; i < templateStrings.length; i++) {\n commandString += templateStrings[i];\n if (i < values.length) {\n commandString += values[i];\n }\n }\n\n let bashPrefix = 'bash -c \"';\n if (commandString.startsWith(bashPrefix)) {\n commandString = commandString.slice(bashPrefix.length, -1);\n }\n\n console.log(`\\n$ ${commandString}`);\n\n return _$(...args);\n};\n\nlet spec = v.object({\n entrypoint: v.optional(v.string()),\n scripts: v.optional(\n v.object({\n build: v.optional(v.string())\n })\n ),\n nodeJsVersion: v.optional(v.string())\n});\n\ntype Spec = ValidationTypeValue<typeof spec>;\n\nlet differentJsTypes = ['.ts', '.js', '.cjs', '.mjs'];\nlet tryExtensions = (base: string) => differentJsTypes.map(ext => base + ext);\n\nlet potentialDirs = ['', 'dist/', 'build/', 'output/', 'out/'];\nlet tryDirs = (filenames: string[]) =>\n filenames.flatMap(name => potentialDirs.map(dir => dir + name));\n\nexport let build = async (): Promise<void> => {\n console.log('Building Node.js function...');\n\n await $`bun i -g @vercel/ncc typescript`;\n await $`curl https://get.volta.sh | bash`;\n\n console.log('Setting up Node.js build environment...');\n\n let packageJson = await readJsonFile<{ main?: string; scripts?: { build?: string } }>(\n 'package.json'\n );\n let functionBayFile =\n (await readJsonFileOptional<Spec>('function-bay.json')) ??\n (await readJsonFileOptional<Spec>('metorial.json'));\n\n let nodeJsVersionIdentifierRaw =\n functionBayFile?.nodeJsVersion ?? process.env.NODE_VERSION ?? '24.x';\n let nodeJsVersionIdentifier = nodeJsVersionIdentifierRaw.split('.')[0] + '.x';\n\n console.log(`Using Node.js version identifier: ${nodeJsVersionIdentifier}`);\n\n let env = { PATH: `${process.env.HOME}/.volta/bin:${process.env.PATH}` };\n\n await $`bash -c \"volta install node@${nodeJsVersionIdentifier}\"`.env(env);\n\n if (fileExistsSync('yarn.lock')) {\n console.log('Detected yarn.lock, installing dependencies with Yarn...');\n await $`bash -c \"volta install yarn@1\"`.env(env);\n await $`bash -c \"yarn install\"`.env(env);\n } else if (fileExistsSync('pnpm-lock.yaml')) {\n console.log('Detected pnpm-lock.yaml, installing dependencies with pnpm...');\n await $`bash -c \"volta install pnpm\"`.env(env);\n await $`bash -c \"pnpm install\"`.env(env);\n } else if (fileExistsSync('bun.lock')) {\n console.log('Detected bun.lock, installing dependencies with Bun...');\n await $`bash -c \"volta install bun\"`.env(env);\n await $`bash -c \"bun install\"`.env(env);\n } else {\n console.log('Installing dependencies with npm...');\n await $`bash -c \"npm install\"`.env(env);\n }\n\n if (functionBayFile?.scripts?.build) {\n let buildScript = functionBayFile.scripts.build;\n console.log(`Detected build script: \"${buildScript}\"`);\n await $`bash -c ${buildScript}`.env(env);\n } else if (packageJson.scripts?.build) {\n console.log(`Detected build script in package.json: \"${packageJson.scripts.build}\"`);\n await $`bash -c \"npm run build\"`.env(env);\n } else {\n console.log('No build script detected, skipping build step.');\n }\n\n let potentialEntrypoints = cleanup([\n functionBayFile?.entrypoint,\n packageJson.main,\n\n ...tryDirs(tryExtensions('index')),\n ...tryDirs(tryExtensions('main')),\n ...tryDirs(tryExtensions('server')),\n ...tryDirs(tryExtensions('function'))\n ]).filter(fileExistsSync);\n\n if (potentialEntrypoints.length === 0) {\n throw new Error(\n 'Could not find entrypoint for function. Please specify one in function-bay.json'\n );\n }\n\n let entrypoint = potentialEntrypoints[0];\n console.log(`Detected entrypoint: ${entrypoint}`);\n if (!functionBayFile?.entrypoint) {\n console.log(\n 'You can specify this entrypoint in metorial.json to skip this detection step in the future.'\n );\n }\n\n console.log('Bundling function to Metorial Function Bay format...');\n\n let launcher = await getMetorialLauncher({\n bundledEntrypoint: entrypoint\n });\n for (let file of launcher.files) {\n await fs.writeFile(path.join(process.cwd(), file.filename), file.content, 'utf-8');\n }\n\n let outputTempDir = await tempDir();\n await $`ncc build ${launcher.entrypoint} -o ${outputTempDir} --minify --source-map --debug --target es2020`.env(\n env\n );\n\n console.log('\\nCreating function package...');\n\n await Function.create({\n runtime: {\n identifier: '@function-bay/nodejs',\n layer: Runtime.layer,\n handler: launcher.handler,\n runtime: {\n identifier: 'nodejs',\n version: nodeJsVersionIdentifier as '24.x'\n }\n },\n directory: outputTempDir\n });\n\n console.log('Function package created successfully.');\n};\n","import { Runtime } from '@function-bay/build';\n\nexport let getMetorialLauncher = async (opts: { bundledEntrypoint: string }) => {\n if (Runtime.provider == 'aws.lambda') {\n return {\n handler: 'metorial_launcher.handler',\n entrypoint: 'metorial_launcher.js',\n files: [\n {\n filename: 'metorial_launcher.js',\n content: `// Auto-generated by Metorial Function Bay\nimport * as handlerModule from './${opts.bundledEntrypoint}';\n\nlet handler = handlerModule;\n\nexports.handler = async (event) => {\n // Load the entrypoint module\n if (typeof handler == 'object') {\n if (typeof handler.default === 'function') {\n handler = handler.default;\n } else if (typeof handler.handler === 'function') {\n handler = handler.handler;\n }\n }\n\n if (typeof handler !== 'function') {\n return {\n statusCode: 500,\n body: JSON.stringify({ error: { code: 'function_bay.launcher', message: \"Entrypoint does not export a valid handler function\" } })\n };\n }\n\n let rawPayload = event.payload;\n if (!rawPayload) {\n return {\n statusCode: 400,\n body: JSON.stringify({ error: { code: 'function_bay.launcher', message: \"Missing payload field\" } })\n };\n }\n\n let data;\n try {\n data = JSON.parse(rawPayload);\n } catch (err) {\n return {\n statusCode: 400,\n body: JSON.stringify({ error: { code: 'function_bay.launcher', message: \"Invalid JSON payload\" } })\n };\n }\n\n try {\n let result = await handler(data);\n\n return {\n statusCode: 200,\n body: JSON.stringify({\n message: \"Payload received\",\n received: data\n })\n };\n } catch (err) {\n if (typeof err == 'object' && err.__function_bay_error) {\n let result = err.toResponse();\n return {\n statusCode: result.statusCode || 500,\n body: JSON.stringify(result)\n };\n }\n\n let fullMessage = '';\n if (typeof err == 'object' && err !== null) {\n if ('name' in err && typeof err.name === 'string') {\n fullMessage += \\`[\\${err.name}]: \\\\n\\`;\n }\n\n if ('message' in err && typeof err.message === 'string') {\n fullMessage += err.message;\n }\n\n if ('stack' in err && typeof err.stack === 'string') {\n fullMessage += '\\\\n' + err.stack;\n }\n } else {\n try {\n fullMessage = JSON.stringify(err);\n } catch {\n fullMessage = String(err || 'Unknown error');\n }\n }\n\n return {\n statusCode: 500,\n body: JSON.stringify({ error: { code: 'function_bay.handler_error', message: fullMessage } })\n };\n }\n};\n`\n },\n\n {\n filename: 'tsconfig.json',\n content: JSON.stringify(\n {\n compilerOptions: {\n lib: ['ESNext'],\n target: 'es2020',\n jsx: 'react-jsx',\n allowJs: true,\n moduleResolution: 'bundler',\n verbatimModuleSyntax: false,\n strict: false,\n skipLibCheck: true,\n noFallthroughCasesInSwitch: false,\n noUncheckedIndexedAccess: false,\n noImplicitOverride: false,\n noUnusedLocals: false,\n noUnusedParameters: false,\n noPropertyAccessFromIndexSignature: false\n }\n },\n null,\n 2\n )\n }\n ]\n };\n }\n\n throw new Error(`Unsupported provider for Node.js runtime: ${Runtime.provider}`);\n};\n","export let cleanup = <T>(arr: (T | null | undefined)[]): T[] =>\n arr.filter((item): item is T => item != null);\n","import fsSync from 'fs';\nimport fs from 'fs/promises';\nimport path from 'path';\n\nexport let readJsonFile = async <T>(inPath: string): Promise<T> => {\n let content = await fs.readFile(path.join(process.cwd(), inPath), 'utf-8');\n return JSON.parse(content) as T;\n};\n\nexport let readJsonFileOptional = async <T>(inPath: string): Promise<T | null> => {\n if (!(await fileExists(inPath))) {\n return null;\n }\n\n return readJsonFile<T>(inPath);\n};\n\nexport let fileExists = async (inPath: string): Promise<boolean> => {\n try {\n await fs.access(path.join(process.cwd(), inPath));\n return true;\n } catch {\n return false;\n }\n};\n\nexport let fileExistsSync = (inPath: string): boolean => {\n try {\n fsSync.accessSync(path.join(process.cwd(), inPath));\n return true;\n } catch {\n return false;\n }\n};\n"],"mappings":"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","names":["import_build","import_promises","import_path","fs","path","fsSync","_$","fs","path"]}
@@ -0,0 +1,3 @@
1
+ declare let build: () => Promise<void>;
2
+
3
+ export { build };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@function-bay/nodejs",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "files": [
8
+ "src/**",
9
+ "dist/**",
10
+ "README.md",
11
+ "package.json"
12
+ ],
13
+ "author": "Tobias Herber",
14
+ "license": "Apache 2",
15
+ "type": "module",
16
+ "source": "src/index.ts",
17
+ "main": "./dist/index.cjs",
18
+ "scripts": {
19
+ "test": "vitest run --passWithNoTests",
20
+ "lint": "prettier src/**/*.ts --check",
21
+ "build": "rm -rf ./dist && tsup",
22
+ "prepublish": "npm run build"
23
+ },
24
+ "devDependencies": {
25
+ "microbundle": "^0.15.1",
26
+ "@function-bay/tsconfig": "^1.0.0",
27
+ "typescript": "^5.9.3",
28
+ "vitest": "^3.2.4",
29
+ "tsup": "^8.5.1",
30
+ "@types/bun": "latest"
31
+ },
32
+ "dependencies": {
33
+ "@function-bay/build": "^1.0.0",
34
+ "@function-bay/types": "^1.0.0",
35
+ "@lowerdeck/validation": "^1.0.4",
36
+ "@types/bun": "^1.3.5"
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,156 @@
1
+ import { Function, Runtime, tempDir } from '@function-bay/build';
2
+ import { v, ValidationTypeValue } from '@lowerdeck/validation';
3
+ import { $ as _$ } from 'bun';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { getMetorialLauncher } from './launcher';
7
+ import { cleanup } from './lib/cleanup';
8
+ import { fileExistsSync, readJsonFile, readJsonFileOptional } from './lib/fs';
9
+
10
+ let $ = (...args: Parameters<typeof _$>) => {
11
+ let commandString = '';
12
+ let [templateStrings, ...values] = args;
13
+ for (let i = 0; i < templateStrings.length; i++) {
14
+ commandString += templateStrings[i];
15
+ if (i < values.length) {
16
+ commandString += values[i];
17
+ }
18
+ }
19
+
20
+ let bashPrefix = 'bash -c "';
21
+ if (commandString.startsWith(bashPrefix)) {
22
+ commandString = commandString.slice(bashPrefix.length, -1);
23
+ }
24
+
25
+ console.log(`\n$ ${commandString}`);
26
+
27
+ return _$(...args);
28
+ };
29
+
30
+ let spec = v.object({
31
+ entrypoint: v.optional(v.string()),
32
+ scripts: v.optional(
33
+ v.object({
34
+ build: v.optional(v.string())
35
+ })
36
+ ),
37
+ nodeJsVersion: v.optional(v.string())
38
+ });
39
+
40
+ type Spec = ValidationTypeValue<typeof spec>;
41
+
42
+ let differentJsTypes = ['.ts', '.js', '.cjs', '.mjs'];
43
+ let tryExtensions = (base: string) => differentJsTypes.map(ext => base + ext);
44
+
45
+ let potentialDirs = ['', 'dist/', 'build/', 'output/', 'out/'];
46
+ let tryDirs = (filenames: string[]) =>
47
+ filenames.flatMap(name => potentialDirs.map(dir => dir + name));
48
+
49
+ export let build = async (): Promise<void> => {
50
+ console.log('Building Node.js function...');
51
+
52
+ await $`bun i -g @vercel/ncc typescript`;
53
+ await $`curl https://get.volta.sh | bash`;
54
+
55
+ console.log('Setting up Node.js build environment...');
56
+
57
+ let packageJson = await readJsonFile<{ main?: string; scripts?: { build?: string } }>(
58
+ 'package.json'
59
+ );
60
+ let functionBayFile =
61
+ (await readJsonFileOptional<Spec>('function-bay.json')) ??
62
+ (await readJsonFileOptional<Spec>('metorial.json'));
63
+
64
+ let nodeJsVersionIdentifierRaw =
65
+ functionBayFile?.nodeJsVersion ?? process.env.NODE_VERSION ?? '24.x';
66
+ let nodeJsVersionIdentifier = nodeJsVersionIdentifierRaw.split('.')[0] + '.x';
67
+
68
+ console.log(`Using Node.js version identifier: ${nodeJsVersionIdentifier}`);
69
+
70
+ let env = { PATH: `${process.env.HOME}/.volta/bin:${process.env.PATH}` };
71
+
72
+ await $`bash -c "volta install node@${nodeJsVersionIdentifier}"`.env(env);
73
+
74
+ if (fileExistsSync('yarn.lock')) {
75
+ console.log('Detected yarn.lock, installing dependencies with Yarn...');
76
+ await $`bash -c "volta install yarn@1"`.env(env);
77
+ await $`bash -c "yarn install"`.env(env);
78
+ } else if (fileExistsSync('pnpm-lock.yaml')) {
79
+ console.log('Detected pnpm-lock.yaml, installing dependencies with pnpm...');
80
+ await $`bash -c "volta install pnpm"`.env(env);
81
+ await $`bash -c "pnpm install"`.env(env);
82
+ } else if (fileExistsSync('bun.lock')) {
83
+ console.log('Detected bun.lock, installing dependencies with Bun...');
84
+ await $`bash -c "volta install bun"`.env(env);
85
+ await $`bash -c "bun install"`.env(env);
86
+ } else {
87
+ console.log('Installing dependencies with npm...');
88
+ await $`bash -c "npm install"`.env(env);
89
+ }
90
+
91
+ if (functionBayFile?.scripts?.build) {
92
+ let buildScript = functionBayFile.scripts.build;
93
+ console.log(`Detected build script: "${buildScript}"`);
94
+ await $`bash -c ${buildScript}`.env(env);
95
+ } else if (packageJson.scripts?.build) {
96
+ console.log(`Detected build script in package.json: "${packageJson.scripts.build}"`);
97
+ await $`bash -c "npm run build"`.env(env);
98
+ } else {
99
+ console.log('No build script detected, skipping build step.');
100
+ }
101
+
102
+ let potentialEntrypoints = cleanup([
103
+ functionBayFile?.entrypoint,
104
+ packageJson.main,
105
+
106
+ ...tryDirs(tryExtensions('index')),
107
+ ...tryDirs(tryExtensions('main')),
108
+ ...tryDirs(tryExtensions('server')),
109
+ ...tryDirs(tryExtensions('function'))
110
+ ]).filter(fileExistsSync);
111
+
112
+ if (potentialEntrypoints.length === 0) {
113
+ throw new Error(
114
+ 'Could not find entrypoint for function. Please specify one in function-bay.json'
115
+ );
116
+ }
117
+
118
+ let entrypoint = potentialEntrypoints[0];
119
+ console.log(`Detected entrypoint: ${entrypoint}`);
120
+ if (!functionBayFile?.entrypoint) {
121
+ console.log(
122
+ 'You can specify this entrypoint in metorial.json to skip this detection step in the future.'
123
+ );
124
+ }
125
+
126
+ console.log('Bundling function to Metorial Function Bay format...');
127
+
128
+ let launcher = await getMetorialLauncher({
129
+ bundledEntrypoint: entrypoint
130
+ });
131
+ for (let file of launcher.files) {
132
+ await fs.writeFile(path.join(process.cwd(), file.filename), file.content, 'utf-8');
133
+ }
134
+
135
+ let outputTempDir = await tempDir();
136
+ await $`ncc build ${launcher.entrypoint} -o ${outputTempDir} --minify --source-map --debug --target es2020`.env(
137
+ env
138
+ );
139
+
140
+ console.log('\nCreating function package...');
141
+
142
+ await Function.create({
143
+ runtime: {
144
+ identifier: '@function-bay/nodejs',
145
+ layer: Runtime.layer,
146
+ handler: launcher.handler,
147
+ runtime: {
148
+ identifier: 'nodejs',
149
+ version: nodeJsVersionIdentifier as '24.x'
150
+ }
151
+ },
152
+ directory: outputTempDir
153
+ });
154
+
155
+ console.log('Function package created successfully.');
156
+ };
@@ -0,0 +1,130 @@
1
+ import { Runtime } from '@function-bay/build';
2
+
3
+ export let getMetorialLauncher = async (opts: { bundledEntrypoint: string }) => {
4
+ if (Runtime.provider == 'aws.lambda') {
5
+ return {
6
+ handler: 'metorial_launcher.handler',
7
+ entrypoint: 'metorial_launcher.js',
8
+ files: [
9
+ {
10
+ filename: 'metorial_launcher.js',
11
+ content: `// Auto-generated by Metorial Function Bay
12
+ import * as handlerModule from './${opts.bundledEntrypoint}';
13
+
14
+ let handler = handlerModule;
15
+
16
+ exports.handler = async (event) => {
17
+ // Load the entrypoint module
18
+ if (typeof handler == 'object') {
19
+ if (typeof handler.default === 'function') {
20
+ handler = handler.default;
21
+ } else if (typeof handler.handler === 'function') {
22
+ handler = handler.handler;
23
+ }
24
+ }
25
+
26
+ if (typeof handler !== 'function') {
27
+ return {
28
+ statusCode: 500,
29
+ body: JSON.stringify({ error: { code: 'function_bay.launcher', message: "Entrypoint does not export a valid handler function" } })
30
+ };
31
+ }
32
+
33
+ let rawPayload = event.payload;
34
+ if (!rawPayload) {
35
+ return {
36
+ statusCode: 400,
37
+ body: JSON.stringify({ error: { code: 'function_bay.launcher', message: "Missing payload field" } })
38
+ };
39
+ }
40
+
41
+ let data;
42
+ try {
43
+ data = JSON.parse(rawPayload);
44
+ } catch (err) {
45
+ return {
46
+ statusCode: 400,
47
+ body: JSON.stringify({ error: { code: 'function_bay.launcher', message: "Invalid JSON payload" } })
48
+ };
49
+ }
50
+
51
+ try {
52
+ let result = await handler(data);
53
+
54
+ return {
55
+ statusCode: 200,
56
+ body: JSON.stringify({
57
+ message: "Payload received",
58
+ received: data
59
+ })
60
+ };
61
+ } catch (err) {
62
+ if (typeof err == 'object' && err.__function_bay_error) {
63
+ let result = err.toResponse();
64
+ return {
65
+ statusCode: result.statusCode || 500,
66
+ body: JSON.stringify(result)
67
+ };
68
+ }
69
+
70
+ let fullMessage = '';
71
+ if (typeof err == 'object' && err !== null) {
72
+ if ('name' in err && typeof err.name === 'string') {
73
+ fullMessage += \`[\${err.name}]: \\n\`;
74
+ }
75
+
76
+ if ('message' in err && typeof err.message === 'string') {
77
+ fullMessage += err.message;
78
+ }
79
+
80
+ if ('stack' in err && typeof err.stack === 'string') {
81
+ fullMessage += '\\n' + err.stack;
82
+ }
83
+ } else {
84
+ try {
85
+ fullMessage = JSON.stringify(err);
86
+ } catch {
87
+ fullMessage = String(err || 'Unknown error');
88
+ }
89
+ }
90
+
91
+ return {
92
+ statusCode: 500,
93
+ body: JSON.stringify({ error: { code: 'function_bay.handler_error', message: fullMessage } })
94
+ };
95
+ }
96
+ };
97
+ `
98
+ },
99
+
100
+ {
101
+ filename: 'tsconfig.json',
102
+ content: JSON.stringify(
103
+ {
104
+ compilerOptions: {
105
+ lib: ['ESNext'],
106
+ target: 'es2020',
107
+ jsx: 'react-jsx',
108
+ allowJs: true,
109
+ moduleResolution: 'bundler',
110
+ verbatimModuleSyntax: false,
111
+ strict: false,
112
+ skipLibCheck: true,
113
+ noFallthroughCasesInSwitch: false,
114
+ noUncheckedIndexedAccess: false,
115
+ noImplicitOverride: false,
116
+ noUnusedLocals: false,
117
+ noUnusedParameters: false,
118
+ noPropertyAccessFromIndexSignature: false
119
+ }
120
+ },
121
+ null,
122
+ 2
123
+ )
124
+ }
125
+ ]
126
+ };
127
+ }
128
+
129
+ throw new Error(`Unsupported provider for Node.js runtime: ${Runtime.provider}`);
130
+ };
@@ -0,0 +1,2 @@
1
+ export let cleanup = <T>(arr: (T | null | undefined)[]): T[] =>
2
+ arr.filter((item): item is T => item != null);
package/src/lib/fs.ts ADDED
@@ -0,0 +1,34 @@
1
+ import fsSync from 'fs';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ export let readJsonFile = async <T>(inPath: string): Promise<T> => {
6
+ let content = await fs.readFile(path.join(process.cwd(), inPath), 'utf-8');
7
+ return JSON.parse(content) as T;
8
+ };
9
+
10
+ export let readJsonFileOptional = async <T>(inPath: string): Promise<T | null> => {
11
+ if (!(await fileExists(inPath))) {
12
+ return null;
13
+ }
14
+
15
+ return readJsonFile<T>(inPath);
16
+ };
17
+
18
+ export let fileExists = async (inPath: string): Promise<boolean> => {
19
+ try {
20
+ await fs.access(path.join(process.cwd(), inPath));
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ };
26
+
27
+ export let fileExistsSync = (inPath: string): boolean => {
28
+ try {
29
+ fsSync.accessSync(path.join(process.cwd(), inPath));
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ };