diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d5e10d859..d4681179e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,4 @@ - Add emulator support to firestore MCP tools. (#8700) - Increased npm timeout for web frameworks to 60s. (#8702) - Fallback to reading web framework dependencies version directly from node_modules package.json when the npm timeout is reached. (#8702) +- Added support for loading TypeScript functions using tsx in the Functions Emulator. (#8663) diff --git a/src/deploy/functions/runtimes/index.ts b/src/deploy/functions/runtimes/index.ts index cb3aa66d1ab..207495d969e 100644 --- a/src/deploy/functions/runtimes/index.ts +++ b/src/deploy/functions/runtimes/index.ts @@ -67,6 +67,8 @@ export interface DelegateContext { // Absolute path of the source directory. sourceDir: string; runtime?: supported.Runtime; + // Whether this delegate is being created for the emulator + isEmulator?: boolean; } type Factory = (context: DelegateContext) => Promise; diff --git a/src/deploy/functions/runtimes/node/index.ts b/src/deploy/functions/runtimes/node/index.ts index 2649e59821e..8414a7451f0 100644 --- a/src/deploy/functions/runtimes/node/index.ts +++ b/src/deploy/functions/runtimes/node/index.ts @@ -25,6 +25,33 @@ import * as versioning from "./versioning"; import * as parseTriggers from "./parseTriggers"; import { fileExistsSync } from "../../../../fsutils"; +/** + * Get the TypeScript source entry point for a functions directory. + * Used by the emulator to load TypeScript directly with tsx. + */ +export function getTypeScriptEntryPoint(functionsDir: string): string | null { + const tsconfigPath = path.join(functionsDir, "tsconfig.json"); + if (!fileExistsSync(tsconfigPath)) { + return null; + } + + try { + const mainPath = require.resolve(functionsDir); + // Transform compiled JS path to TypeScript source + // e.g., /path/to/project/lib/index.js -> /path/to/project/src/index.ts + const tsSourcePath = mainPath.replace(/\/lib\//, "/src/").replace(/\.js$/, ".ts"); + + if (fileExistsSync(tsSourcePath)) { + return tsSourcePath; + } + } catch (e) { + logger.debug("Failed to resolve TS entrypoint", e); + // Fail-safe and fallback to assuming JS codebase. + } + + return null; +} + // The versions of the Firebase Functions SDK that added support for the container contract. const MIN_FUNCTIONS_SDK_VERSION = "3.20.0"; @@ -54,7 +81,13 @@ export async function tryCreateDelegate(context: DelegateContext): Promise { async function loadTriggers(): Promise { let triggerModule; + + // Check if we have an override entry point (e.g., for TypeScript with tsx) + const entryPoint = process.env.FUNCTIONS_SOURCE; + try { - triggerModule = require(process.cwd()); + if (entryPoint) { + logDebug(`Loading functions from specified entry point: ${entryPoint}`); + triggerModule = require(entryPoint); + } else { + triggerModule = require(process.cwd()); + } } catch (err: any) { if (err.code !== "ERR_REQUIRE_ESM") { // Try to run diagnostics to see what could've gone wrong before rethrowing the error. await moduleResolutionDetective(err); throw err; } - const modulePath = require.resolve(process.cwd()); + const modulePath = entryPoint || require.resolve(process.cwd()); // Resolve module path to file:// URL. Required for windows support. const moduleURL = pathToFileURL(modulePath).href; triggerModule = await dynamicImport(moduleURL); diff --git a/templates/init/functions/typescript/package.lint.json b/templates/init/functions/typescript/package.lint.json index c2d4f5493e6..57deecb2036 100644 --- a/templates/init/functions/typescript/package.lint.json +++ b/templates/init/functions/typescript/package.lint.json @@ -25,7 +25,8 @@ "eslint-config-google": "^0.14.0", "eslint-plugin-import": "^2.25.4", "firebase-functions-test": "^3.1.0", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "tsx": "^4.19.4" }, "private": true } diff --git a/templates/init/functions/typescript/package.nolint.json b/templates/init/functions/typescript/package.nolint.json index 066161c31cd..5ce934823b2 100644 --- a/templates/init/functions/typescript/package.nolint.json +++ b/templates/init/functions/typescript/package.nolint.json @@ -19,7 +19,8 @@ }, "devDependencies": { "typescript": "^5.7.3", - "firebase-functions-test": "^3.1.0" + "firebase-functions-test": "^3.1.0", + "tsx": "^4.19.4" }, "private": true }