From 29c10b1dc94e4353c09d8882191f3051061a895b Mon Sep 17 00:00:00 2001 From: Tianzi Cai Date: Thu, 21 Aug 2025 12:15:47 -0700 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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;