Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save aleclarson/97c1d93e2c784a4570def280d6c88b00 to your computer and use it in GitHub Desktop.
Save aleclarson/97c1d93e2c784a4570def280d6c88b00 to your computer and use it in GitHub Desktop.
Multiple functions (with watch mode) for @google-cloud/functions-framework

With this gist, you can test multiple functions at a time. Functions are always loaded on-demand, so they're always up-to-date. TypeScript functions are supported out-of-the-box.

Pre-requisites

pnpm install esbuild -D

Usage

Point the functions-framework command at the dev.js module in this gist.

functions-framework --target=dev --source=/path/to/directory

Your function modules must export a default function:

// tasks/helloWorld.task.js
import functions from "@google-cloud/functions-framework"

export default function helloWorld(req, res) {
  res.send("Hello world")
}

functions.http("helloWorld", helloWorld)

Your functions are invoked through the URL pathname. Here's an example with HTTPie:

http :8080/hello-world

In this example, you'd put the dev.js script in the tasks/ directory. Note that it will emit compiled files to a dist/ directory relative to the configured root path.

Options

Make sure your functions.http(…) call aligns with the filename, or you can customize the match function in the config object at the top of the dev.js script.

By default, you're expected to use either a .task.ts or .task.js filename suffix. But you can customize this.

/**
* This script provides a single entry point for any number of Google Cloud
* Run functions. TypeScript modules are supported. Functions are watched
* for changes and automatically recompiled. You can configure this script
* by editing the `config` object below.
*
* @author Alec Larson
* @license MIT
* @see https://gist.github.com/aleclarson/97c1d93e2c784a4570def280d6c88b00
*/
const config = {
// The common root directory for your functions, relative to the
// directory containing this script.
root: "./",
// The glob pattern for your functions. I personally use a ".task.*"
// suffix so that shared modules are only bundled when used.
globs: ["**/*.task.ts", "**/*.task.js"],
// For matching a function's filename to the URL.
match: (file, url) =>
url.pathname === "/" + file.replace(/\.task\.[jt]s$/, ""),
};
import functions from "@google-cloud/functions-framework";
import esbuild from "esbuild";
import path from "node:path";
async function createBuild() {
let pendingBuild;
const context = await esbuild.context({
entryPoints: config.globs,
absWorkingDir: path.resolve(import.meta.dirname, config.root),
outdir: path.join(config.root, "dist"),
bundle: true,
format: "esm",
packages: "external",
sourcemap: true,
metafile: true,
plugins: [
{
name: "build-status",
setup(build) {
build.onStart(() => {
pendingBuild = Promise.withResolvers();
});
build.onEnd((result) => {
pendingBuild.resolve(result);
});
},
},
],
});
await context.watch();
console.log("[esbuild] Watching for changes...");
return {
async match(url) {
const result = await pendingBuild.promise;
for (const [file, output] of Object.entries(
result.metafile?.outputs ?? {}
)) {
if (!output.entryPoint) {
continue;
}
if (config.match(output.entryPoint, url)) {
const taskPath = path.join(config.root, file) + "?t=" + Date.now();
const taskModule = await import(taskPath);
if (typeof taskModule.default === "function") {
return taskModule.default;
}
console.error(
"[!] Task must export a default function: %s",
taskPath
);
return null;
}
}
return null;
},
};
}
const buildPromise = createBuild();
functions.http("dev", async (req, res) => {
const url = new URL(req.url, "http://" + req.headers.host);
const build = await buildPromise;
const handler = await build.match(url);
if (handler) {
handler(req, res);
} else {
res.status(404).end();
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment