cloud-run-functions 0.1.4 → 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.
@@ -55,10 +55,7 @@
55
55
  "oneOf": [
56
56
  {
57
57
  "description": "The adapter wraps your Cloud Run functions at runtime, allowing you to write them with a platform-agnostic HTTP framework, like Hattip.\n\nSet this to \"node\" to skip using an adapter, in which case, your functions should conform to what @google-cloud/functions-framework expects.\n\n@default \"node\"",
58
- "enum": [
59
- "hattip",
60
- "node"
61
- ]
58
+ "enum": ["hattip", "node"]
62
59
  },
63
60
  {
64
61
  "type": "null"
@@ -91,4 +88,4 @@
91
88
  }
92
89
  },
93
90
  "required": []
94
- }
91
+ }
@@ -0,0 +1,125 @@
1
+ import {
2
+ getFunctionFilter,
3
+ loadConfig
4
+ } from "./chunk-MOB3GBJW.js";
5
+
6
+ // src/tools/build.ts
7
+ import esbuild from "esbuild";
8
+ import { findUpSync } from "find-up-simple";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { glob } from "tinyglobby";
12
+ var virtualModuleId = "virtual:cloud-run-functions";
13
+ var virtualModuleNamespace = "crf-functions";
14
+ async function build(root, { define, outdir } = {}) {
15
+ const workingDir = process.cwd();
16
+ const searchDir = path.resolve(workingDir, root ?? "");
17
+ const config = loadConfig(searchDir);
18
+ const functionsRoot = config.configDir ? path.resolve(config.configDir, config.root ?? "") : searchDir;
19
+ const functionFilter = getFunctionFilter(config);
20
+ const adapter = config.adapter ?? "node";
21
+ const entries = await glob(functionFilter.globs, {
22
+ cwd: functionsRoot,
23
+ onlyFiles: true
24
+ });
25
+ const entryPoints = Array.from(
26
+ new Set(entries.map((entry) => entry.split(path.sep).join("/")))
27
+ );
28
+ const routes = entryPoints.map((entry) => {
29
+ const name = entry.replace(functionFilter.suffixPattern, "");
30
+ return {
31
+ entry,
32
+ name,
33
+ path: "/" + name
34
+ };
35
+ }).sort((a, b) => a.path.localeCompare(b.path));
36
+ const seenPaths = /* @__PURE__ */ new Set();
37
+ for (const route of routes) {
38
+ if (seenPaths.has(route.path)) {
39
+ throw new Error(`Duplicate route detected: ${route.path}`);
40
+ }
41
+ seenPaths.add(route.path);
42
+ }
43
+ const outputDir = path.resolve(workingDir, outdir ?? "dist");
44
+ fs.mkdirSync(outputDir, { recursive: true });
45
+ const packageDir = findUpSync("dist", {
46
+ cwd: import.meta.dirname,
47
+ type: "directory"
48
+ });
49
+ if (!packageDir) {
50
+ throw new Error("Unable to locate the package dist directory.");
51
+ }
52
+ const source = path.join(packageDir, "targets/build.js");
53
+ const outfile = path.join(outputDir, "index.js");
54
+ const result = await esbuild.build({
55
+ entryPoints: [source],
56
+ absWorkingDir: functionsRoot,
57
+ outfile,
58
+ define,
59
+ bundle: true,
60
+ format: "cjs",
61
+ platform: "node",
62
+ packages: "bundle",
63
+ sourcemap: true,
64
+ sourcesContent: false,
65
+ logOverride: {
66
+ "empty-glob": "silent"
67
+ },
68
+ external: ["@google-cloud/functions-framework"],
69
+ plugins: [
70
+ {
71
+ name: "crf-virtual-functions",
72
+ setup(build2) {
73
+ const filter = new RegExp(
74
+ `^${virtualModuleId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`
75
+ );
76
+ build2.onResolve({ filter }, () => ({
77
+ path: virtualModuleId,
78
+ namespace: virtualModuleNamespace
79
+ }));
80
+ build2.onLoad(
81
+ { filter: /.*/, namespace: virtualModuleNamespace },
82
+ () => ({
83
+ contents: createVirtualModule({
84
+ routes,
85
+ adapter
86
+ }),
87
+ loader: "js",
88
+ resolveDir: functionsRoot
89
+ })
90
+ );
91
+ }
92
+ }
93
+ ]
94
+ });
95
+ console.log(
96
+ `[esbuild] Bundled ${routes.length} function${routes.length === 1 ? "" : "s"} into ${outfile}`
97
+ );
98
+ return result;
99
+ }
100
+ function createVirtualModule({
101
+ routes,
102
+ adapter
103
+ }) {
104
+ const imports = routes.map((route, index) => {
105
+ const importPath = route.entry.startsWith(".") ? route.entry : `./${route.entry}`;
106
+ return `import * as route${index} from ${JSON.stringify(importPath)};`;
107
+ });
108
+ const routeEntries = routes.map(
109
+ (route, index) => ` { name: ${JSON.stringify(route.name)}, path: ${JSON.stringify(
110
+ route.path
111
+ )}, module: route${index} },`
112
+ );
113
+ return [
114
+ ...imports,
115
+ `export const adapter = ${JSON.stringify(adapter)};`,
116
+ "export const routes = [",
117
+ ...routeEntries,
118
+ "];",
119
+ ""
120
+ ].join("\n");
121
+ }
122
+
123
+ export {
124
+ build
125
+ };
@@ -0,0 +1,109 @@
1
+ import {
2
+ dedent
3
+ } from "./chunk-SPF3AMMV.js";
4
+
5
+ // src/common/functionFilter.ts
6
+ import path from "node:path";
7
+ function getFunctionFilter(options) {
8
+ const functionGlobs = [];
9
+ const functionSuffixes = /* @__PURE__ */ new Set();
10
+ const requiredSuffix = options.entrySuffix?.replace(/^\.?/, ".") ?? "";
11
+ for (const glob of options.globs ?? ["**/*"]) {
12
+ let ext = path.extname(glob);
13
+ if (ext) {
14
+ functionSuffixes.add(requiredSuffix + ext);
15
+ functionGlobs.push(
16
+ requiredSuffix ? glob.replace(ext, requiredSuffix + ext) : glob
17
+ );
18
+ continue;
19
+ }
20
+ for (ext of options.extensions ?? [".ts", ".js"]) {
21
+ ext = requiredSuffix + ext;
22
+ functionSuffixes.add(ext);
23
+ functionGlobs.push(glob + ext);
24
+ }
25
+ }
26
+ const suffixPattern = new RegExp(
27
+ `(${Array.from(functionSuffixes, (e) => e.replace(/\./g, "\\.")).sort((a, b) => b.length - a.length).join("|")})$`
28
+ );
29
+ return {
30
+ globs: functionGlobs,
31
+ suffixPattern
32
+ };
33
+ }
34
+
35
+ // src/config/index.ts
36
+ import * as z2 from "@zod/mini";
37
+ import Joycon from "joycon";
38
+ import path2 from "node:path";
39
+
40
+ // src/config/schema.ts
41
+ import * as z from "@zod/mini";
42
+ var configSchema = z.partial(
43
+ z.interface({
44
+ root: z.string().register(z.globalRegistry, {
45
+ description: dedent`
46
+ The base directory when searching for entry points.
47
+
48
+ Defaults to the config directory (or if no config file is found, the current working directory).
49
+ `
50
+ }),
51
+ globs: z.array(z.string()).register(z.globalRegistry, {
52
+ description: dedent`
53
+ The globs determine which directories are searched for entry points. Only ".ts" and ".js" files are matched, so you're not required to include them in the globs.
54
+
55
+ By default all directories in the "root" are searched.
56
+ `
57
+ }),
58
+ extensions: z.array(z.string()).register(z.globalRegistry, {
59
+ description: dedent`
60
+ The extensions to match for entry points.
61
+
62
+ @default [".ts", ".js"]
63
+ `
64
+ }),
65
+ entrySuffix: z.string().register(z.globalRegistry, {
66
+ description: dedent`
67
+ The entry suffix should be a string like ".task" or ".function" which must be present in the file name (before the extension) or else that file will be ignored when scanning for entry points.
68
+
69
+ It can also be an empty string.
70
+ `
71
+ }),
72
+ adapter: z.enum(["hattip", "node"]).register(z.globalRegistry, {
73
+ description: dedent`
74
+ The adapter wraps your Cloud Run functions at runtime, allowing you to write them with a platform-agnostic HTTP framework, like Hattip.
75
+
76
+ Set this to "node" to skip using an adapter, in which case, your functions should conform to what @google-cloud/functions-framework expects.
77
+
78
+ @default "node"
79
+ `
80
+ }),
81
+ maxInstanceConcurrency: z.union([z.number(), z.record(z.string(), z.number())]).register(z.globalRegistry, {
82
+ description: dedent`
83
+ The maximum number of instances (per function) that can be run concurrently. You can either set the same limit for all functions or set a different limit for each function.
84
+
85
+ @default 5
86
+ `
87
+ })
88
+ })
89
+ );
90
+
91
+ // src/config/index.ts
92
+ var joycon = new Joycon();
93
+ function loadConfig(cwd) {
94
+ const result = joycon.loadSync(["crf.config.json"], cwd);
95
+ if (!result.path) {
96
+ return {
97
+ configDir: null
98
+ };
99
+ }
100
+ return {
101
+ ...z2.parse(configSchema, result.data),
102
+ configDir: path2.dirname(result.path)
103
+ };
104
+ }
105
+
106
+ export {
107
+ getFunctionFilter,
108
+ loadConfig
109
+ };
@@ -0,0 +1,25 @@
1
+ // src/tools/preview.ts
2
+ import { findUpSync } from "find-up-simple";
3
+ import path from "node:path";
4
+ import $ from "picospawn";
5
+ function preview({ outDir, ...options } = {}) {
6
+ const packageDir = findUpSync("dist", {
7
+ cwd: import.meta.dirname,
8
+ type: "directory"
9
+ });
10
+ const binDir = path.resolve(packageDir, "../node_modules/.bin");
11
+ const sourceDir = path.resolve(process.cwd(), outDir ?? "dist");
12
+ const source = path.join(sourceDir, "index.js");
13
+ return $("functions-framework --target=build --source %s", [source], {
14
+ stdio: "inherit",
15
+ ...options,
16
+ env: {
17
+ ...options.env ?? process.env,
18
+ PATH: `${binDir}:${process.env.PATH}`
19
+ }
20
+ });
21
+ }
22
+
23
+ export {
24
+ preview
25
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { BuildOptions, BundleOptions, build } from './tools/build.js';
1
2
  export { DevOptions, dev } from './tools/dev.js';
3
+ export { PreviewOptions, preview } from './tools/preview.js';
4
+ import 'esbuild';
2
5
  import 'picospawn';
3
- import './tools/build.js';
package/dist/index.js CHANGED
@@ -1,6 +1,16 @@
1
+ import {
2
+ build
3
+ } from "./chunk-L3GFRAQX.js";
4
+ import "./chunk-MOB3GBJW.js";
5
+ import "./chunk-SPF3AMMV.js";
1
6
  import {
2
7
  dev
3
8
  } from "./chunk-DG37B63B.js";
9
+ import {
10
+ preview
11
+ } from "./chunk-XHFBJ7D2.js";
4
12
  export {
5
- dev
13
+ build,
14
+ dev,
15
+ preview
6
16
  };
package/dist/main.d.ts CHANGED
@@ -1,2 +1 @@
1
-
2
- export { }
1
+ #!/usr/bin/env node
package/dist/main.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import {
2
3
  objectify
3
4
  } from "./chunk-SPF3AMMV.js";
@@ -14,6 +15,20 @@ import {
14
15
  string,
15
16
  subcommands
16
17
  } from "cmd-ts";
18
+
19
+ // src/common/parseDefines.ts
20
+ function parseDefines(define) {
21
+ if (!define) {
22
+ return void 0;
23
+ }
24
+ return objectify(
25
+ define.map((d) => d.split(":")),
26
+ ([key]) => key,
27
+ ([, value]) => value ? isNaN(parseFloat(value)) ? JSON.stringify(value) : value : ""
28
+ );
29
+ }
30
+
31
+ // src/main.ts
17
32
  var dev = command({
18
33
  name: "dev",
19
34
  description: "Start the development server",
@@ -40,33 +55,62 @@ var dev = command({
40
55
  const { dev: dev2 } = await import("./tools/dev.js");
41
56
  await dev2(root, {
42
57
  port,
43
- define: define && objectify(
44
- define.map((d) => d.split(":")),
45
- ([key]) => key,
46
- ([, value]) => value ? isNaN(parseFloat(value)) ? JSON.stringify(value) : value : ""
47
- )
58
+ define: parseDefines(define)
48
59
  });
49
60
  }
50
61
  });
51
62
  var build = command({
52
63
  name: "build",
53
- description: "Generate a bundle for each function",
64
+ description: "Bundle your functions for deployment",
54
65
  args: {
55
66
  root: positional({
56
67
  type: optional(string),
57
68
  displayName: "root",
58
69
  description: "Directory to search for function entrypoints"
70
+ }),
71
+ outdir: option({
72
+ type: optional(string),
73
+ long: "outdir",
74
+ short: "o",
75
+ description: "The directory to write bundled output to"
76
+ }),
77
+ define: multioption({
78
+ type: optional(array(string)),
79
+ long: "define",
80
+ short: "d",
81
+ description: "Statically replace specific variables in the source code"
82
+ })
83
+ },
84
+ async handler({ root, outdir, define }) {
85
+ const { build: build2 } = await import("./tools/build.js");
86
+ await build2(root, {
87
+ outdir,
88
+ define: parseDefines(define)
89
+ });
90
+ }
91
+ });
92
+ var preview = command({
93
+ name: "preview",
94
+ description: "Preview bundled functions locally",
95
+ args: {
96
+ outDir: option({
97
+ type: optional(string),
98
+ long: "outDir",
99
+ short: "o",
100
+ description: "The directory containing the bundled output"
59
101
  })
60
102
  },
61
- async handler() {
62
- throw new Error("Not implemented");
103
+ async handler({ outDir }) {
104
+ const { preview: preview2 } = await import("./tools/preview.js");
105
+ await preview2({ outDir });
63
106
  }
64
107
  });
65
108
  var cli = subcommands({
66
109
  name: "cloud-run-functions",
67
110
  cmds: {
68
111
  dev,
69
- build
112
+ build,
113
+ preview
70
114
  }
71
115
  });
72
116
  await run(cli, process.argv.slice(2));
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,53 @@
1
+ // src/targets/build.ts
2
+ import "source-map-support/register.js";
3
+ import functions from "@google-cloud/functions-framework";
4
+ import { adapter, routes } from "virtual:cloud-run-functions";
5
+ var routesByPath = new Map(routes.map((route) => [route.path, route]));
6
+ var handlerCache = /* @__PURE__ */ new Map();
7
+ function unwrapHandler(module) {
8
+ let handler = module;
9
+ while (handler && typeof handler !== "function") {
10
+ handler = handler.default;
11
+ }
12
+ return typeof handler === "function" ? handler : null;
13
+ }
14
+ async function getHandler(route) {
15
+ const cached = handlerCache.get(route.path);
16
+ if (cached) {
17
+ return cached;
18
+ }
19
+ let handler = unwrapHandler(route.module);
20
+ if (!handler) {
21
+ throw new Error(`Task ${route.name} is not a function.`);
22
+ }
23
+ switch (adapter) {
24
+ case "hattip": {
25
+ const { createMiddleware } = await import("@hattip/adapter-node");
26
+ handler = createMiddleware(handler);
27
+ break;
28
+ }
29
+ }
30
+ handlerCache.set(route.path, handler);
31
+ return handler;
32
+ }
33
+ async function match(url) {
34
+ const route = routesByPath.get(url.pathname);
35
+ if (!route) {
36
+ return null;
37
+ }
38
+ return getHandler(route);
39
+ }
40
+ functions.http("build", async (req, res) => {
41
+ const url = new URL(req.url, "http://" + req.headers.host);
42
+ const handler = await match(url);
43
+ if (handler) {
44
+ try {
45
+ await handler(req, res);
46
+ } catch (error) {
47
+ console.error(error);
48
+ res.status(500).end();
49
+ }
50
+ } else {
51
+ res.status(404).end();
52
+ }
53
+ });
@@ -1,5 +1,8 @@
1
1
  import {
2
- dedent,
2
+ getFunctionFilter,
3
+ loadConfig
4
+ } from "../chunk-MOB3GBJW.js";
5
+ import {
3
6
  isNumber,
4
7
  timeout,
5
8
  toResult
@@ -13,7 +16,7 @@ import { findUpSync } from "find-up-simple";
13
16
  import fs2 from "node:fs";
14
17
  import { Module } from "node:module";
15
18
  import os from "node:os";
16
- import path3 from "node:path";
19
+ import path from "node:path";
17
20
 
18
21
  // src/common/emptyDir.ts
19
22
  import fs from "node:fs";
@@ -23,122 +26,21 @@ function emptyDir(dir) {
23
26
  return dir;
24
27
  }
25
28
 
26
- // src/common/functionFilter.ts
27
- import path from "node:path";
28
- function getFunctionFilter(options) {
29
- const functionGlobs = [];
30
- const functionSuffixes = /* @__PURE__ */ new Set();
31
- const requiredSuffix = options.entrySuffix?.replace(/^\.?/, ".") ?? "";
32
- for (const glob of options.globs ?? ["**/*"]) {
33
- let ext = path.extname(glob);
34
- if (ext) {
35
- functionSuffixes.add(requiredSuffix + ext);
36
- functionGlobs.push(
37
- requiredSuffix ? glob.replace(ext, requiredSuffix + ext) : glob
38
- );
39
- continue;
40
- }
41
- for (ext of options.extensions ?? [".ts", ".js"]) {
42
- ext = requiredSuffix + ext;
43
- functionSuffixes.add(ext);
44
- functionGlobs.push(glob + ext);
45
- }
46
- }
47
- const suffixPattern = new RegExp(
48
- `(${Array.from(functionSuffixes, (e) => e.replace(/\./g, "\\.")).sort((a, b) => b.length - a.length).join("|")})$`
49
- );
50
- return {
51
- globs: functionGlobs,
52
- suffixPattern
53
- };
54
- }
55
-
56
29
  // src/common/hash.ts
57
30
  import crypto from "node:crypto";
58
31
  function hash(data, len) {
59
32
  return crypto.createHash("sha256").update(data).digest("hex").slice(0, len);
60
33
  }
61
34
 
62
- // src/config/index.ts
63
- import * as z2 from "@zod/mini";
64
- import Joycon from "joycon";
65
- import path2 from "node:path";
66
-
67
- // src/config/schema.ts
68
- import * as z from "@zod/mini";
69
- var configSchema = z.partial(
70
- z.interface({
71
- root: z.string().register(z.globalRegistry, {
72
- description: dedent`
73
- The base directory when searching for entry points.
74
-
75
- Defaults to the config directory (or if no config file is found, the current working directory).
76
- `
77
- }),
78
- globs: z.array(z.string()).register(z.globalRegistry, {
79
- description: dedent`
80
- The globs determine which directories are searched for entry points. Only ".ts" and ".js" files are matched, so you're not required to include them in the globs.
81
-
82
- By default all directories in the "root" are searched.
83
- `
84
- }),
85
- extensions: z.array(z.string()).register(z.globalRegistry, {
86
- description: dedent`
87
- The extensions to match for entry points.
88
-
89
- @default [".ts", ".js"]
90
- `
91
- }),
92
- entrySuffix: z.string().register(z.globalRegistry, {
93
- description: dedent`
94
- The entry suffix should be a string like ".task" or ".function" which must be present in the file name (before the extension) or else that file will be ignored when scanning for entry points.
95
-
96
- It can also be an empty string.
97
- `
98
- }),
99
- adapter: z.enum(["hattip", "node"]).register(z.globalRegistry, {
100
- description: dedent`
101
- The adapter wraps your Cloud Run functions at runtime, allowing you to write them with a platform-agnostic HTTP framework, like Hattip.
102
-
103
- Set this to "node" to skip using an adapter, in which case, your functions should conform to what @google-cloud/functions-framework expects.
104
-
105
- @default "node"
106
- `
107
- }),
108
- maxInstanceConcurrency: z.union([z.number(), z.record(z.string(), z.number())]).register(z.globalRegistry, {
109
- description: dedent`
110
- The maximum number of instances (per function) that can be run concurrently. You can either set the same limit for all functions or set a different limit for each function.
111
-
112
- @default 5
113
- `
114
- })
115
- })
116
- );
117
-
118
- // src/config/index.ts
119
- var joycon = new Joycon();
120
- function loadConfig(cwd) {
121
- const result = joycon.loadSync(["crf.config.json"], cwd);
122
- if (!result.path) {
123
- return {
124
- configDir: null
125
- };
126
- }
127
- return {
128
- ...z2.parse(configSchema, result.data),
129
- configDir: path2.dirname(result.path)
130
- };
131
- }
132
-
133
35
  // src/targets/dev.ts
134
36
  async function createBuild() {
135
37
  const options = JSON.parse(process.env.CRF_OPTIONS);
136
- const searchDir = path3.resolve(options.workingDir, options.searchDir ?? "");
38
+ const searchDir = path.resolve(options.workingDir, options.searchDir ?? "");
137
39
  const config = loadConfig(searchDir);
138
- const root = config.configDir ? path3.resolve(config.configDir, config.root ?? "") : searchDir;
40
+ const root = config.configDir ? path.resolve(config.configDir, config.root ?? "") : searchDir;
139
41
  const functionFilter = getFunctionFilter(config);
140
42
  const cacheDir = emptyDir(
141
- path3.join(
43
+ path.join(
142
44
  fs2.realpathSync(os.tmpdir()),
143
45
  "cloud-run-functions-" + hash(root, 8)
144
46
  )
@@ -240,7 +142,7 @@ async function createBuild() {
240
142
  taskState.running++;
241
143
  taskStates.set(taskName, taskState);
242
144
  const require2 = Module.createRequire(import.meta.filename);
243
- let taskHandler = require2(path3.join(root, file));
145
+ let taskHandler = require2(path.join(root, file));
244
146
  while (taskHandler && typeof taskHandler !== "function") {
245
147
  taskHandler = taskHandler.default;
246
148
  }
@@ -1,3 +1,5 @@
1
+ import esbuild from 'esbuild';
2
+
1
3
  type BuildOptions = {
2
4
  /**
3
5
  * Statically replace specific variables in the source code.
@@ -13,5 +15,32 @@ type BuildOptions = {
13
15
  */
14
16
  define?: Record<string, string>;
15
17
  };
18
+ type BundleOptions = BuildOptions & {
19
+ /**
20
+ * The directory to write the bundled output to.
21
+ * @default "dist"
22
+ */
23
+ outdir?: string;
24
+ };
25
+ declare function build(root?: string, { define, outdir }?: BundleOptions): Promise<esbuild.BuildResult<{
26
+ entryPoints: string[];
27
+ absWorkingDir: string;
28
+ outfile: string;
29
+ define: Record<string, string> | undefined;
30
+ bundle: true;
31
+ format: "cjs";
32
+ platform: "node";
33
+ packages: "bundle";
34
+ sourcemap: true;
35
+ sourcesContent: false;
36
+ logOverride: {
37
+ 'empty-glob': "silent";
38
+ };
39
+ external: string[];
40
+ plugins: {
41
+ name: string;
42
+ setup(build: esbuild.PluginBuild): void;
43
+ }[];
44
+ }>>;
16
45
 
17
- export type { BuildOptions };
46
+ export { type BuildOptions, type BundleOptions, build };
@@ -0,0 +1,8 @@
1
+ import {
2
+ build
3
+ } from "../chunk-L3GFRAQX.js";
4
+ import "../chunk-MOB3GBJW.js";
5
+ import "../chunk-SPF3AMMV.js";
6
+ export {
7
+ build
8
+ };
@@ -1,6 +1,7 @@
1
1
  import * as picospawn from 'picospawn';
2
2
  import { PicospawnOptions } from 'picospawn';
3
3
  import { BuildOptions } from './build.js';
4
+ import 'esbuild';
4
5
 
5
6
  interface DevOptions extends PicospawnOptions, BuildOptions {
6
7
  /**
@@ -0,0 +1,16 @@
1
+ import * as picospawn from 'picospawn';
2
+ import { PicospawnOptions } from 'picospawn';
3
+
4
+ interface PreviewOptions extends PicospawnOptions {
5
+ /**
6
+ * The directory containing the bundled output.
7
+ * @default "dist"
8
+ */
9
+ outDir?: string;
10
+ }
11
+ /**
12
+ * Start the preview server for the bundled output.
13
+ */
14
+ declare function preview({ outDir, ...options }?: PreviewOptions): picospawn.PicospawnPromise<unknown>;
15
+
16
+ export { type PreviewOptions, preview };
@@ -0,0 +1,6 @@
1
+ import {
2
+ preview
3
+ } from "../chunk-XHFBJ7D2.js";
4
+ export {
5
+ preview
6
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cloud-run-functions",
3
3
  "type": "module",
4
- "version": "0.1.4",
4
+ "version": "0.2.1",
5
5
  "bin": "./dist/main.js",
6
6
  "exports": {
7
7
  "types": "./dist/index.d.ts",
package/readme.md CHANGED
@@ -1,33 +1,166 @@
1
1
  # cloud-run-functions
2
2
 
3
- > ⚠️ This project is not ready for production use.
3
+ File-based routing plus local dev and bundling for `@google-cloud/functions-framework`.
4
4
 
5
- Enhances the `@google-cloud/functions-framework` package with "hot reloading" (for local development) and a bundler (for deployment).
5
+ You point it at a directory of `.ts` or `.js` files. \
6
+ Each file becomes an HTTP route and its default export is treated as the handler.
6
7
 
8
+ ## Install
9
+
10
+ `@google-cloud/functions-framework` is required because this package runs it under the hood.
11
+
12
+ ```sh
13
+ pnpm add -D cloud-run-functions @google-cloud/functions-framework
14
+ ```
15
+
16
+ Optional:
17
+
18
+ ```sh
19
+ pnpm add -D dotenv
20
+ pnpm add -D @hattip/adapter-node
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ Create a function file:
26
+
27
+ `functions/hello.ts`
28
+
29
+ ```ts
30
+ export default (req, res) => {
31
+ res.status(200).send('hello')
32
+ }
33
+ ```
34
+
35
+ Run the dev server:
36
+
37
+ ```sh
38
+ pnpx cloud-run-functions dev functions
39
+ ```
40
+
41
+ Call it:
42
+
43
+ ```sh
44
+ curl http://localhost:8080/hello
45
+ ```
46
+
47
+ ## Routing
48
+
49
+ - A file path becomes a URL path
50
+ - `functions/hello.ts` becomes `/hello`
51
+ - `functions/users/create.ts` becomes `/users/create`
52
+ - Your file should default export a function handler
53
+ - If you set `entrySuffix` to `.task`, then `hello.task.ts` becomes `/hello`
54
+
55
+ ## CLI
56
+
57
+ The binary name is `cloud-run-functions`.
58
+
59
+ ### dev
60
+
61
+ Start the development server with hot reload.
62
+
63
+ ```sh
64
+ npx cloud-run-functions dev [root]
7
65
  ```
8
- pnpm add cloud-run-functions -D
66
+
67
+ - `root` is the directory to search for function entrypoints. Default is the current working directory
68
+ - `--port, -p <port>` sets the port. Default is `8080`
69
+ - `--define, -d <key:value>` can be repeated. It passes `define` values to esbuild
70
+
71
+ ### build
72
+
73
+ Bundle your functions for deployment.
74
+
75
+ ```sh
76
+ npx cloud-run-functions build [root]
9
77
  ```
10
78
 
11
- ## Development server
79
+ - `root` is the directory to search for function entrypoints. Default is the current working directory
80
+ - `--outdir, -o <dir>` sets the output directory. Default is `dist`
81
+ - `--define, -d <key:value>` can be repeated. It passes `define` values to esbuild
82
+
83
+ Output:
12
84
 
13
- The dev server uses the `@google-cloud/functions-framework` package to run your functions locally. We handle _transpiling_ and _hot reloading_ for you. Just tell the dev server where to find your functions.
85
+ - Writes `index.js` and sourcemaps into `outdir`
86
+ - Run it with the Functions Framework using the `build` target
87
+
88
+ Example:
14
89
 
15
90
  ```sh
16
- npx cloud-run-functions dev ./path/to/functions/
91
+ npx cloud-run-functions build functions
92
+ npx functions-framework --target=build --source dist/index.js
17
93
  ```
18
94
 
19
- By default, any `.ts` or `.js` module is considered "loadable" by the dev server. This behavior is configurable with a `crf.config.json` file.
95
+ ### preview
20
96
 
21
- The dev server uses filesystem routing. By default, the dev server runs on port 8080. So if you do `http get :8080/hello` from your terminal, the dev server will look for a file called `hello.ts` or `hello.js` in the `./path/to/functions/` directory. If that file exists, its default export will be used as the function handler.
97
+ Preview bundled functions locally.
22
98
 
23
- ## Bundling
99
+ ```sh
100
+ npx cloud-run-functions preview [--outDir <dir>]
101
+ ```
102
+
103
+ - `--outDir, -o <dir>` sets the directory containing the bundled output. Default is `dist`
104
+
105
+ This runs the Functions Framework using the `build` target and your bundled `index.js`.
106
+
107
+ ## Define values
24
108
 
25
- When you're ready to deploy, use the `build` command to bundle your functions.
109
+ The CLI format is `--define key:value` or `-d key:value`.
110
+
111
+ - If `value` looks like a number, it is used as one
112
+ - Otherwise it is treated as a string literal
113
+
114
+ Examples:
26
115
 
27
116
  ```sh
28
- npx cloud-run-functions build ./path/to/functions/
117
+ npx cloud-run-functions dev functions -d process.env.STAGE:dev
118
+ npx cloud-run-functions build functions -d __BUILD_ID__:123
29
119
  ```
30
120
 
31
- ## Tips
121
+ If you need full control over esbuild `define` values, use the programmatic API.
122
+
123
+ ## Configuration
124
+
125
+ Create a `crf.config.json` file. \
126
+ It is searched for by walking up from the `root` directory you pass to the CLI.
127
+
128
+ Example:
129
+
130
+ ```json
131
+ {
132
+ "root": "functions",
133
+ "entrySuffix": ".task",
134
+ "adapter": "node",
135
+ "maxInstanceConcurrency": 5
136
+ }
137
+ ```
138
+
139
+ Options:
140
+
141
+ - `root` string. base directory when searching for entry points. default is the config directory
142
+ - `globs` string array. globs to search within `root`. default is `["**/*"]`
143
+ - `extensions` string array. file extensions to match. default is `[".ts", ".js"]`
144
+ - `entrySuffix` string. require a suffix like `.task` before the extension
145
+ - `adapter` `"node"` or `"hattip"`. default is `"node"`
146
+ - `maxInstanceConcurrency` number or record of `{ [routeName]: number }`. default is `5`. used by `dev` to limit concurrent requests per route
147
+
148
+ Adapter notes:
149
+
150
+ - `adapter: "node"` means your default export should be a Functions Framework handler `(req, res) => ...`
151
+ - `adapter: "hattip"` means your default export should be a Hattip app and it will be wrapped at runtime
152
+
153
+ ## Dotenv support
154
+
155
+ If `dotenv` is installed, `dev` will load the closest `.env` file under your functions root.
156
+ Values in `.env` do not override existing `process.env` values.
157
+
158
+ ## Programmatic API
159
+
160
+ The package also exports the underlying functions used by the CLI:
161
+
162
+ ```ts
163
+ import { build, dev, preview } from 'cloud-run-functions'
164
+ ```
32
165
 
33
- - If you have the [dotenv](https://www.npmjs.com/package/dotenv) package installed, the dev server will import it and automatically load environment variables from the closest `.env` file. Note that environment variables in `.env` won't override existing `process.env` values.
166
+ See `src/index.ts` for the current exports and `src/tools/*.ts` for option types.