Skip to content

feat: add Dart runtime delegate for emulator#9966

Merged
joehan merged 29 commits into
firebase:mainfrom
invertase:@invertase/dart
Mar 18, 2026
Merged

feat: add Dart runtime delegate for emulator#9966
joehan merged 29 commits into
firebase:mainfrom
invertase:@invertase/dart

Conversation

@Lyokone
Copy link
Copy Markdown
Contributor

@Lyokone Lyokone commented Feb 24, 2026

Adds Dart as a new runtime for Firebase Functions, gated behind the functionsrunapionly experiment flag.

  • Dart runtime delegate: Detects Dart projects via pubspec.yaml, discovers functions from functions.yaml generated by
    build_runner, and manages build_runner watch for hot reload via the delegate's watch() method.
  • Emulator support: Dart uses a shared-process model (like Python) — one process serves all functions, routing by request path
    instead of FUNCTION_TARGET env var.
  • firebase init functions: Scaffolds a new Dart Functions project.
  • Added dart3 runtime with experimental status and isLanguageRuntime() helper for clean runtime checks.

Scenarios Tested

  • npm run build / npm run lint / npm test — all pass
  • npm run generate:json-schema — regenerated with dart3
  • Manual emulator testing: HTTPS triggers route correctly, hot reload works, clean shutdown

Sample Commands

firebase experiments:enable functionsrunapionly
firebase init functions  # Select "Dart"
firebase emulators:start --only functions

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @Lyokone, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly expands Firebase Functions capabilities by introducing experimental support for Dart as a runtime. It provides a comprehensive Dart runtime delegate, integrates Dart's build_runner for function discovery and hot reload within the emulator, and adds firebase init scaffolding for new Dart projects. This enables developers to write and test Firebase Functions using Dart, leveraging its ecosystem and features.

Highlights

  • Dart Runtime Delegate: Introduced a new runtime delegate for Dart, enabling Firebase Functions to detect Dart projects via pubspec.yaml, discover functions from functions.yaml (generated by build_runner), and manage build_runner watch for hot reload.
  • Emulator Support for Dart: Implemented emulator support for Dart functions, utilizing a shared-process model similar to Python. This means one Dart process serves all functions, routing requests based on the path rather than FUNCTION_TARGET environment variables. Hot reload functionality is integrated through build_runner watch.
  • Firebase Init Functions for Dart: Added scaffolding for new Dart Functions projects within firebase init functions, including template files (pubspec.yaml, .gitignore, main.dart) and configuration for the latest supported Dart runtime and ignore rules.
  • Utility Function for Runtime Checks: Added a new helper function isLanguageRuntime() to simplify checks for whether an optional runtime string belongs to a specific language.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/package_config.json
    • Added a generated Dart package configuration file for a Crashlytics Flutter example.
  • scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/package_graph.json
    • Added a generated Dart package dependency graph for a Crashlytics Flutter example.
  • scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/version
    • Added a file specifying the Dart version for a Crashlytics Flutter example.
  • scripts/agent-evals/templates/crashlytics-flutter/.flutter-plugins-dependencies
    • Added a generated Flutter plugin dependencies file for a Crashlytics Flutter example.
  • scripts/agent-evals/templates/crashlytics-flutter/pubspec.lock
    • Added a Dart package lock file for a Crashlytics Flutter example.
  • src/deploy/functions/build.ts
    • Removed a comment indicating that the entryPoint field would become optional for the 'run' platform.
  • src/deploy/functions/runtimes/dart/index.ts
    • Added a new file implementing the Dart runtime delegate, including logic for pubspec.yaml detection, build_runner integration for function discovery and watch mode, and platform normalization.
  • src/deploy/functions/runtimes/index.ts
    • Reordered import statements for consistency.
  • src/deploy/functions/runtimes/supported/index.ts
    • Added isLanguageRuntime helper function to check if a runtime string corresponds to a given language.
  • src/emulator/functionsEmulator.ts
    • Imported isLanguageRuntime for runtime checks.
    • Added watchCleanups array to manage cleanup functions for watch processes.
    • Modified sendRequest to pass runtime information to worker pool methods and handle Dart-specific request paths.
    • Updated startWatching to first load triggers, then conditionally start build_runner watch for Dart projects, and manage its cleanup.
    • Removed redundant loadTriggers call from startWatching.
    • Added cleanup logic for watchCleanups in the stop method.
    • Implemented startDart method to initialize Dart runtime processes, including port assignment and environment setup.
    • Modified startRuntime to invoke startDart for Dart runtimes.
    • Updated addWorker to accept and pass the runtime type.
    • Adjusted handleHttpsRequest to construct Dart-specific request paths for shared-process routing and pass runtime information.
  • src/emulator/functionsRuntimeWorker.ts
    • Imported isLanguageRuntime for runtime checks.
    • Modified getKey to return a shared key (~dart-shared~) for Dart runtimes, reflecting its shared-process model.
    • Updated readyForWork, getIdleWorker, submitRequest, addWorker, getTriggerWorkers, and setTriggerWorkers to accept and utilize the runtime parameter for Dart-specific worker management.
  • src/init/features/functions/dart.ts
    • Added a new file providing setup logic for Dart functions during firebase init, including template generation for pubspec.yaml, .gitignore, and main.dart, and an option to install dependencies.
  • src/init/features/functions/index.ts
    • Added 'Dart' as a selectable language option in the firebase init functions wizard.
    • Configured Dart-specific ignore patterns and set the default runtime for Dart projects during initialization.
