Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ void main(List<String> args) {
}
```

> [!IMPORTANT]
> Register your functions directly inside the `runFunctions` (or `fireUp`)
> callback. The build-time manifest generator only discovers registrations made
> within that callback, so functions registered from helper methods called by the
> callback will run locally but won't be deployed.

## Status: Experimental

This package provides a Dart implementation of Firebase Cloud Functions. Only HTTPS triggers are currently supported in production. Other trigger types are experimental and have [varying levels of support](doc/triggers.md).
Expand Down
58 changes: 54 additions & 4 deletions lib/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -301,23 +301,34 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
/// firebase.https.onRequest(name: 'fn', options: opts, ...);
final Map<String, InstanceCreationExpression> _variableToOptionsExpr = {};

/// Whether the visitor is currently inside the `runFunctions`/`fireUp`
/// registration callback. Only registrations reached while this is true are
/// discovered, so functions must be registered directly in the callback.
var _isCollectingRegistrations = false;

@override
void visitMethodInvocation(MethodInvocation node) {
super.visitMethodInvocation(node);
final target = node.target;
final methodName = node.methodName.name;
if (target != null) {

if (target == null && _isFunctionsRunnerInvocation(methodName)) {
_visitRegistrationRunner(node);
return;
}

Comment thread
Lyokone marked this conversation as resolved.
if (_isCollectingRegistrations && target != null) {
// Check against all namespaces
for (final namespace in namespaces) {
if (namespace.isNamespace(target)) {
if (namespace.matches(methodName)) {
namespace.extractor(node, methodName);
// Found a match, no need to check other namespaces for this node
// The function handler runs later, so don't scan its body as if it
// were part of initialization.
return;
}
}
}
} else {
} else if (target == null) {
// Check for parameter definitions (top-level function calls with no target)
if (_isParamDefinition(methodName)) {
_extractParameterFromMethod(node, methodName);
Expand All @@ -332,6 +343,11 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
if (node.function case final SimpleIdentifier function) {
final functionName = function.name;

if (_isFunctionsRunnerInvocation(functionName)) {
_visitRegistrationRunnerArguments(functionName, node.argumentList);
return;
}

// Check for parameter definitions
if (_isParamDefinition(functionName)) {
_extractParameter(node, functionName);
Expand All @@ -354,6 +370,40 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
super.visitCompilationUnit(node);
}

bool _isFunctionsRunnerInvocation(String methodName) =>
methodName == 'runFunctions' || methodName == 'fireUp';

void _visitRegistrationRunner(MethodInvocation node) {
_visitRegistrationRunnerArguments(node.methodName.name, node.argumentList);
}

void _visitRegistrationRunnerArguments(String runnerName, ArgumentList args) {
final arguments = args.arguments;
final runnerArgIndex = runnerName == 'fireUp' ? 1 : 0;
if (arguments.length <= runnerArgIndex) return;

_visitRegistrationExpression(arguments[runnerArgIndex]);
}

void _visitRegistrationExpression(Expression expression) {
switch (expression) {
case FunctionExpression(:final body):
_visitCollectingRegistrations(() => body.accept(this));
case ParenthesizedExpression(:final expression):
_visitRegistrationExpression(expression);
}
}

void _visitCollectingRegistrations(void Function() visit) {
final previous = _isCollectingRegistrations;
_isCollectingRegistrations = true;
try {
visit();
} finally {
_isCollectingRegistrations = previous;
}
}

@override
void visitVariableDeclarationStatement(VariableDeclarationStatement node) {
// Track local variable declarations (e.g., `const opts = HttpsOptions(...)`
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/dart_reference/bin/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,16 @@ void main(List<String> args) async {
});
}

/// Registrations made outside the `runFunctions`/`fireUp` callback are not
/// discovered: functions must be registered directly inside the callback.
// ignore: unreachable_from_main
void unregisteredHelper(Firebase firebase) {
firebase.https.onRequest(
name: 'unregisteredHelper',
(request) async => Response.ok('This function should not be discovered'),
);
}

/// Options assigned to a top-level const variable.
const httpsVarOpts = HttpsOptions(
region: Region(SupportedRegion.europeWest3),
Expand Down
7 changes: 7 additions & 0 deletions test/snapshots/manifest_snapshot_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,13 @@ void main() {
}
});

test('should ignore uncalled top-level registration helpers', () {
final dartEndpoints = dartManifest['endpoints'] as Map;

expect(dartEndpoints, isNot(contains('unregisteredHelper')));
expect(dartEndpoints, isNot(contains('unregistered-helper')));
});

// =========================================================================
// Callable Functions Tests
// =========================================================================
Expand Down
Loading