From 29c10b1dc94e4353c09d8882191f3051061a895b Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 21 Aug 2025 12:15:47 -0700 Subject: [PATCH 1/9] feat: export main at package level --- src/function_registry.ts | 2 +- src/index.ts | 5 +++++ src/main.ts | 38 ++++++++++++++++++++++++++++++-------- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/function_registry.ts b/src/function_registry.ts index 788190ccf..7410aa2a6 100644 --- a/src/function_registry.ts +++ b/src/function_registry.ts @@ -34,7 +34,7 @@ const registrationContainer = new Map>(); /** * Helper method to store a registered function in the registration container */ -const register = ( +const register = ( functionName: string, signatureType: SignatureType, userFunction: HandlerFunction, diff --git a/src/index.ts b/src/index.ts index 9c804b631..477443311 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,3 +21,8 @@ export * from './functions'; * @public */ export {http, cloudEvent} from './function_registry'; + +/** + * @public + */ +export {main as run} from './main'; diff --git a/src/main.ts b/src/main.ts index fed4987b9..edc0ebee4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,13 +21,22 @@ import {getUserFunction} from './loader'; import {ErrorHandler} from './invoker'; import {getServer} from './server'; import {parseOptions, helpText, OptionsError} from './options'; +import { + HttpFunction, + EventFunction, + CloudEventFunction, + HandlerFunction, +} from './functions'; import {loggingHandlerAddExecutionContext} from './logger'; /** * Main entrypoint for the functions framework that loads the user's function * and starts the HTTP server. + * @param code - A function to be executed. */ -export const main = async () => { +export const main = async ( + code?: HttpFunction | EventFunction | CloudEventFunction, +) => { try { const options = parseOptions(); @@ -40,11 +49,21 @@ export const main = async () => { loggingHandlerAddExecutionContext(); } - const loadedFunction = await getUserFunction( - options.sourceLocation, - options.target, - options.signatureType, - ); + let loadedFunction; + // If a function is provided directly, use it. + if (code) { + loadedFunction = { + userFunction: code, + signatureType: options.signatureType || 'http', + }; + } else { + // Otherwise, load the function from file. + loadedFunction = await getUserFunction( + options.sourceLocation, + options.target, + options.signatureType, + ); + } if (!loadedFunction) { console.error('Could not load the function, shutting down.'); // eslint-disable-next-line no-process-exit @@ -55,7 +74,7 @@ export const main = async () => { // It is possible to overwrite the configured signature type in code so we // reset it here based on what we loaded. options.signatureType = signatureType; - const server = getServer(userFunction!, options); + const server = getServer(userFunction as HandlerFunction, options); const errorHandler = new ErrorHandler(server); server .listen(options.port, () => { @@ -79,4 +98,7 @@ export const main = async () => { }; // Call the main method to load the user code and start the http server. -void main(); +// Only call main if the module is not being required. +if (require.main === module) { + void main(); +} From adcf757733a21639a11fddbcd48a232ffc556a9b Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 21 Aug 2025 13:52:12 -0700 Subject: [PATCH 2/9] update docs --- docs/generated/api.json | 65 ++++++++++++++++++++++++++++++++++++ docs/generated/api.md.api.md | 4 +++ 2 files changed, 69 insertions(+) diff --git a/docs/generated/api.json b/docs/generated/api.json index 6fc566951..bc13ffed8 100644 --- a/docs/generated/api.json +++ b/docs/generated/api.json @@ -1589,6 +1589,71 @@ "endIndex": 2 } ] + }, + { + "kind": "Function", + "canonicalReference": "@google-cloud/functions-framework!run_2:function(1)", + "docComment": "/**\n * Main entrypoint for the functions framework that loads the user's function and starts the HTTP server.\n *\n * @param code - A function to be executed.\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "main: (code?: " + }, + { + "kind": "Reference", + "text": "HttpFunction", + "canonicalReference": "@google-cloud/functions-framework!HttpFunction:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "EventFunction", + "canonicalReference": "@google-cloud/functions-framework!EventFunction:interface" + }, + { + "kind": "Content", + "text": " | " + }, + { + "kind": "Reference", + "text": "CloudEventFunction", + "canonicalReference": "@google-cloud/functions-framework!CloudEventFunction:interface" + }, + { + "kind": "Content", + "text": ") => " + }, + { + "kind": "Reference", + "text": "Promise", + "canonicalReference": "!Promise:interface" + }, + { + "kind": "Content", + "text": "" + } + ], + "fileUrlPath": "src/main.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 9 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "code", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 6 + }, + "isOptional": true + } + ], + "name": "run_2" } ] } diff --git a/docs/generated/api.md.api.md b/docs/generated/api.md.api.md index 7e227ada4..56de67548 100644 --- a/docs/generated/api.md.api.md +++ b/docs/generated/api.md.api.md @@ -105,6 +105,10 @@ export { Request_2 as Request } export { Response_2 as Response } +// @public +const run_2: (code?: HttpFunction | EventFunction | CloudEventFunction) => Promise; +export { run_2 as run } + // (No @packageDocumentation comment for this package) ``` From f3fa19ffe7e5b443734a219f5f2dfde1b8b745f4 Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 21 Aug 2025 15:07:47 -0700 Subject: [PATCH 3/9] add test --- test/integration/programmatic.ts | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 test/integration/programmatic.ts diff --git a/test/integration/programmatic.ts b/test/integration/programmatic.ts new file mode 100644 index 000000000..e86b86d4a --- /dev/null +++ b/test/integration/programmatic.ts @@ -0,0 +1,108 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as supertest from 'supertest'; +import {main} from '../../src/main'; +import * as server from '../../src/server'; +import {HttpFunction, CloudEventFunction} from '../../src/functions'; +import {Server} from 'http'; + +describe('programmatic functions', () => { + let exitStub: sinon.SinonStub; + let errorStub: sinon.SinonStub; + let getServerStub: sinon.SinonStub; + + beforeEach(() => { + exitStub = sinon.stub(process, 'exit'); + errorStub = sinon.stub(console, 'error'); + }); + + afterEach(() => { + exitStub.restore(); + errorStub.restore(); + if (getServerStub) { + getServerStub.restore(); + } + }); + + it('should run an HTTP function', async () => { + const httpFunc: HttpFunction = (req, res) => { + res.send('hello'); + }; + + let capturedServer: Server | null = null; + let listenStub: sinon.SinonStub; + const originalGetServer = server.getServer; + getServerStub = sinon.stub(server, 'getServer').callsFake((fn, opts) => { + const s = originalGetServer(fn, opts); + capturedServer = s; + listenStub = sinon.stub(s, 'listen').returns(s); + return s; + }); + + await main(httpFunc); + + listenStub!.restore(); + + assert.ok(capturedServer); + const st = supertest(capturedServer!); + const response = await st.get('/'); + assert.strictEqual(response.status, 200); + assert.strictEqual(response.text, 'hello'); + }); + + it('should run a CloudEvent function', async () => { + let receivedEvent: any = null; + const cloudEventFunc: CloudEventFunction = cloudEvent => { + receivedEvent = cloudEvent; + }; + + let capturedServer: Server | null = null; + let listenStub: sinon.SinonStub; + const originalGetServer = server.getServer; + getServerStub = sinon.stub(server, 'getServer').callsFake((fn, opts) => { + const s = originalGetServer(fn, opts); + capturedServer = s; + listenStub = sinon.stub(s, 'listen').returns(s); + return s; + }); + + const argv = process.argv; + process.argv = ['node', 'index.js', '--signature-type=cloudevent']; + await main(cloudEventFunc); + process.argv = argv; + + listenStub!.restore(); + + assert.ok(capturedServer); + const st = supertest(capturedServer!); + const event = { + specversion: '1.0', + type: 'com.google.cloud.storage', + source: 'test', + id: 'test', + data: 'hello', + }; + const response = await st + .post('/') + .send(event) + .set('Content-Type', 'application/cloudevents+json'); + + assert.strictEqual(response.status, 204); + assert.ok(receivedEvent); + assert.strictEqual(receivedEvent.data, 'hello'); + }); +}); From 11c74aff605a7b33ecebcc211c130ce64804b74b Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 21 Aug 2025 15:22:11 -0700 Subject: [PATCH 4/9] update README --- README.md | 29 +++++++++++++++++++++++++++++ test/integration/programmatic.ts | 1 + 2 files changed, 30 insertions(+) diff --git a/README.md b/README.md index 88144492e..75cb266a2 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ npm install @google-cloud/functions-framework }; ``` +Option 1: + 1. Run the following command: ```sh @@ -76,6 +78,33 @@ npm install @google-cloud/functions-framework 1. Open http://localhost:8080/ in your browser and see _Hello, World_. +Option 2: + +1. Create a `main.js` file with the following contents: + + ```js + import { run } from '@google-cloud/functions-framework'; + import { helloWorld } from './hello.js'; + + run(helloWorld); + ``` + +1. Run `node main.js` to start the local development server to serve the function: + + ``` + Serving function... + Function: function + Signature type: http + URL: http://localhost:8080/ + ``` + +1. Send requests to this function using `curl` from another terminal window: + + ```sh + curl localhost:8080 + # Output: Hello, World + ``` + ### Quickstart: Set up a new project 1. Create a `package.json` file using `npm init`: diff --git a/test/integration/programmatic.ts b/test/integration/programmatic.ts index e86b86d4a..4f9a56e22 100644 --- a/test/integration/programmatic.ts +++ b/test/integration/programmatic.ts @@ -65,6 +65,7 @@ describe('programmatic functions', () => { }); it('should run a CloudEvent function', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let receivedEvent: any = null; const cloudEventFunc: CloudEventFunction = cloudEvent => { receivedEvent = cloudEvent; From ddbd87ede7efabb469a961b94524af2dbca617f0 Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 4 Sep 2025 15:11:29 -0700 Subject: [PATCH 5/9] update gemini --- .gemini/settings.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gemini/settings.json diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 000000000..0ac7545be --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,6 @@ + { + "experimental": { + "extensionManagement": true + } + } + \ No newline at end of file From 50e79025d3b3916ca0e71048d9c330c1852de6b5 Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 13 Jan 2026 14:36:41 -0800 Subject: [PATCH 6/9] dotguides --- .gitignore | 3 +- .guides/config.json | 1 + .guides/docs/topic.prompt | 11 +++++++ .guides/usage.prompt | 2 ++ .kiro/steering/product.md | 19 +++++++++++++ .kiro/steering/structure.md | 57 +++++++++++++++++++++++++++++++++++++ .kiro/steering/tech.md | 56 ++++++++++++++++++++++++++++++++++++ 7 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 .guides/config.json create mode 100644 .guides/docs/topic.prompt create mode 100644 .guides/usage.prompt create mode 100644 .kiro/steering/product.md create mode 100644 .kiro/steering/structure.md create mode 100644 .kiro/steering/tech.md diff --git a/.gitignore b/.gitignore index d07654551..12a4a3610 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ function_output.json serverlog_stderr.txt serverlog_stdout.txt temp -test/conformance/package-lock.json \ No newline at end of file +test/conformance/package-lock.json +firebase-debug.log \ No newline at end of file diff --git a/.guides/config.json b/.guides/config.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/.guides/config.json @@ -0,0 +1 @@ +{} diff --git a/.guides/docs/topic.prompt b/.guides/docs/topic.prompt new file mode 100644 index 000000000..b1d8efb51 --- /dev/null +++ b/.guides/docs/topic.prompt @@ -0,0 +1,11 @@ +--- +title: run a function from server.js +description: how to invoke a function from the server code instead of using the Functions Framework CLI +--- +It is possible to serve a remote function directly from the server code instead of using the Functions Framework CLI. + +The benefits include: + +- Functions can be started with node directly: `node src/server.js` +- Furnctions can be used with node watcher without extra setup: `node watch src/server.js` +- Better Typescript support with ts-node/tsx: `tsx --watch src/server.ts` \ No newline at end of file diff --git a/.guides/usage.prompt b/.guides/usage.prompt new file mode 100644 index 000000000..97a8f9733 --- /dev/null +++ b/.guides/usage.prompt @@ -0,0 +1,2 @@ +This is a local development version of the package. + diff --git a/.kiro/steering/product.md b/.kiro/steering/product.md new file mode 100644 index 000000000..b71d2ed35 --- /dev/null +++ b/.kiro/steering/product.md @@ -0,0 +1,19 @@ +# Product Overview + +The Google Cloud Functions Framework for Node.js is an open-source FaaS (Function as a Service) framework built on Express.js for writing portable Node.js functions. + +## Core Purpose +- Enables writing lightweight functions that run across multiple environments (Google Cloud Functions, Cloud Run, local development, Knative) +- Provides automatic request handling and event unmarshalling +- Supports multiple function signatures: HTTP, background events, and CloudEvents +- Eliminates the need to write HTTP server boilerplate + +## Key Features +- Local development server for testing +- Automatic CloudEvents spec compliance +- Portable between serverless platforms +- Express.js-based request/response handling +- Support for Google Cloud Functions event payloads + +## Target Users +Developers building serverless functions for Google Cloud Platform and other Knative-compatible environments. \ No newline at end of file diff --git a/.kiro/steering/structure.md b/.kiro/steering/structure.md new file mode 100644 index 000000000..70d12aa94 --- /dev/null +++ b/.kiro/steering/structure.md @@ -0,0 +1,57 @@ +# Project Structure + +## Root Directory +- `package.json` - Main package configuration and dependencies +- `tsconfig.json` - TypeScript configuration extending gts +- `api-extractor.json` - API documentation extraction config +- `.eslintrc.json` - ESLint configuration extending gts +- `.prettierrc.js` - Prettier configuration via gts + +## Source Code (`src/`) +- `index.ts` - Main public API exports +- `main.ts` - CLI entry point and server startup +- `functions.ts` - Core function registration and handling +- `function_registry.ts` - Function registration system +- `function_wrappers.ts` - Function signature adapters +- `server.ts` - Express server setup and configuration +- `invoker.ts` - Function invocation logic +- `loader.ts` - Dynamic function loading +- `types.ts` - TypeScript type definitions +- `options.ts` - Configuration and command-line options +- `logger.ts` - Logging utilities +- `testing.ts` - Testing utilities (exported separately) + +### Middleware (`src/middleware/`) +- `background_event_to_cloud_event.ts` - Event format conversion +- `cloud_event_to_background_event.ts` - Reverse event conversion +- `timeout.ts` - Request timeout handling + +### Other Core Files +- `cloud_events.ts` - CloudEvents specification handling +- `execution_context.ts` - Request execution context +- `async_local_storage.ts` - Async context management +- `pubsub_middleware.ts` - Pub/Sub specific middleware + +## Testing (`test/`) +- Mirror structure of `src/` directory +- `integration/` - Integration tests for different function types +- `system-test/` - End-to-end system tests +- `conformance/` - Conformance test setup +- `data/` - Test fixtures and sample functions + +## Build Output (`build/`) +- Generated JavaScript and type definitions +- Mirrors `src/` structure +- Main exports: `build/src/index.js` and `build/src/index.d.ts` + +## Documentation (`docs/`) +- `generated/` - Auto-generated API documentation +- Various markdown guides for specific features +- `esm/` - ESM usage examples + +## Conventions +- TypeScript source files in `src/` +- Corresponding test files in `test/` with same name +- Middleware components in dedicated subdirectory +- Integration tests grouped by function signature type +- All exports go through `src/index.ts` for clean public API \ No newline at end of file diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md new file mode 100644 index 000000000..10ffa1da8 --- /dev/null +++ b/.kiro/steering/tech.md @@ -0,0 +1,56 @@ +# Technology Stack + +## Core Technologies +- **Language**: TypeScript/JavaScript (Node.js >=10.0.0) +- **Framework**: Express.js for HTTP handling +- **Build System**: TypeScript compiler with Google TypeScript Style (gts) +- **Testing**: Mocha test framework +- **Package Manager**: npm + +## Key Dependencies +- `express` - HTTP server framework +- `cloudevents` - CloudEvents specification support +- `body-parser` - Request body parsing +- `minimist` - Command-line argument parsing + +## Development Tools +- **Linting**: ESLint with Google TypeScript Style (gts) +- **Formatting**: Prettier (configured via gts) +- **Type Checking**: TypeScript 5.8.2 +- **API Documentation**: Microsoft API Extractor + +## Common Commands + +### Development +```bash +npm run build # Clean, compile TypeScript +npm run compile # Compile TypeScript only +npm run watch # Compile with watch mode +npm run clean # Clean build directory +``` + +### Testing +```bash +npm test # Run unit tests +npm run conformance # Run conformance tests (requires Go 1.16+) +npm run pretest # Compile before testing +``` + +### Code Quality +```bash +npm run check # Run gts linting +npm run fix # Auto-fix linting issues +npm run docs # Generate API documentation +``` + +### Local Development +```bash +npx @google-cloud/functions-framework --target=functionName +functions-framework --target=functionName # If globally installed +``` + +## Build Output +- Compiled JavaScript: `build/src/` +- Type definitions: `build/src/**/*.d.ts` +- Main entry: `build/src/index.js` +- CLI binary: `build/src/main.js` \ No newline at end of file From 91d0690dbfee246509e4af20fd988386c931ff23 Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 13 Jan 2026 15:03:43 -0800 Subject: [PATCH 7/9] update gemini settings --- .gemini/settings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gemini/settings.json b/.gemini/settings.json index 0ac7545be..e60955590 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -1,6 +1,10 @@ { "experimental": { "extensionManagement": true + }, + "mcp": { + "enabled": true, + "excluded": ["gemini-cloud-assist-mcp", "firebase", "julesServer", "securityServer", "osvScanner", "observability"] } } \ No newline at end of file From 772ad962b8d3811bcc1a1cac6527d04f7071dae6 Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 13 Jan 2026 15:15:11 -0800 Subject: [PATCH 8/9] mod --- .gemini/settings.json | 4 ++++ .guides/docs/topic.prompt | 11 ----------- 2 files changed, 4 insertions(+), 11 deletions(-) delete mode 100644 .guides/docs/topic.prompt diff --git a/.gemini/settings.json b/.gemini/settings.json index 0ac7545be..fd60aa9a4 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -1,6 +1,10 @@ { "experimental": { "extensionManagement": true + }, + "mcp": { + "enabled": true, + "excluded": ["gemini-cloud-assist-mcp", "firebase", "julesServer", "securityServer", "osvScanner", "observability"] } } \ No newline at end of file diff --git a/.guides/docs/topic.prompt b/.guides/docs/topic.prompt deleted file mode 100644 index b1d8efb51..000000000 --- a/.guides/docs/topic.prompt +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: run a function from server.js -description: how to invoke a function from the server code instead of using the Functions Framework CLI ---- -It is possible to serve a remote function directly from the server code instead of using the Functions Framework CLI. - -The benefits include: - -- Functions can be started with node directly: `node src/server.js` -- Furnctions can be used with node watcher without extra setup: `node watch src/server.js` -- Better Typescript support with ts-node/tsx: `tsx --watch src/server.ts` \ No newline at end of file From fd782e306d33b41dcc2bf43fbdcdceaab9375d3c Mon Sep 17 00:00:00 2001 From: Yara Date: Tue, 13 Jan 2026 15:15:31 -0800 Subject: [PATCH 9/9] mod --- .guides/docs/runFunctions.prompt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .guides/docs/runFunctions.prompt diff --git a/.guides/docs/runFunctions.prompt b/.guides/docs/runFunctions.prompt new file mode 100644 index 000000000..ad3705b1a --- /dev/null +++ b/.guides/docs/runFunctions.prompt @@ -0,0 +1,15 @@ +--- +title: run a function from a plain Node.js script +description: when to invoke a function from the server code instead of using the Functions Framework CLI +--- +It is possible to serve a remote function directly from a plain script instead of the Functions Framework CLI. + +The benefits include: + +- Functions can be started with node directly: `node src/server.js` +- Furnctions can be used with node watcher without extra setup: `node watch src/server.js` +- Better Typescript support with ts-node/tsx: `tsx --watch src/server.ts` +- Easier debugging in IDEs because it is much easier to attach a debugger (like in VS Code or WebStorm) a plain Node.js script (node main.js) than to a command-line tool spawned via npx +- Custom initialization code because running code before server starts can be useful + +When adding integration tests, choose this option so that you can easily stop the server instance within your test suite, rather than spawning a child process to run the CLI. \ No newline at end of file