Activity
  • The author has performed manual emulator testing, confirming correct routing for HTTPS triggers, functional hot reload, and clean shutdown.
  • The author has verified that npm run build, npm run lint, and npm test all pass.
  • The author has regenerated the JSON schema using npm run generate:json-schema with dart3.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces Dart runtime support for Firebase Functions, including a new runtime delegate, emulator support with hot reload via build_runner watch, and scaffolding for new Dart projects during firebase init functions. The changes involve adding new files for Dart-specific configurations and modifying existing TypeScript files to integrate Dart into the functions emulator and runtime detection logic. The code appears to be well-structured and follows existing patterns for other runtimes like Python and Node.js. The changes also include updates to the .gitignore template and the firebase.json configuration to properly handle Dart projects.

Comment thread src/init/features/functions/dart.ts Outdated
Comment thread src/init/features/functions/dart.ts Outdated
Comment thread src/deploy/functions/build.ts
Comment thread src/deploy/functions/runtimes/dart/index.ts
Comment thread src/deploy/functions/runtimes/dart/index.ts Outdated
Comment thread src/emulator/functionsEmulator.ts Outdated
Comment thread src/emulator/functionsEmulator.ts Outdated
Comment thread src/emulator/functionsEmulator.ts Outdated
Comment thread src/emulator/functionsRuntimeWorker.ts
Comment thread src/deploy/functions/runtimes/dart/index.ts Outdated
Comment thread src/deploy/functions/runtimes/dart/index.ts Outdated
Comment thread src/deploy/functions/runtimes/dart/index.ts
@brittanycho brittanycho requested a review from inlined March 4, 2026 01:56
dependencies:
# TODO(ehesp): Replace with published package version once available on pub.dev
firebase_functions:
path: ../
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we leave this as a relative path or update it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As soon as it's published we'll be able to add the correct version

Comment thread templates/init/functions/dart/pubspec.yaml Outdated
Comment thread src/deploy/functions/runtimes/dart/index.ts
@brittanycho
Copy link
Copy Markdown
Contributor

brittanycho commented Mar 5, 2026

Left some additional comments, mostly minor nits and otherwise I think generally lgtm

@inlined, @Ehesp - was curious if you had any thoughts or comments here?

Copy link
Copy Markdown

@natebosch natebosch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dart package template LGTM after some tweaks. I did not review any of the typescript. The extra checked in files should likely be dropped from this PR.

Comment thread templates/init/functions/dart/server.dart Outdated
Comment thread templates/init/functions/dart/pubspec.yaml Outdated
Comment thread templates/init/functions/dart/pubspec.yaml Outdated
Comment thread scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/package_config.json Outdated
Comment thread scripts/agent-evals/templates/crashlytics-flutter/pubspec.lock Outdated
Comment thread templates/init/functions/dart/server.dart Outdated
Copy link
Copy Markdown
Member

@inlined inlined left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only feedback I'd ask you to critically think about is whether the dart behavior of having a shared pool should be all run functions.

With CRF I was thinking that we could group 1..n functions in a service. So one codebase of N functions can deploy 1 <= M <= N services. You split basically if you have security or resource reasons to split (at least for BG functions. For callable/https functions the URL changes and we need to spec that out).

Aside: Is that how Dart is going to work in prod too?

{ exit: 1 },
);
}
return Promise.resolve(new Delegate(context.projectId, context.sourceDir, runtime));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to call Promise.resolve if you're in an async function

