cloud-run-functions 0.1.0 → 0.1.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.
@@ -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,38 @@
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
- import { findUpSync } from "find-up-simple";
5
+ import fs2 from "node:fs";
6
+ import { Module } from "node:module";
5
7
  import os from "node:os";
6
8
  import path2 from "node:path";
7
9
 
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
10
  // node_modules/.pnpm/radashi@12.4.0/node_modules/radashi/dist/radashi.js
11
+ var TimeoutError = class extends Error {
12
+ constructor(message) {
13
+ super(message ?? "Operation timed out");
14
+ this.name = "TimeoutError";
15
+ }
16
+ };
17
+ function timeout(ms, error) {
18
+ return new Promise(
19
+ (_, reject) => setTimeout(
20
+ () => reject(isFunction(error) ? error() : new TimeoutError(error)),
21
+ ms
22
+ )
23
+ );
24
+ }
25
+ async function toResult(promise) {
26
+ try {
27
+ const result = await promise;
28
+ return [void 0, result];
29
+ } catch (error) {
30
+ if (isError(error)) {
31
+ return [error, void 0];
32
+ }
33
+ throw error;
34
+ }
35
+ }
17
36
  function dedent(text, ...values) {
18
37
  var _a;
19
38
  if (isArray(text)) {
@@ -41,8 +60,26 @@ var asyncIteratorSymbol = (
41
60
  /* c8 ignore next */
42
61
  Symbol.asyncIterator || Symbol.for("Symbol.asyncIterator")
43
62
  );
63
+ function isError(value) {
64
+ return isTagged(value, "[object Error]");
65
+ }
66
+ function isFunction(value) {
67
+ return typeof value === "function";
68
+ }
69
+ function isNumber(value) {
70
+ return typeof value === "number" && !Number.isNaN(value);
71
+ }
72
+ function isTagged(value, tag) {
73
+ return Object.prototype.toString.call(value) === tag;
74
+ }
75
+
76
+ // src/config/index.ts
77
+ import * as z2 from "@zod/mini";
78
+ import Joycon from "joycon";
79
+ import path from "node:path";
44
80
 
45
81
  // src/config/schema.ts
82
+ import * as z from "@zod/mini";
46
83
  var configSchema = z.partial(
47
84
  z.interface({
48
85
  root: z.string().register(z.globalRegistry, {
@@ -81,6 +118,13 @@ var configSchema = z.partial(
81
118
 
82
119
  @default "node"
83
120
  `
121
+ }),
122
+ maxInstanceConcurrency: z.union([z.number(), z.record(z.string(), z.number())]).register(z.globalRegistry, {
123
+ description: dedent`
124
+ 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.
125
+
126
+ @default 5
127
+ `
84
128
  })
85
129
  })
86
130
  );
@@ -116,11 +160,10 @@ function hash(data, len) {
116
160
 
117
161
  // src/targets/dev.ts
118
162
  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;
163
+ const options = JSON.parse(process.env.CRF_OPTIONS);
164
+ const searchDir = path2.resolve(options.workingDir, options.searchDir ?? "");
121
165
  const config = loadConfig(searchDir);
122
166
  const root = config.configDir ? path2.resolve(config.configDir, config.root ?? "") : searchDir;
123
- let pendingBuild;
124
167
  const entryPoints = [];
125
168
  const requiredSuffix = config.entrySuffix?.replace(/^\.?/, ".") ?? "";
126
169
  const knownSuffixes = /* @__PURE__ */ new Set();
@@ -142,27 +185,25 @@ async function createBuild() {
142
185
  const knownSuffixesRE = new RegExp(
143
186
  `(${Array.from(knownSuffixes, (e) => e.replace(/\./g, "\\.")).sort((a, b) => b.length - a.length).join("|")})$`
144
187
  );
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)}`)
188
+ const cacheDir = emptyDir(
189
+ path2.join(
190
+ fs2.realpathSync(os.tmpdir()),
191
+ "cloud-run-functions-" + hash(root, 8)
192
+ )
151
193
  );
152
- console.log({
153
- root,
154
- outDir,
155
- entryPoints,
156
- knownSuffixesRE
157
- });
194
+ let pendingBuild;
195
+ let finishedBuild;
158
196
  const context = await esbuild.context({
159
197
  entryPoints,
160
198
  absWorkingDir: root,
161
- outdir: outDir,
199
+ outdir: cacheDir,
200
+ define: options.define,
162
201
  bundle: true,
163
- format: "esm",
164
- packages: nodeModulesDir ? "external" : "bundle",
202
+ format: "cjs",
203
+ platform: "node",
204
+ packages: "bundle",
165
205
  sourcemap: true,
206
+ sourcesContent: false,
166
207
  metafile: true,
167
208
  logOverride: {
168
209
  "empty-glob": "silent"
@@ -171,11 +212,16 @@ async function createBuild() {
171
212
  {
172
213
  name: "build-status",
173
214
  setup(build) {
215
+ pendingBuild = Promise.withResolvers();
174
216
  build.onStart(() => {
175
- pendingBuild = Promise.withResolvers();
217
+ pendingBuild ??= Promise.withResolvers();
176
218
  });
177
219
  build.onEnd((result) => {
178
- pendingBuild.resolve(result);
220
+ if (pendingBuild) {
221
+ pendingBuild.resolve(result);
222
+ pendingBuild = void 0;
223
+ }
224
+ finishedBuild = result;
179
225
  });
180
226
  }
181
227
  }
@@ -186,11 +232,16 @@ async function createBuild() {
186
232
  try {
187
233
  const dotenv = await import("dotenv");
188
234
  dotenv.config();
235
+ console.log("[dotenv] Environment variables loaded.");
189
236
  } catch {
190
237
  }
238
+ const taskStates = /* @__PURE__ */ new Map();
191
239
  return {
192
240
  async match(url) {
193
- const result = await pendingBuild.promise;
241
+ const result = await pendingBuild?.promise ?? finishedBuild;
242
+ if (!result) {
243
+ return null;
244
+ }
194
245
  for (const [file, output] of Object.entries(
195
246
  result.metafile?.outputs ?? {}
196
247
  )) {
@@ -199,18 +250,50 @@ async function createBuild() {
199
250
  }
200
251
  const taskName = output.entryPoint.replace(knownSuffixesRE, "");
201
252
  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;
253
+ const taskState = taskStates.get(taskName) ?? {
254
+ running: 0,
255
+ queue: []
256
+ };
257
+ const taskConcurrency = isNumber(config.maxInstanceConcurrency) ? config.maxInstanceConcurrency : config.maxInstanceConcurrency?.[taskName] ?? 5;
258
+ if (taskState.running >= taskConcurrency) {
259
+ const ticket = Promise.withResolvers();
260
+ taskState.queue.push(ticket);
261
+ const [error] = await toResult(
262
+ Promise.race([ticket.promise, timeout(3e4)])
263
+ );
264
+ if (error) {
265
+ return (_req, res) => {
266
+ res.status(429).end();
267
+ };
268
+ }
269
+ }
270
+ taskState.running++;
271
+ taskStates.set(taskName, taskState);
272
+ const require2 = Module.createRequire(import.meta.filename);
273
+ let taskHandler = require2(path2.join(root, file));
274
+ while (taskHandler && typeof taskHandler !== "function") {
275
+ taskHandler = taskHandler.default;
276
+ }
277
+ if (!taskHandler) {
278
+ return () => {
279
+ throw new Error(`Task ${taskName} is not a function.`);
280
+ };
281
+ }
206
282
  switch (config.adapter) {
207
283
  case "hattip": {
208
284
  const { createMiddleware } = await import("@hattip/adapter-node");
209
- return createMiddleware(taskHandler);
285
+ taskHandler = createMiddleware(taskHandler);
210
286
  }
211
- default:
212
- return taskHandler;
213
287
  }
288
+ return (req, res) => {
289
+ const end = res.end.bind(res);
290
+ res.end = (...args) => {
291
+ taskState.running--;
292
+ taskState.queue.shift()?.resolve();
293
+ return end(...args);
294
+ };
295
+ return taskHandler(req, res);
296
+ };
214
297
  }
215
298
  }
216
299
  return null;
@@ -223,7 +306,12 @@ functions.http("dev", async (req, res) => {
223
306
  const build = await buildPromise;
224
307
  const handler = await build.match(url);
225
308
  if (handler) {
226
- handler(req, res);
309
+ try {
310
+ await handler(req, res);
311
+ } catch (error) {
312
+ console.error(error);
313
+ res.status(500).end();
314
+ }
227
315
  } else {
228
316
  res.status(404).end();
229
317
  }
@@ -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.1",
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
- };