cloud-run-functions 0.1.0 → 0.1.2

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.
@@ -64,6 +64,30 @@
64
64
  "type": "null"
65
65
  }
66
66
  ]
67
+ },
68
+ "maxInstanceConcurrency": {
69
+ "oneOf": [
70
+ {
71
+ "description": "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.\n\n@default 5",
72
+ "oneOf": [
73
+ {
74
+ "type": "number"
75
+ },
76
+ {
77
+ "type": "object",
78
+ "propertyNames": {
79
+ "type": "string"
80
+ },
81
+ "additionalProperties": {
82
+ "type": "number"
83
+ }
84
+ }
85
+ ]
86
+ },
87
+ {
88
+ "type": "null"
89
+ }
90
+ ]
67
91
  }
68
92
  },
69
93
  "required": []
@@ -0,0 +1,33 @@
1
+ // src/tools/dev.ts
2
+ import { findUpSync } from "find-up-simple";
3
+ import path from "node:path";
4
+ import $ from "picospawn";
5
+ function dev(root, { port, define, ...options } = {}) {
6
+ const packageDir = findUpSync("dist", {
7
+ cwd: import.meta.dirname,
8
+ type: "directory"
9
+ });
10
+ const source = path.join(packageDir, "targets/dev.js");
11
+ const binDir = path.resolve(packageDir, "../node_modules/.bin");
12
+ return $(
13
+ "functions-framework --target=dev --source %s",
14
+ [source, port != null && ["--port", port.toString()]],
15
+ {
16
+ stdio: "inherit",
17
+ ...options,
18
+ env: {
19
+ ...options.env ?? process.env,
20
+ CRF_OPTIONS: JSON.stringify({
21
+ searchDir: root,
22
+ workingDir: process.cwd(),
23
+ define
24
+ }),
25
+ PATH: `${binDir}:${process.env.PATH}`
26
+ }
27
+ }
28
+ );
29
+ }
30
+
31
+ export {
32
+ dev
33
+ };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { dev } from './tools/dev.js';
1
+ export { DevOptions, dev } from './tools/dev.js';
2
2
  import 'picospawn';
3
+ import './tools/build.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dev
3
- } from "./chunk-XTHOFO7F.js";
3
+ } from "./chunk-DG37B63B.js";
4
4
  export {
5
5
  dev
6
6
  };
@@ -1,19 +1,39 @@
1
1
  // src/targets/dev.ts
2
+ import "source-map-support/register.js";
2
3
  import functions from "@google-cloud/functions-framework";
3
4
  import esbuild from "esbuild";
4
5
  import { findUpSync } from "find-up-simple";
6
+ import fs2 from "node:fs";
7
+ import { Module } from "node:module";
5
8
  import os from "node:os";
6
9
  import path2 from "node:path";
7
10
 
8
- // src/config/index.ts
9
- import * as z2 from "@zod/mini";
10
- import Joycon from "joycon";
11
- import path from "node:path";
12
-
13
- // src/config/schema.ts
14
- import * as z from "@zod/mini";
15
-
16
11
  // node_modules/.pnpm/radashi@12.4.0/node_modules/radashi/dist/radashi.js
12
+ var TimeoutError = class extends Error {
13
+ constructor(message) {
14
+ super(message ?? "Operation timed out");
15
+ this.name = "TimeoutError";
16
+ }
17
+ };
18
+ function timeout(ms, error) {
19
+ return new Promise(
20
+ (_, reject) => setTimeout(
21
+ () => reject(isFunction(error) ? error() : new TimeoutError(error)),
22
+ ms
23
+ )
24
+ );
25
+ }
26
+ async function toResult(promise) {
27
+ try {
28
+ const result = await promise;
29
+ return [void 0, result];
30
+ } catch (error) {
31
+ if (isError(error)) {
32
+ return [error, void 0];
33
+ }
34
+ throw error;
35
+ }
36
+ }
17
37
  function dedent(text, ...values) {
18
38
  var _a;
19
39
  if (isArray(text)) {
@@ -41,8 +61,26 @@ var asyncIteratorSymbol = (
41
61
  /* c8 ignore next */
42
62
  Symbol.asyncIterator || Symbol.for("Symbol.asyncIterator")
43
63
  );
64
+ function isError(value) {
65
+ return isTagged(value, "[object Error]");
66
+ }
67
+ function isFunction(value) {
68
+ return typeof value === "function";
69
+ }
70
+ function isNumber(value) {
71
+ return typeof value === "number" && !Number.isNaN(value);
72
+ }
73
+ function isTagged(value, tag) {
74
+ return Object.prototype.toString.call(value) === tag;
75
+ }
76
+
77
+ // src/config/index.ts
78
+ import * as z2 from "@zod/mini";
79
+ import Joycon from "joycon";
80
+ import path from "node:path";
44
81
 
45
82
  // src/config/schema.ts
83
+ import * as z from "@zod/mini";
46
84
  var configSchema = z.partial(
47
85
  z.interface({
48
86
  root: z.string().register(z.globalRegistry, {
@@ -81,6 +119,13 @@ var configSchema = z.partial(
81
119
 
82
120
  @default "node"
83
121
  `
122
+ }),
123
+ maxInstanceConcurrency: z.union([z.number(), z.record(z.string(), z.number())]).register(z.globalRegistry, {
124
+ description: dedent`
125
+ 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.
126
+
127
+ @default 5
128
+ `
84
129
  })
85
130
  })
86
131
  );
@@ -116,11 +161,10 @@ function hash(data, len) {
116
161
 
117
162
  // src/targets/dev.ts
118
163
  async function createBuild() {
119
- const callerDir = process.env.CALLER_DIR ?? "";
120
- const searchDir = process.env.CRF_ROOT ? path2.resolve(callerDir, process.env.CRF_ROOT) : callerDir;
164
+ const options = JSON.parse(process.env.CRF_OPTIONS);
165
+ const searchDir = path2.resolve(options.workingDir, options.searchDir ?? "");
121
166
  const config = loadConfig(searchDir);
122
167
  const root = config.configDir ? path2.resolve(config.configDir, config.root ?? "") : searchDir;
123
- let pendingBuild;
124
168
  const entryPoints = [];
125
169
  const requiredSuffix = config.entrySuffix?.replace(/^\.?/, ".") ?? "";
126
170
  const knownSuffixes = /* @__PURE__ */ new Set();
@@ -142,27 +186,25 @@ async function createBuild() {
142
186
  const knownSuffixesRE = new RegExp(
143
187
  `(${Array.from(knownSuffixes, (e) => e.replace(/\./g, "\\.")).sort((a, b) => b.length - a.length).join("|")})$`
144
188
  );
145
- const nodeModulesDir = findUpSync("node_modules", {
146
- cwd: root,
147
- type: "directory"
148
- });
149
- const outDir = emptyDir(
150
- nodeModulesDir ? path2.join(nodeModulesDir, `.cache/cloud-run-functions-${hash(root, 8)}`) : path2.join(os.tmpdir(), `cloud-run-functions-${hash(root, 8)}`)
189
+ const cacheDir = emptyDir(
190
+ path2.join(
191
+ fs2.realpathSync(os.tmpdir()),
192
+ "cloud-run-functions-" + hash(root, 8)
193
+ )
151
194
  );
152
- console.log({
153
- root,
154
- outDir,
155
- entryPoints,
156
- knownSuffixesRE
157
- });
195
+ let pendingBuild;
196
+ let finishedBuild;
158
197
  const context = await esbuild.context({
159
198
  entryPoints,
160
199
  absWorkingDir: root,
161
- outdir: outDir,
200
+ outdir: cacheDir,
201
+ define: options.define,
162
202
  bundle: true,
163
- format: "esm",
164
- packages: nodeModulesDir ? "external" : "bundle",
203
+ format: "cjs",
204
+ platform: "node",
205
+ packages: "bundle",
165
206
  sourcemap: true,
207
+ sourcesContent: false,
166
208
  metafile: true,
167
209
  logOverride: {
168
210
  "empty-glob": "silent"
@@ -171,11 +213,16 @@ async function createBuild() {
171
213
  {
172
214
  name: "build-status",
173
215
  setup(build) {
216
+ pendingBuild = Promise.withResolvers();
174
217
  build.onStart(() => {
175
- pendingBuild = Promise.withResolvers();
218
+ pendingBuild ??= Promise.withResolvers();
176
219
  });
177
220
  build.onEnd((result) => {
178
- pendingBuild.resolve(result);
221
+ if (pendingBuild) {
222
+ pendingBuild.resolve(result);
223
+ pendingBuild = void 0;
224
+ }
225
+ finishedBuild = result;
179
226
  });
180
227
  }
181
228
  }
@@ -183,14 +230,22 @@ async function createBuild() {
183
230
  });
184
231
  await context.watch();
185
232
  console.log("[esbuild] Watching for changes...");
186
- try {
187
- const dotenv = await import("dotenv");
188
- dotenv.config();
189
- } catch {
233
+ const envPath = findUpSync(".env", { cwd: root });
234
+ if (envPath) {
235
+ try {
236
+ const dotenv = await import("dotenv");
237
+ dotenv.config({ path: envPath });
238
+ console.log("[dotenv] Environment variables loaded.");
239
+ } catch {
240
+ }
190
241
  }
242
+ const taskStates = /* @__PURE__ */ new Map();
191
243
  return {
192
244
  async match(url) {
193
- const result = await pendingBuild.promise;
245
+ const result = await pendingBuild?.promise ?? finishedBuild;
246
+ if (!result) {
247
+ return null;
248
+ }
194
249
  for (const [file, output] of Object.entries(
195
250
  result.metafile?.outputs ?? {}
196
251
  )) {
@@ -199,18 +254,50 @@ async function createBuild() {
199
254
  }
200
255
  const taskName = output.entryPoint.replace(knownSuffixesRE, "");
201
256
  if (url.pathname === "/" + taskName) {
202
- const taskPath = path2.join(root, file) + "?t=" + Date.now();
203
- console.log("Importing:", taskPath);
204
- const taskModule = await import(taskPath);
205
- const taskHandler = taskModule.default;
257
+ const taskState = taskStates.get(taskName) ?? {
258
+ running: 0,
259
+ queue: []
260
+ };
261
+ const taskConcurrency = isNumber(config.maxInstanceConcurrency) ? config.maxInstanceConcurrency : config.maxInstanceConcurrency?.[taskName] ?? 5;
262
+ if (taskState.running >= taskConcurrency) {
263
+ const ticket = Promise.withResolvers();
264
+ taskState.queue.push(ticket);
265
+ const [error] = await toResult(
266
+ Promise.race([ticket.promise, timeout(3e4)])
267
+ );
268
+ if (error) {
269
+ return (_req, res) => {
270
+ res.status(429).end();
271
+ };
272
+ }
273
+ }
274
+ taskState.running++;
275
+ taskStates.set(taskName, taskState);
276
+ const require2 = Module.createRequire(import.meta.filename);
277
+ let taskHandler = require2(path2.join(root, file));
278
+ while (taskHandler && typeof taskHandler !== "function") {
279
+ taskHandler = taskHandler.default;
280
+ }
281
+ if (!taskHandler) {
282
+ return () => {
283
+ throw new Error(`Task ${taskName} is not a function.`);
284
+ };
285
+ }
206
286
  switch (config.adapter) {
207
287
  case "hattip": {
208
288
  const { createMiddleware } = await import("@hattip/adapter-node");
209
- return createMiddleware(taskHandler);
289
+ taskHandler = createMiddleware(taskHandler);
210
290
  }
211
- default:
212
- return taskHandler;
213
291
  }
292
+ return (req, res) => {
293
+ const end = res.end.bind(res);
294
+ res.end = (...args) => {
295
+ taskState.running--;
296
+ taskState.queue.shift()?.resolve();
297
+ return end(...args);
298
+ };
299
+ return taskHandler(req, res);
300
+ };
214
301
  }
215
302
  }
216
303
  return null;
@@ -223,7 +310,12 @@ functions.http("dev", async (req, res) => {
223
310
  const build = await buildPromise;
224
311
  const handler = await build.match(url);
225
312
  if (handler) {
226
- handler(req, res);
313
+ try {
314
+ await handler(req, res);
315
+ } catch (error) {
316
+ console.error(error);
317
+ res.status(500).end();
318
+ }
227
319
  } else {
228
320
  res.status(404).end();
229
321
  }
@@ -1,2 +1,17 @@
1
+ type BuildOptions = {
2
+ /**
3
+ * Statically replace specific variables in the source code.
4
+ *
5
+ * ⚠️ The value must be valid JavaScript syntax!
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * define: {
10
+ * 'process.env.NODE_ENV': '"development"',
11
+ * }
12
+ * ```
13
+ */
14
+ define?: Record<string, string>;
15
+ };
1
16
 
2
- export { }
17
+ export type { BuildOptions };
@@ -1,8 +1,17 @@
1
1
  import * as picospawn from 'picospawn';
2
+ import { PicospawnOptions } from 'picospawn';
3
+ import { BuildOptions } from './build.js';
2
4
 
5
+ interface DevOptions extends PicospawnOptions, BuildOptions {
6
+ /**
7
+ * Customize the port to use for the development server.
8
+ * @default 8080
9
+ */
10
+ port?: string | number;
11
+ }
3
12
  /**
4
13
  * Start the development server in a separate process.
5
14
  */
6
- declare function dev(root?: string): picospawn.PicospawnPromise<string>;
15
+ declare function dev(root?: string, { port, define, ...options }?: DevOptions): picospawn.PicospawnPromise<unknown>;
7
16
 
8
- export { dev };
17
+ export { type DevOptions, dev };
package/dist/tools/dev.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dev
3
- } from "../chunk-XTHOFO7F.js";
3
+ } from "../chunk-DG37B63B.js";
4
4
  export {
5
5
  dev
6
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.0",
4
+ "version": "0.1.2",
5
5
  "bin": "./dist/main.js",
6
6
  "exports": {
7
7
  "types": "./dist/index.d.ts",
@@ -39,7 +39,8 @@
39
39
  "find-up-simple": "^1.0.1",
40
40
  "joycon": "^3.1.1",
41
41
  "ordana": "^0.4.0",
42
- "picospawn": "^0.3.1",
42
+ "picospawn": "^0.3.2",
43
+ "source-map-support": "^0.5.21",
43
44
  "tinyglobby": "^0.2.13"
44
45
  },
45
46
  "peerDependencies": {
package/readme.md CHANGED
@@ -27,3 +27,7 @@ When you're ready to deploy, use the `build` command to bundle your functions.
27
27
  ```sh
28
28
  npx cloud-run-functions build ./path/to/functions/
29
29
  ```
30
+
31
+ ## Tips
32
+
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.
@@ -1,25 +0,0 @@
1
- // src/tools/dev.ts
2
- import { findUpSync } from "find-up-simple";
3
- import path from "node:path";
4
- import $ from "picospawn";
5
- function dev(root) {
6
- const packageDir = findUpSync("dist", {
7
- cwd: import.meta.dirname,
8
- type: "directory"
9
- });
10
- const source = path.join(packageDir, "targets/dev.js");
11
- const binDir = path.resolve(packageDir, "../node_modules/.bin");
12
- return $("functions-framework --target=dev --source", [source], {
13
- stdio: "inherit",
14
- env: {
15
- ...process.env,
16
- CRF_ROOT: root,
17
- CALLER_DIR: process.cwd(),
18
- PATH: `${binDir}:${process.env.PATH}`
19
- }
20
- });
21
- }
22
-
23
- export {
24
- dev
25
- };