const match = /Dart SDK version:\s*(\d+\.\d+\.\d+)/.exec(versionOutput);
if (match) {
const installedVersion = match[1];
if (installedVersion.localeCompare(MIN_DART_SDK_VERSION, undefined, { numeric: true }) < 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fascinating method of doing this. Do you want to make sure you blindly accept all versions of dart instead of using the semver library and doing semver arithmetic?


// Verify pubspec.yaml exists and is readable.
const pubspecYamlPath = path.join(this.sourceDir, "pubspec.yaml");
try {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the more modern version of what you do in tryCreateDelegate. Why not reconcile to just one?


// Verify the entry point file exists.
const entryPointPath = path.join(this.sourceDir, this.entryPoint);
try {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're going to check for three files using async libraries you might as well do it in parallel


// Run `dart pub get` if dependencies have not been resolved yet.
const packageConfigPath = path.join(this.sourceDir, ".dart_tool", "package_config.json");
try {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always prefer to return early and avoid nesting. If this try is effectively an if statement, consider something like:

try {
  await access
  return;
} catch () { /* noop */ }

// Install dependencies

Also, all of this is probably because exists is deprecated and you don't have a promise version? The reasons for its deprecation are basically irrelevant here (it's encouraged for you to try to do the thing rather than using exists as a dry run).

I might just define exists using promisify and simplify all this code:

const [pubspecExists, entryPointExists, packageConfigExists] = await Promise.all([pubspecYamlPath, entryPointPath, packageConfigPath]).map(exists);

if (!pubspecExists) {
  throw error
}

if (!entryPointExists) {
  throw erroR
}

if (packageConfigExists) {
  return;
}

// install pacakges

await fs.promises.access(packageConfigPath, fs.constants.R_OK);
} catch {
logLabeledBullet("functions", "running dart pub get...");
const pubGetProcess = spawn(this.bin, ["pub", "get"], {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is copying other code but I wonder if there's an opportunity for a utility here that we can use throughout the codebase...

import { spawn } from 'node:child_process';
import { createInterface } from 'node:readline';
import { once } from 'node:events';

async function execChild({
  bin: string,
  args: string[],
  cwd?: string,
  logsPrefix?: string,
): Promise<number> {
  const child = spawn('dart', ['pub', 'get'], { stdio: ['ignore', 'pipe', 'pipe'] });
  child.stderr?.pipe(child.stdout!);

  const rl = createInterface({ input: child.stdout!, terminal: false });
  const printOutput = (async () => {
    for await (const line of rl) {
      logger.debug(logsPrefix ? `${logsPrefix} ${line.trim}` ? line.trim()`);
    }
  })();

  const processEnd = Promise.race([
    once(child, 'close'),
    once(child, 'error').then(([err]) => { throw err; })
  ]);

  const [_, exitCode] = await Promise.all([printOutput, processEnd]);

  return exitCode;
}

Then you could just say

const exitCode = await execChild({ bin: this.bin, args: ["pub", "get"], logsPrefix: "[dart pub get"]});
if (exitCode) { 
  throw new FirebaseError(...);
}

here and below say

const buildRunnerExit =  await execChild({
  bin: this.bin,
  args: ["run", "build_runner", "build", "--delete-conflicting-outputs"],
  cwd: this.sourceDir,
  loggerPrefix: "[build_runner]"
});


// ...
const compileExit = await execChild({
  bin: this.bin,
  args: ["compile", "exe",  this.entryPoint, "-o", "bin/server", "--target-os=linux", "--target-arch=x64"],
  cwd: this.sourceDir,
  logsPrefix: "[dart compile]",
});

* Returns a cleanup function that stops the build_runner process.
* The returned promise resolves once the initial build completes.
*/
async watch(onRebuild?: () => void): Promise<() => Promise<void>> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing


// For Dart, include the function name in the path so the server can route
// For other runtimes, use / as they use FUNCTION_TARGET env var
const isDart = isLanguageRuntime(record.backend.runtime, "dart");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is a bit of a bigger change, but.... what if we just make this the behavior when platform is "run" and always make dart emit "run"?

We'll almost certainly want to investigate allowing collocation of functions in Run services if/when we move other languages to CRF.

runtime = await this.startPython(backend, { ...runtimeEnv, ...secretEnvs });
} else if (isLanguageRuntime(backend.runtime, "dart")) {
runtime = await this.startDart(backend, { ...runtimeEnv, ...secretEnvs });
} else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we now have the ability to now say

} else if (isLangaugeRuntime(backend.runtime, "nodejs")) {
  // ...
}
assertExhaustive(backend.runtime);

@brittanycho brittanycho enabled auto-merge (squash) March 16, 2026 23:00
@brittanycho brittanycho disabled auto-merge March 16, 2026 23:00
@brittanycho brittanycho enabled auto-merge (squash) March 16, 2026 23:04
@brittanycho brittanycho disabled auto-merge March 16, 2026 23:15
@brittanycho brittanycho enabled auto-merge (squash) March 16, 2026 23:35
@inlined inlined disabled auto-merge March 17, 2026 23:52
@joehan joehan merged commit 2a6f0d0 into firebase:main Mar 18, 2026
27 checks passed
andrewbrook pushed a commit that referenced this pull request Mar 25, 2026
* feat: Add dart delegate

* feat: add support for hot reloading

* fix: wait for build_runner initial build to complete

* fix rooting

* emulator update

* update functions.yaml directory

* clean run on emulator

* clear project

* improve runtime checks

* merge fixes

* clean

* Fixing feedback from Gemini

* compile before deploy

* remove placeholder runtime

* cross compiling

* emulator fix

* fix rooting

* fix emulator detection

* skipping compilation in emulator run

* clean output

* prevent duplicate watches

* dart entry point and min dart version enforcing

* change entry point to bin/server.dart

* update template

---------

Co-authored-by: Elliot Hesp <elliot.hesp@gmail.com>
Co-authored-by: Darren Ackers <ackers86@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants