diff --git a/README.md b/README.md index a31c30e..f17ba1e 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,12 @@ void main(List 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). diff --git a/lib/builder.dart b/lib/builder.dart index e236f77..7fc5d5c 100644 --- a/lib/builder.dart +++ b/lib/builder.dart @@ -301,23 +301,34 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor { /// firebase.https.onRequest(name: 'fn', options: opts, ...); final Map _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; + } + + 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); @@ -332,6 +343,11 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor { 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); @@ -354,6 +370,40 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor { 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(...)` diff --git a/test/fixtures/dart_reference/bin/server.dart b/test/fixtures/dart_reference/bin/server.dart index 5adee59..5a27c0f 100644 --- a/test/fixtures/dart_reference/bin/server.dart +++ b/test/fixtures/dart_reference/bin/server.dart @@ -787,6 +787,16 @@ void main(List 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), diff --git a/test/snapshots/manifest_snapshot_test.dart b/test/snapshots/manifest_snapshot_test.dart index 852f794..8ca8964 100644 --- a/test/snapshots/manifest_snapshot_test.dart +++ b/test/snapshots/manifest_snapshot_test.dart @@ -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 // =========================================================================