diff --git a/scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/package_config.json b/scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/package_config.json new file mode 100644 index 00000000000..51e48c2db75 --- /dev/null +++ b/scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/package_config.json @@ -0,0 +1,232 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "_flutterfire_internals", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/_flutterfire_internals-1.3.35", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "async", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/async-2.13.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "boolean_selector", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "characters", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/characters-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "clock", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/clock-1.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "collection", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/collection-1.19.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "cupertino_icons", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/cupertino_icons-1.0.8", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "fake_async", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/fake_async-1.3.3", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "firebase_core", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/firebase_core-2.32.0", + "packageUri": "lib/", + "languageVersion": "2.18" + }, + { + "name": "firebase_core_platform_interface", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/firebase_core_platform_interface-5.4.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "firebase_core_web", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/firebase_core_web-2.24.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "firebase_crashlytics", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/firebase_crashlytics-3.5.7", + "packageUri": "lib/", + "languageVersion": "2.18" + }, + { + "name": "firebase_crashlytics_platform_interface", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/firebase_crashlytics_platform_interface-3.6.35", + "packageUri": "lib/", + "languageVersion": "2.18" + }, + { + "name": "flutter", + "rootUri": "file:///Users/guillaume/fvm/versions/stable/packages/flutter", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "flutter_lints", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/flutter_lints-2.0.3", + "packageUri": "lib/", + "languageVersion": "2.19" + }, + { + "name": "flutter_test", + "rootUri": "file:///Users/guillaume/fvm/versions/stable/packages/flutter_test", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "flutter_web_plugins", + "rootUri": "file:///Users/guillaume/fvm/versions/stable/packages/flutter_web_plugins", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "leak_tracker", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/leak_tracker-11.0.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "leak_tracker_flutter_testing", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "leak_tracker_testing", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.2", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "lints", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/lints-2.1.1", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "matcher", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/matcher-0.12.17", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "material_color_utilities", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "meta", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/meta-1.17.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "path", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/path-1.9.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "plugin_platform_interface", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "sky_engine", + "rootUri": "file:///Users/guillaume/fvm/versions/stable/bin/cache/pkg/sky_engine", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "source_span", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/source_span-1.10.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "stack_trace", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/stack_trace-1.12.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "stream_channel", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/stream_channel-2.1.4", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "string_scanner", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/string_scanner-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "term_glyph", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/term_glyph-1.2.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "test_api", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/test_api-0.7.7", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "vector_math", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/vector_math-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "vm_service", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/vm_service-15.0.2", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "web", + "rootUri": "file:///Users/guillaume/.pub-cache/hosted/pub.dev/web-1.1.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "crashlytics_flutter_example", + "rootUri": "../", + "packageUri": "lib/", + "languageVersion": "2.18" + } + ], + "generator": "pub", + "generatorVersion": "3.10.0", + "flutterRoot": "file:///Users/guillaume/fvm/versions/stable", + "flutterVersion": "3.38.1", + "pubCache": "file:///Users/guillaume/.pub-cache" +} diff --git a/scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/package_graph.json b/scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/package_graph.json new file mode 100644 index 00000000000..e7b069a22de --- /dev/null +++ b/scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/package_graph.json @@ -0,0 +1,317 @@ +{ + "roots": [ + "crashlytics_flutter_example" + ], + "packages": [ + { + "name": "crashlytics_flutter_example", + "version": "1.0.0+1", + "dependencies": [ + "cupertino_icons", + "firebase_core", + "firebase_crashlytics", + "flutter" + ], + "devDependencies": [ + "flutter_lints", + "flutter_test" + ] + }, + { + "name": "flutter_test", + "version": "0.0.0", + "dependencies": [ + "clock", + "collection", + "fake_async", + "flutter", + "leak_tracker_flutter_testing", + "matcher", + "meta", + "path", + "stack_trace", + "stream_channel", + "test_api", + "vector_math" + ] + }, + { + "name": "flutter", + "version": "0.0.0", + "dependencies": [ + "characters", + "collection", + "material_color_utilities", + "meta", + "sky_engine", + "vector_math" + ] + }, + { + "name": "vector_math", + "version": "2.2.0", + "dependencies": [] + }, + { + "name": "test_api", + "version": "0.7.7", + "dependencies": [ + "async", + "boolean_selector", + "collection", + "meta", + "source_span", + "stack_trace", + "stream_channel", + "string_scanner", + "term_glyph" + ] + }, + { + "name": "stream_channel", + "version": "2.1.4", + "dependencies": [ + "async" + ] + }, + { + "name": "stack_trace", + "version": "1.12.1", + "dependencies": [ + "path" + ] + }, + { + "name": "path", + "version": "1.9.1", + "dependencies": [] + }, + { + "name": "meta", + "version": "1.17.0", + "dependencies": [] + }, + { + "name": "matcher", + "version": "0.12.17", + "dependencies": [ + "async", + "meta", + "stack_trace", + "term_glyph", + "test_api" + ] + }, + { + "name": "leak_tracker_flutter_testing", + "version": "3.0.10", + "dependencies": [ + "flutter", + "leak_tracker", + "leak_tracker_testing", + "matcher", + "meta" + ] + }, + { + "name": "fake_async", + "version": "1.3.3", + "dependencies": [ + "clock", + "collection" + ] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "clock", + "version": "1.1.2", + "dependencies": [] + }, + { + "name": "sky_engine", + "version": "0.0.0", + "dependencies": [] + }, + { + "name": "material_color_utilities", + "version": "0.11.1", + "dependencies": [ + "collection" + ] + }, + { + "name": "characters", + "version": "1.4.0", + "dependencies": [] + }, + { + "name": "leak_tracker_testing", + "version": "3.0.2", + "dependencies": [ + "leak_tracker", + "matcher", + "meta" + ] + }, + { + "name": "leak_tracker", + "version": "11.0.2", + "dependencies": [ + "clock", + "collection", + "meta", + "path", + "vm_service" + ] + }, + { + "name": "term_glyph", + "version": "1.2.2", + "dependencies": [] + }, + { + "name": "boolean_selector", + "version": "2.1.2", + "dependencies": [ + "source_span", + "string_scanner" + ] + }, + { + "name": "flutter_lints", + "version": "2.0.3", + "dependencies": [ + "lints" + ] + }, + { + "name": "async", + "version": "2.13.0", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "lints", + "version": "2.1.1", + "dependencies": [] + }, + { + "name": "cupertino_icons", + "version": "1.0.8", + "dependencies": [] + }, + { + "name": "string_scanner", + "version": "1.4.1", + "dependencies": [ + "source_span" + ] + }, + { + "name": "source_span", + "version": "1.10.2", + "dependencies": [ + "collection", + "path", + "term_glyph" + ] + }, + { + "name": "vm_service", + "version": "15.0.2", + "dependencies": [] + }, + { + "name": "firebase_core", + "version": "2.32.0", + "dependencies": [ + "firebase_core_platform_interface", + "firebase_core_web", + "flutter", + "meta" + ] + }, + { + "name": "firebase_core_platform_interface", + "version": "5.4.2", + "dependencies": [ + "collection", + "flutter", + "flutter_test", + "meta", + "plugin_platform_interface" + ] + }, + { + "name": "plugin_platform_interface", + "version": "2.1.8", + "dependencies": [ + "meta" + ] + }, + { + "name": "flutter_web_plugins", + "version": "0.0.0", + "dependencies": [ + "flutter" + ] + }, + { + "name": "firebase_core_web", + "version": "2.24.0", + "dependencies": [ + "firebase_core_platform_interface", + "flutter", + "flutter_web_plugins", + "meta", + "web" + ] + }, + { + "name": "web", + "version": "1.1.1", + "dependencies": [] + }, + { + "name": "firebase_crashlytics", + "version": "3.5.7", + "dependencies": [ + "firebase_core", + "firebase_core_platform_interface", + "firebase_crashlytics_platform_interface", + "flutter", + "stack_trace" + ] + }, + { + "name": "firebase_crashlytics_platform_interface", + "version": "3.6.35", + "dependencies": [ + "_flutterfire_internals", + "collection", + "firebase_core", + "flutter", + "meta", + "plugin_platform_interface" + ] + }, + { + "name": "_flutterfire_internals", + "version": "1.3.35", + "dependencies": [ + "collection", + "firebase_core", + "firebase_core_platform_interface", + "flutter", + "meta" + ] + } + ], + "configVersion": 1 +} \ No newline at end of file diff --git a/scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/version b/scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/version new file mode 100644 index 00000000000..2f58982b313 --- /dev/null +++ b/scripts/agent-evals/templates/crashlytics-flutter/.dart_tool/version @@ -0,0 +1 @@ +3.38.1 \ No newline at end of file diff --git a/scripts/agent-evals/templates/crashlytics-flutter/.flutter-plugins-dependencies b/scripts/agent-evals/templates/crashlytics-flutter/.flutter-plugins-dependencies new file mode 100644 index 00000000000..7cf6acd078c --- /dev/null +++ b/scripts/agent-evals/templates/crashlytics-flutter/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"firebase_core","path":"/Users/guillaume/.pub-cache/hosted/pub.dev/firebase_core-2.32.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_crashlytics","path":"/Users/guillaume/.pub-cache/hosted/pub.dev/firebase_crashlytics-3.5.7/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false}],"android":[{"name":"firebase_core","path":"/Users/guillaume/.pub-cache/hosted/pub.dev/firebase_core-2.32.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_crashlytics","path":"/Users/guillaume/.pub-cache/hosted/pub.dev/firebase_crashlytics-3.5.7/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false}],"macos":[{"name":"firebase_core","path":"/Users/guillaume/.pub-cache/hosted/pub.dev/firebase_core-2.32.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_crashlytics","path":"/Users/guillaume/.pub-cache/hosted/pub.dev/firebase_crashlytics-3.5.7/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false}],"linux":[],"windows":[{"name":"firebase_core","path":"/Users/guillaume/.pub-cache/hosted/pub.dev/firebase_core-2.32.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"firebase_core_web","path":"/Users/guillaume/.pub-cache/hosted/pub.dev/firebase_core_web-2.24.0/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_crashlytics","dependencies":["firebase_core"]}],"date_created":"2026-02-24 15:14:25.074245","version":"3.38.1","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/scripts/agent-evals/templates/crashlytics-flutter/pubspec.lock b/scripts/agent-evals/templates/crashlytics-flutter/pubspec.lock new file mode 100644 index 00000000000..c330e521fb5 --- /dev/null +++ b/scripts/agent-evals/templates/crashlytics-flutter/pubspec.lock @@ -0,0 +1,282 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7" + url: "https://pub.dev" + source: hosted + version: "1.3.35" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c" + url: "https://pub.dev" + source: hosted + version: "2.32.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb" + url: "https://pub.dev" + source: hosted + version: "5.4.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3 + url: "https://pub.dev" + source: hosted + version: "2.24.0" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + sha256: "9897c01efaa950d2f6da8317d12452749a74dc45f33b46390a14cfe28067f271" + url: "https://pub.dev" + source: hosted + version: "3.5.7" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + sha256: "16a71e08fbf6e00382816e1b13397898c29a54fa0ad969c2c2a3b82a704877f0" + url: "https://pub.dev" + source: hosted + version: "3.6.35" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" +sdks: + dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.22.0" diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index 66af3bee80d..fbcc46c1c0b 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -244,7 +244,6 @@ export type Endpoint = Triggered & { platform?: "gcfv1" | "gcfv2" | "run"; // Necessary for the GCF API to determine what code to load with the Functions Framework. - // Will become optional once "run" is supported as a platform entryPoint: string; // The services account that this function should run as. diff --git a/src/deploy/functions/runtimes/dart.ts b/src/deploy/functions/runtimes/dart.ts deleted file mode 100644 index bcc90b637c0..00000000000 --- a/src/deploy/functions/runtimes/dart.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as fs from "fs-extra"; -import * as path from "path"; -import * as yaml from "js-yaml"; -import { DelegateContext, RuntimeDelegate } from "./index"; -import * as discovery from "./discovery"; - -// TODO: Temporary file for testing no build deploy. Remove this file after Invertase prepare phase is merged -/** - * Create a runtime delegate for the Dart runtime, if applicable. - * @param context runtimes.DelegateContext - * @return Delegate Dart runtime delegate - */ -export async function tryCreateDelegate( - context: DelegateContext, -): Promise { - const yamlPath = path.join(context.sourceDir, "functions.yaml"); - if (!(await fs.pathExists(yamlPath))) { - return undefined; - } - - // If runtime is specified, use it. Otherwise default to "dart3". - // "dart" is often used as a generic alias, map it to "dart3" - const runtime = context.runtime || "dart3"; - - return { - language: "dart", - runtime: runtime, - bin: "", // No bin needed for no-build - validate: async () => { - // Basic validation that the file is parseable - try { - const content = await fs.readFile(yamlPath, "utf8"); - yaml.load(content); - } catch (e: any) { - throw new Error(`Failed to parse functions.yaml: ${e.message}`); - } - }, - build: async () => { - // No-op for no-build - return Promise.resolve(); - }, - watch: async () => { - return Promise.resolve(async () => { - // No-op - }); - }, - discoverBuild: async () => { - const build = await discovery.detectFromYaml(context.sourceDir, context.projectId, runtime); - if (!build) { - // This should not happen because we checked for existence in tryCreateDelegate - throw new Error("Could not find functions.yaml"); - } - return build; - }, - }; -} diff --git a/src/deploy/functions/runtimes/dart/index.ts b/src/deploy/functions/runtimes/dart/index.ts new file mode 100644 index 00000000000..a53580ff49e --- /dev/null +++ b/src/deploy/functions/runtimes/dart/index.ts @@ -0,0 +1,316 @@ +import * as fs from "fs"; +import * as path from "path"; +import { promisify } from "util"; +import * as spawn from "cross-spawn"; +import { ChildProcess } from "child_process"; + +import * as runtimes from ".."; +import * as backend from "../../backend"; +import * as discovery from "../discovery"; +import * as supported from "../supported"; +import { logger } from "../../../../logger"; +import { FirebaseError } from "../../../../error"; +import { logLabeledBullet } from "../../../../utils"; +import { Build } from "../../build"; +import { EmulatorRegistry } from "../../../../emulator/registry"; +import { Emulators } from "../../../../emulator/types"; + +/** + * Create a runtime delegate for the Dart runtime, if applicable. + * @param context runtimes.DelegateContext + * @return Delegate Dart runtime delegate + */ +export async function tryCreateDelegate( + context: runtimes.DelegateContext, +): Promise { + const pubspecYamlPath = path.join(context.sourceDir, "pubspec.yaml"); + + if (!(await promisify(fs.exists)(pubspecYamlPath))) { + logger.debug("Customer code is not Dart code."); + return; + } + const runtime = context.runtime ?? supported.latest("dart"); + if (!supported.isRuntime(runtime)) { + throw new FirebaseError(`Runtime ${runtime as string} is not a valid Dart runtime`); + } + if (!supported.runtimeIsLanguage(runtime, "dart")) { + throw new FirebaseError( + `Internal error. Trying to construct a dart runtime delegate for runtime ${runtime}`, + { exit: 1 }, + ); + } + return Promise.resolve(new Delegate(context.projectId, context.sourceDir, runtime)); +} + +export class Delegate implements runtimes.RuntimeDelegate { + public readonly language = "dart"; + public readonly bin = "dart"; + + private static watchModeActive = false; + private buildRunnerProcess: ChildProcess | null = null; + + constructor( + private readonly projectId: string, + private readonly sourceDir: string, + public readonly runtime: supported.Runtime & supported.RuntimeOf<"dart">, + ) {} + + async validate(): Promise { + const pubspecYamlPath = path.join(this.sourceDir, "pubspec.yaml"); + try { + await fs.promises.access(pubspecYamlPath, fs.constants.R_OK); + } catch (err: any) { + throw new FirebaseError(`Failed to read pubspec.yaml at ${pubspecYamlPath}: ${err.message}`); + } + } + + async build(): Promise { + // If build_runner watch is already running (on any delegate instance), + // it handles rebuilds automatically. Skip to avoid infinite reload loops. + if (Delegate.watchModeActive) { + return; + } + + // Run build_runner to generate up-to-date functions.yaml + logLabeledBullet("functions", "running build_runner..."); + + const buildRunnerProcess = spawn( + this.bin, + ["run", "build_runner", "build", "--delete-conflicting-outputs"], + { + cwd: this.sourceDir, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + + buildRunnerProcess.stdout?.on("data", (chunk: Buffer) => { + logger.debug(`[build_runner] ${chunk.toString("utf8").trim()}`); + }); + buildRunnerProcess.stderr?.on("data", (chunk: Buffer) => { + logger.debug(`[build_runner] ${chunk.toString("utf8").trim()}`); + }); + + await new Promise((resolve, reject) => { + buildRunnerProcess.on("exit", (code) => { + if (code === 0 || code === null) { + resolve(); + } else { + reject( + new FirebaseError( + `build_runner failed with exit code ${code}. ` + + `Make sure your Dart project is properly configured.`, + ), + ); + } + }); + buildRunnerProcess.on("error", reject); + }); + + // Cross-compile Dart to a Linux x86_64 executable for Cloud Run. + // Skip compilation when running in the emulator (the emulator runs + // Dart source directly via `dart run`). + if (EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { + logger.debug("Skipping Dart compilation in emulator mode."); + return; + } + + // Requires Dart 3.8+ for --target-os and --target-arch support. + const binDir = path.join(this.sourceDir, "bin"); + await fs.promises.mkdir(binDir, { recursive: true }); + + logLabeledBullet("functions", "compiling Dart to linux-x64 executable..."); + + const compileProcess = spawn( + this.bin, + [ + "compile", + "exe", + "lib/main.dart", + "-o", + "bin/server", + "--target-os=linux", + "--target-arch=x64", + ], + { + cwd: this.sourceDir, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + + compileProcess.stdout?.on("data", (chunk: Buffer) => { + logger.debug(`[dart compile] ${chunk.toString("utf8").trim()}`); + }); + compileProcess.stderr?.on("data", (chunk: Buffer) => { + logger.debug(`[dart compile] ${chunk.toString("utf8").trim()}`); + }); + + await new Promise((resolve, reject) => { + compileProcess.on("exit", (code) => { + if (code === 0 || code === null) { + resolve(); + } else { + reject( + new FirebaseError( + `Dart compilation failed with exit code ${code}. ` + + `Make sure your Dart project compiles successfully with: ` + + `dart compile exe lib/main.dart --target-os=linux --target-arch=x64`, + ), + ); + } + }); + compileProcess.on("error", reject); + }); + + logLabeledBullet("functions", "Dart compilation complete."); + } + + /** + * Start build_runner in watch mode for hot reload. + * 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> { + Delegate.watchModeActive = true; + logger.debug("Starting build_runner watch for Dart functions..."); + + const buildRunnerProcess = spawn( + this.bin, + ["run", "build_runner", "watch", "--delete-conflicting-outputs"], + { + cwd: this.sourceDir, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + this.buildRunnerProcess = buildRunnerProcess; + + let initialBuildComplete = false; + let resolveInitialBuild: () => void; + let rejectInitialBuild: (err: Error) => void; + + const initialBuildPromise = new Promise((resolve, reject) => { + resolveInitialBuild = resolve; + rejectInitialBuild = reject; + }); + + const buildCompletePattern = /Succeeded after|Built with build_runner/; + + buildRunnerProcess.stdout?.on("data", (chunk: Buffer) => { + const output = chunk.toString("utf8").trim(); + if (output) { + logger.debug(`[build_runner] ${output}`); + if (buildCompletePattern.test(output)) { + if (!initialBuildComplete) { + initialBuildComplete = true; + logger.debug("build_runner initial build completed"); + resolveInitialBuild(); + } else if (onRebuild) { + // Subsequent rebuild detected — notify the emulator to reload triggers + onRebuild(); + } + } + } + }); + + buildRunnerProcess.stderr?.on("data", (chunk: Buffer) => { + const output = chunk.toString("utf8").trim(); + if (output) { + logger.debug(`[build_runner] ${output}`); + } + }); + + buildRunnerProcess.on("exit", (code) => { + if (code !== 0 && code !== null) { + logger.debug(`build_runner exited with code ${code}. Initial build failed.`); + if (!initialBuildComplete) { + rejectInitialBuild( + new FirebaseError( + `build_runner exited with code ${code}. Your Dart functions may not be deployed or emulated correctly.`, + ), + ); + } + } + this.buildRunnerProcess = null; + }); + + buildRunnerProcess.on("error", (err) => { + logger.debug( + `Failed to start build_runner: ${err.message}. Your Dart functions may not be deployed or emulated correctly.`, + ); + if (!initialBuildComplete) { + rejectInitialBuild(err); + } + }); + + await initialBuildPromise; + + // Return cleanup function + return async () => { + if (this.buildRunnerProcess && !this.buildRunnerProcess.killed) { + this.buildRunnerProcess.kill("SIGTERM"); + this.buildRunnerProcess = null; + } + }; + } + + async discoverBuild( + _configValues: backend.RuntimeConfigValues, // eslint-disable-line @typescript-eslint/no-unused-vars + envs: backend.EnvironmentVariables, + ): Promise { + const yamlDir = this.sourceDir; + const yamlPath = path.join(yamlDir, "functions.yaml"); + let discovered = await discovery.detectFromYaml(yamlDir, this.projectId, this.runtime); + + if (!discovered) { + logger.debug("functions.yaml not found, running build_runner to generate it..."); + const buildRunnerProcess = spawn(this.bin, ["run", "build_runner", "build"], { + cwd: this.sourceDir, + stdio: ["ignore", "pipe", "pipe"], + }); + + buildRunnerProcess.stdout?.on("data", (chunk: Buffer) => { + logger.debug(`[build_runner] ${chunk.toString("utf8")}`); + }); + buildRunnerProcess.stderr?.on("data", (chunk: Buffer) => { + logger.debug(`[build_runner] ${chunk.toString("utf8")}`); + }); + + await new Promise((resolve, reject) => { + buildRunnerProcess.on("exit", (code) => { + if (code === 0 || code === null) { + resolve(); + } else { + reject( + new FirebaseError( + `build_runner failed with exit code ${code}. Make sure your Dart project is properly configured.`, + ), + ); + } + }); + buildRunnerProcess.on("error", reject); + }); + + discovered = await discovery.detectFromYaml(yamlDir, this.projectId, this.runtime); + if (!discovered) { + throw new FirebaseError( + `Could not find functions.yaml at ${yamlPath} after running build_runner. ` + + `Make sure your Dart project is properly configured with firebase_functions.`, + ); + } + } + + // The Dart manifest emits platform "gcfv2" so the emulator treats + // functions as v2 CloudEvent endpoints (getSignatureType needs "gcfv2"). + // During deploy, convert to "run" so fabricator.ts creates Cloud Run services. + // The emulator passes FUNCTIONS_EMULATOR=true in envs; deploy does not. + const isEmulator = envs.FUNCTIONS_EMULATOR === "true"; + if (!isEmulator) { + for (const ep of Object.values(discovered.endpoints)) { + if (ep.platform === "gcfv2") { + (ep as any).platform = "run"; + } + } + } + + return discovered; + } +} diff --git a/src/deploy/functions/runtimes/index.ts b/src/deploy/functions/runtimes/index.ts index 9ad33973183..7c055403eb7 100644 --- a/src/deploy/functions/runtimes/index.ts +++ b/src/deploy/functions/runtimes/index.ts @@ -1,11 +1,11 @@ import * as backend from "../backend"; import * as build from "../build"; +import * as dart from "./dart"; import * as node from "./node"; import * as python from "./python"; import * as validate from "../validate"; import { FirebaseError } from "../../../error"; import * as supported from "./supported"; -import * as dart from "./dart"; import * as experiments from "../../../experiments"; /** @@ -47,7 +47,7 @@ export interface RuntimeDelegate { * This is for languages like TypeScript which have a "watch" feature. * Returns a cancel function. */ - watch(): Promise<() => Promise>; + watch(onRebuild?: () => void): Promise<() => Promise>; /** * Inspect the customer's source for the backend spec it describes. diff --git a/src/deploy/functions/runtimes/supported/index.ts b/src/deploy/functions/runtimes/supported/index.ts index 683949a23c0..21bef65beba 100644 --- a/src/deploy/functions/runtimes/supported/index.ts +++ b/src/deploy/functions/runtimes/supported/index.ts @@ -17,6 +17,11 @@ export function runtimeIsLanguage( return runtime.startsWith(language); } +/** Check if an optional runtime string belongs to a given language. */ +export function isLanguageRuntime(runtime: string | undefined, language: Language): boolean { + return !!runtime && runtime.startsWith(language); +} + /** * Find the latest supported Runtime for a Language. */ diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index be5d5288f27..29b2681e283 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -60,7 +60,7 @@ import { BlockingFunctionsConfig } from "../gcp/identityPlatform"; import { resolveBackend } from "../deploy/functions/build"; import { getCredentialsEnvironment, setEnvVarsForEmulators } from "./env"; import { runWithVirtualEnv } from "../functions/python"; -import { Runtime } from "../deploy/functions/runtimes/supported"; +import { isLanguageRuntime, Runtime } from "../deploy/functions/runtimes/supported"; import { ExtensionsEmulator } from "./extensionsEmulator"; const EVENT_INVOKE_GA4 = "functions_invoke"; // event name GA4 (alphanumertic) @@ -222,6 +222,7 @@ export class FunctionsEmulator implements EmulatorInstance { private staticBackends: EmulatableBackend[] = []; private dynamicBackends: EmulatableBackend[] = []; private watchers: chokidar.FSWatcher[] = []; + private watchCleanups: Array<() => Promise> = []; debugMode = false; @@ -399,7 +400,7 @@ export class FunctionsEmulator implements EmulatorInstance { async sendRequest(trigger: EmulatedTriggerDefinition, body?: any) { const record = this.getTriggerRecordByKey(this.getTriggerKey(trigger)); const pool = this.workerPools[record.backend.codebase]; - if (!pool.readyForWork(trigger.id)) { + if (!pool.readyForWork(trigger.id, record.backend.runtime)) { try { await this.startRuntime(record.backend, trigger); } catch (e: any) { @@ -407,7 +408,7 @@ export class FunctionsEmulator implements EmulatorInstance { return; } } - const worker = pool.getIdleWorker(trigger.id)!; + const worker = pool.getIdleWorker(trigger.id, record.backend.runtime)!; if (this.debugMode) { await worker.sendDebugMsg({ functionTarget: trigger.entryPoint, @@ -419,11 +420,18 @@ export class FunctionsEmulator implements EmulatorInstance { "Content-Type": "application/json", "Content-Length": `${reqBody.length}`, }; + + // 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"); + const path = isDart ? `/${trigger.entryPoint}` : `/`; + return new Promise((resolve, reject) => { const req = http.request( { ...worker.runtime.conn.httpReqOpts(), - path: `/`, + method: "POST", + path: path, headers: headers, }, resolve, @@ -473,26 +481,55 @@ export class FunctionsEmulator implements EmulatorInstance { `Watching "${backend.functionsDir}" for Cloud Functions...`, ); - const watcher = chokidar.watch(backend.functionsDir, { - ignored: [ - /.+?[\\\/]node_modules[\\\/].+?/, // Ignore node_modules - /(^|[\/\\])\../, // Ignore files which begin the a period - /.+\.log/, // Ignore files which have a .log extension - /.+?[\\\/]venv[\\\/].+?/, // Ignore site-packages in venv - ...(backend.ignore?.map((i) => `**/${i}`) ?? []), - ], - persistent: true, - }); - - this.watchers.push(watcher); - - const debouncedLoadTriggers = debounce(() => this.loadTriggers(backend), 1000); - watcher.on("change", (filePath) => { - this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`); - return debouncedLoadTriggers(); - }); - + // First load triggers to discover the runtime type await this.loadTriggers(backend, /* force= */ true); + + const isDart = isLanguageRuntime(backend.runtime, "dart"); + + if (isDart) { + // For Dart, build_runner watch handles source file watching and rebuilds + // functions.yaml automatically. We use its onRebuild callback to reload + // triggers, avoiding chokidar entirely (which would cause infinite loops + // since loadTriggers runs build_runner build which rewrites functions.yaml). + const runtimeDelegateContext: runtimes.DelegateContext = { + projectId: this.args.projectId, + projectDir: this.args.projectDir, + sourceDir: backend.functionsDir, + runtime: backend.runtime, + }; + const delegate = await runtimes.getRuntimeDelegate(runtimeDelegateContext); + this.logger.logLabeled( + "BULLET", + "functions", + `Starting build_runner watch for Dart functions...`, + ); + const debouncedLoadTriggers = debounce(() => this.loadTriggers(backend), 1000); + const cleanup = await delegate.watch(() => { + this.logger.log("DEBUG", "build_runner rebuilt, reloading triggers"); + debouncedLoadTriggers(); + }); + this.watchCleanups.push(cleanup); + this.logger.logLabeled("SUCCESS", "functions", `build_runner initial build completed`); + } else { + const watcher = chokidar.watch(backend.functionsDir, { + ignored: [ + /(^|[\/\\])\../, // Ignore hidden files/dirs (covers .dart_tool, .git, etc.) + /.+\.log/, // Ignore log files + /.+?[\\\/]node_modules[\\\/].+?/, // Ignore node_modules + /.+?[\\\/]venv[\\\/].+?/, // Ignore venv + ...(backend.ignore?.map((i) => `**/${i}`) ?? []), + ], + persistent: true, + }); + + this.watchers.push(watcher); + + const debouncedLoadTriggers = debounce(() => this.loadTriggers(backend), 1000); + watcher.on("change", (filePath) => { + this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`); + return debouncedLoadTriggers(); + }); + } } await this.performPostLoadOperations(); return; @@ -519,6 +556,12 @@ export class FunctionsEmulator implements EmulatorInstance { } this.watchers = []; + // Stop delegate watch processes (e.g., build_runner for Dart) + for (const cleanup of this.watchCleanups) { + await cleanup(); + } + this.watchCleanups = []; + if (this.destroyServer) { await this.destroyServer(); } @@ -1668,6 +1711,57 @@ export class FunctionsEmulator implements EmulatorInstance { }; } + async startDart( + backend: EmulatableBackend, + envs: Record, + ): Promise { + if (this.debugMode) { + this.logger.log("WARN", "--inspect-functions not supported for Dart functions. Ignored."); + } + + // Use TCP/IP stack for Dart, similar to Python + const port = await portfinder.getPortPromise({ + port: 8081 + randomInt(0, 1000), // Add a small jitter to avoid race condition. + }); + + const args = ["run", "--no-serve-devtools", "lib/main.dart"]; + + // For Dart, don't set FUNCTION_TARGET in environment - the server loads all functions + // and routes based on the request path (similar to Python's functions-framework) + const dartEnvs = { ...envs }; + delete dartEnvs.FUNCTION_TARGET; + delete dartEnvs.FUNCTION_SIGNATURE_TYPE; + + const bin = backend.bin || "dart"; + logger.debug(`Starting Dart runtime with args: ${args.join(" ")} on port ${port}`); + const childProcess = spawn(bin, args, { + cwd: backend.functionsDir, + env: { + ...process.env, + ...dartEnvs, + HOST: "127.0.0.1", + PORT: port.toString(), + }, + stdio: ["pipe", "pipe", "pipe"], + }); + + // Log stdout and stderr for debugging + childProcess.stdout?.on("data", (chunk: Buffer) => { + this.logger.log("DEBUG", `[dart] ${chunk.toString("utf8")}`); + }); + + childProcess.stderr?.on("data", (chunk: Buffer) => { + this.logger.log("DEBUG", `[dart] ${chunk.toString("utf8")}`); + }); + + return { + process: childProcess, + events: new EventEmitter(), + cwd: backend.functionsDir, + conn: new TCPConn("127.0.0.1", port), + }; + } + async startRuntime( backend: EmulatableBackend, trigger?: EmulatedTriggerDefinition, @@ -1676,8 +1770,10 @@ export class FunctionsEmulator implements EmulatorInstance { const secretEnvs = await this.resolveSecretEnvs(backend, trigger); let runtime; - if (backend.runtime!.startsWith("python")) { + if (isLanguageRuntime(backend.runtime, "python")) { runtime = await this.startPython(backend, { ...runtimeEnv, ...secretEnvs }); + } else if (isLanguageRuntime(backend.runtime, "dart")) { + runtime = await this.startDart(backend, { ...runtimeEnv, ...secretEnvs }); } else { runtime = await this.startNode(backend, { ...runtimeEnv, ...secretEnvs }); } @@ -1687,7 +1783,7 @@ export class FunctionsEmulator implements EmulatorInstance { }; const pool = this.workerPools[backend.codebase]; - const worker = pool.addWorker(trigger, runtime, extensionLogInfo); + const worker = pool.addWorker(trigger, runtime, extensionLogInfo, backend.runtime); await worker.waitForSocketReady(); return worker; } @@ -1777,18 +1873,37 @@ export class FunctionsEmulator implements EmulatorInstance { // To match production behavior we need to drop the path prefix // req.url = /:projectId/:region/:trigger_name/* const url = new URL(`${req.protocol}://${req.hostname}${req.url}`); - const path = `${url.pathname}${url.search}`.replace( + let path = `${url.pathname}${url.search}`.replace( new RegExp(`\/${this.args.projectId}\/[^\/]*\/${req.params.trigger_name}\/?`), "/", ); + // For Dart, route via path since all functions share a single process. + // The Dart server routes based on the first path segment (function name). + // Use trigger.entryPoint (e.g. "helloworld") which is the actual function name + // registered in the Dart server, not trigger_name which may include region prefix + // (e.g. "us-central1-helloworld-0" from background function routes). + const isDart = isLanguageRuntime(record.backend.runtime, "dart"); + if (isDart) { + // Background trigger routes (e.g., /functions/projects/.../triggers/...) + // leave a path artifact (/functions/projects/) after regex replacement. + // Only append remaining path for HTTP trigger routes where the user may + // have sub-paths (e.g., /helloworld/extra/path). + const isBackgroundRoute = req.url.startsWith("/functions/projects/"); + if (isBackgroundRoute || path === "/") { + path = `/${trigger.entryPoint}`; + } else { + path = `/${trigger.entryPoint}${path}`; + } + } + // We do this instead of just 302'ing because many HTTP clients don't respect 302s so it may // cause unexpected situations - not to mention CORS troubles and this enables us to use // a socketPath (IPC socket) instead of consuming yet another port which is probably faster as well. this.logger.log("DEBUG", `[functions] Got req.url=${req.url}, mapping to path=${path}`); const pool = this.workerPools[record.backend.codebase]; - if (!pool.readyForWork(trigger.id)) { + if (!pool.readyForWork(trigger.id, record.backend.runtime)) { try { await this.startRuntime(record.backend, trigger); } catch (e: any) { @@ -1817,6 +1932,7 @@ export class FunctionsEmulator implements EmulatorInstance { res as http.ServerResponse, reqBody, debugBundle, + record.backend.runtime, ); } } diff --git a/src/emulator/functionsRuntimeWorker.ts b/src/emulator/functionsRuntimeWorker.ts index 5c32326f296..0593dc5a2a0 100644 --- a/src/emulator/functionsRuntimeWorker.ts +++ b/src/emulator/functionsRuntimeWorker.ts @@ -9,6 +9,7 @@ import { EmulatorLogger, ExtensionLogInfo } from "./emulatorLogger"; import { FirebaseError } from "../error"; import { Serializable } from "child_process"; import { getFunctionDiscoveryTimeout } from "../deploy/functions/runtimes/discovery"; +import { isLanguageRuntime } from "../deploy/functions/runtimes/supported"; type LogListener = (el: EmulatorLog) => any; @@ -296,12 +297,16 @@ export class RuntimeWorkerPool { constructor(private mode: FunctionsExecutionMode = FunctionsExecutionMode.AUTO) {} - getKey(triggerId: string | undefined): string { + getKey(triggerId: string | undefined, runtime?: string): string { if (this.mode === FunctionsExecutionMode.SEQUENTIAL) { return "~shared~"; - } else { - return triggerId || "~diagnostic~"; } + // For Dart, use a shared key so all functions in a codebase share the same worker process. + // Dart loads all functions into a single process and routes based on request path. + if (isLanguageRuntime(runtime, "dart")) { + return "~dart-shared~"; + } + return triggerId || "~diagnostic~"; } /** @@ -345,8 +350,8 @@ export class RuntimeWorkerPool { * * @param triggerId */ - readyForWork(triggerId: string | undefined): boolean { - const idleWorker = this.getIdleWorker(triggerId); + readyForWork(triggerId: string | undefined, runtime?: string): boolean { + const idleWorker = this.getIdleWorker(triggerId, runtime); return !!idleWorker; } @@ -366,9 +371,10 @@ export class RuntimeWorkerPool { resp: http.ServerResponse, body: unknown, debug?: FunctionsRuntimeBundle["debug"], + runtime?: string, ): Promise { this.log(`submitRequest(triggerId=${triggerId})`); - const worker = this.getIdleWorker(triggerId); + const worker = this.getIdleWorker(triggerId, runtime); if (!worker) { throw new FirebaseError( "Internal Error: can't call submitRequest without checking for idle workers", @@ -380,11 +386,11 @@ export class RuntimeWorkerPool { return worker.request(req, resp, body, !!debug); } - getIdleWorker(triggerId: string | undefined): RuntimeWorker | undefined { + getIdleWorker(triggerId: string | undefined, runtime?: string): RuntimeWorker | undefined { this.cleanUpWorkers(); - const triggerWorkers = this.getTriggerWorkers(triggerId); + const triggerWorkers = this.getTriggerWorkers(triggerId, runtime); if (!triggerWorkers.length) { - this.setTriggerWorkers(triggerId, []); + this.setTriggerWorkers(triggerId, [], runtime); return; } @@ -406,8 +412,10 @@ export class RuntimeWorkerPool { trigger: EmulatedTriggerDefinition | undefined, runtime: FunctionsRuntimeInstance, extensionLogInfo: ExtensionLogInfo, + runtimeType?: string, ): RuntimeWorker { - this.log(`addWorker(${this.getKey(trigger?.id)})`); + const key = this.getKey(trigger?.id, runtimeType); + this.log(`addWorker(${key})`); // Disable worker timeout if: // (1) This is a diagnostic call without trigger id OR // (2) If in SEQUENTIAL execution mode @@ -419,20 +427,24 @@ export class RuntimeWorkerPool { disableTimeout ? undefined : trigger?.timeoutSeconds, ); - const keyWorkers = this.getTriggerWorkers(trigger?.id); + const keyWorkers = this.getTriggerWorkers(trigger?.id, runtimeType); keyWorkers.push(worker); - this.setTriggerWorkers(trigger?.id, keyWorkers); + this.setTriggerWorkers(trigger?.id, keyWorkers, runtimeType); this.log(`Adding worker with key ${worker.triggerKey}, total=${keyWorkers.length}`); return worker; } - getTriggerWorkers(triggerId: string | undefined): Array { - return this.workers.get(this.getKey(triggerId)) || []; + getTriggerWorkers(triggerId: string | undefined, runtime?: string): Array { + return this.workers.get(this.getKey(triggerId, runtime)) || []; } - private setTriggerWorkers(triggerId: string | undefined, workers: Array) { - this.workers.set(this.getKey(triggerId), workers); + private setTriggerWorkers( + triggerId: string | undefined, + workers: Array, + runtime?: string, + ) { + this.workers.set(this.getKey(triggerId, runtime), workers); } private cleanUpWorkers() { diff --git a/src/init/features/functions/dart.ts b/src/init/features/functions/dart.ts new file mode 100644 index 00000000000..d8f232372ee --- /dev/null +++ b/src/init/features/functions/dart.ts @@ -0,0 +1,38 @@ +import * as spawn from "cross-spawn"; +import { Config } from "../../../config"; +import { confirm } from "../../../prompt"; +import { latest } from "../../../deploy/functions/runtimes/supported"; +import { readTemplateSync } from "../../../templates"; + +const PUBSPEC_TEMPLATE = readTemplateSync("init/functions/dart/pubspec.yaml"); +const MAIN_TEMPLATE = readTemplateSync("init/functions/dart/main.dart"); +const GITIGNORE_TEMPLATE = readTemplateSync("init/functions/dart/_gitignore"); + +/** + * Create a Dart Firebase Functions project. + */ +export async function setup(setup: any, config: Config): Promise { + await config.askWriteProjectFile(`${setup.functions.source}/pubspec.yaml`, PUBSPEC_TEMPLATE); + await config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE); + await config.askWriteProjectFile(`${setup.functions.source}/lib/main.dart`, MAIN_TEMPLATE); + + // Write the latest supported runtime version to the config. + config.set("functions.runtime", latest("dart")); + // Add dart specific ignores to config. + config.set("functions.ignore", [".dart_tool", "build"]); + + const install = await confirm({ + message: "Do you want to install dependencies now?", + default: true, + }); + if (install) { + const installProcess = spawn("dart", ["pub", "get"], { + cwd: config.path(setup.functions.source), + stdio: ["inherit", "inherit", "inherit"], + }); + await new Promise((resolve, reject) => { + installProcess.on("exit", resolve); + installProcess.on("error", reject); + }); + } +} diff --git a/src/init/features/functions/index.ts b/src/init/features/functions/index.ts index 4641045387e..af2370f2ee9 100644 --- a/src/init/features/functions/index.ts +++ b/src/init/features/functions/index.ts @@ -177,6 +177,10 @@ async function languageSetup(setup: any): Promise { value: "python", }); } + choices.push({ + name: "Dart", + value: "dart", + }); const language = await select({ message: "What language would you like to use to write Cloud Functions?", default: "javascript", @@ -208,6 +212,18 @@ async function languageSetup(setup: any): Promise { // but in theory this doesn't have to be the case. cbconfig.runtime = supported.latest("python") as supported.ActiveRuntime; break; + case "dart": + cbconfig.ignore = [ + ".dart_tool", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local", + ]; + // In practical sense, latest supported runtime will not be a decomissioned runtime, + // but in theory this doesn't have to be the case. + cbconfig.runtime = supported.latest("dart") as supported.ActiveRuntime; + break; } setup.functions.languageChoice = language; } diff --git a/templates/init/functions/dart/_gitignore b/templates/init/functions/dart/_gitignore new file mode 100644 index 00000000000..fb25e1562cd --- /dev/null +++ b/templates/init/functions/dart/_gitignore @@ -0,0 +1,11 @@ +.dart_tool/ +build/ +*.dart.js +*.info.json +*.js +*.js.map +*.js.deps +*.js.symbols +firebase-debug.log +firebase-debug.*.log +*.local diff --git a/templates/init/functions/dart/main.dart b/templates/init/functions/dart/main.dart new file mode 100644 index 00000000000..6179e7066ae --- /dev/null +++ b/templates/init/functions/dart/main.dart @@ -0,0 +1,13 @@ +import 'package:firebase_functions/firebase_functions.dart'; + +void main(List args) { + fireUp(args, (firebase) { + + firebase.https.onRequest( + name: 'helloWorld', + options: const HttpsOptions(cors: Cors(['*'])), + (request) async { + return Response(200, body: 'Hello from Dart Functions!'); + }); + }); +} diff --git a/templates/init/functions/dart/pubspec.yaml b/templates/init/functions/dart/pubspec.yaml new file mode 100644 index 00000000000..75085077f8a --- /dev/null +++ b/templates/init/functions/dart/pubspec.yaml @@ -0,0 +1,14 @@ +name: functions +description: Firebase Functions for Dart +version: 1.0.0 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + # TODO(ehesp): Replace with published package version once available on pub.dev + firebase_functions: + path: ../ + +dev_dependencies: + build_runner: ^2.4.0