Adds a local stac dev workflow so Stac screens and themes can be tested without deploying to Stac Cloud.#479
Adds a local stac dev workflow so Stac screens and themes can be tested without deploying to Stac Cloud.#479divyanshub024 wants to merge 2 commits into
stac dev workflow so Stac screens and themes can be tested without deploying to Stac Cloud.#479Conversation
📝 WalkthroughWalkthroughThis PR adds local development server support via the new ChangesLocal Development Server Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
examples/movie_app/lib/main.dart (1)
22-22: ⚡ Quick winScope the localhost override to debug builds.
This override is applied unconditionally, so release/profile builds will also point at
http://127.0.0.1:45700and fail to reach Stac Cloud. The intent (per the PR) is local debugging only, so gate it behindkDebugMode.♻️ Proposed change
+import 'package:flutter/foundation.dart';await Stac.initialize( - options: defaultStacOptions.copyWith(apiBaseUrl: 'http://127.0.0.1:45700'), + options: kDebugMode + ? defaultStacOptions.copyWith(apiBaseUrl: 'http://127.0.0.1:45700') + : defaultStacOptions, dio: dio, parsers: [MovieCarouselParser()], );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/movie_app/lib/main.dart` at line 22, The localhost apiBaseUrl override is applied unconditionally on defaultStacOptions; restrict it to debug builds by using kDebugMode so release/profile use the default endpoint. Modify the code that sets options (the call to defaultStacOptions.copyWith(apiBaseUrl: 'http://127.0.0.1:45700')) to only apply copyWith when kDebugMode is true (importing flutter/foundation.dart if needed), otherwise leave options as defaultStacOptions (or copyWith without the override).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/movie_app/stac/app_theme.dart`:
- Around line 8-9: The current color pair primary: '`#212121`' and onPrimary:
'`#050608`' fails WCAG contrast; update the onPrimary value (or adjust primary) so
the contrast ratio is >= 4.5:1 for normal text (or >= 3:1 for large text).
Locate the primary and onPrimary declarations in app_theme.dart and replace
onPrimary with a much lighter color (e.g., '`#FFFFFF`' or a light gray like
'`#F5F5F5`') or compute a contrast-safe foreground dynamically; ensure the chosen
onPrimary yields at least 4.5:1 contrast against '`#212121`' before committing.
In `@packages/stac_cli/lib/src/services/dev_service.dart`:
- Around line 122-181: The handler dispatched via unawaited(server.listen(...))
can throw after CORS headers are set and leave the HttpResponse open; update
_handleRequest to wrap its entire body in a try/catch/finally (or use try { ...
} on success paths } catch (e, st) { await _writeJson(response,
HttpStatus.internalServerError, {'error': 'Internal server error'});
ConsoleLogger.error('...', e, st); } finally { if (!response.headersSent &&
response.contentLength == 0) { /* ensure close even if nothing written */ }
await response.close(); }) so that any exception (including those from
_serveArtifact, stat(), or file.readAsString()) is caught and the response is
always closed; use _writeJson for a 500 where feasible and reference
_handleRequest, _serveArtifact, _writeJson, and _setCorsHeaders when making the
change.
---
Nitpick comments:
In `@examples/movie_app/lib/main.dart`:
- Line 22: The localhost apiBaseUrl override is applied unconditionally on
defaultStacOptions; restrict it to debug builds by using kDebugMode so
release/profile use the default endpoint. Modify the code that sets options (the
call to defaultStacOptions.copyWith(apiBaseUrl: 'http://127.0.0.1:45700')) to
only apply copyWith when kDebugMode is true (importing flutter/foundation.dart
if needed), otherwise leave options as defaultStacOptions (or copyWith without
the override).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 61130290-b666-4140-862f-65646a299e8d
📒 Files selected for processing (18)
docs/cli.mdxdocs/quickstart.mdxexamples/movie_app/ios/Flutter/AppFrameworkInfo.plistexamples/movie_app/ios/Runner/AppDelegate.swiftexamples/movie_app/ios/Runner/Info.plistexamples/movie_app/lib/main.dartexamples/movie_app/stac/app_theme.dartpackages/stac/lib/src/services/stac_cloud.dartpackages/stac_cli/README.mdpackages/stac_cli/bin/stac_cli.dartpackages/stac_cli/lib/src/commands/deploy_command.dartpackages/stac_cli/lib/src/commands/dev_command.dartpackages/stac_cli/lib/src/commands/init_command.dartpackages/stac_cli/lib/src/commands/project/create_command.dartpackages/stac_cli/lib/src/commands/project/list_command.dartpackages/stac_cli/lib/src/services/build_service.dartpackages/stac_cli/lib/src/services/dev_service.dartpackages/stac_core/lib/core/stac_options.dart
💤 Files with no reviewable changes (1)
- examples/movie_app/ios/Flutter/AppFrameworkInfo.plist
| primary: '#212121', | ||
| onPrimary: '#050608', |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python3 - <<'PY'
def lum(hex_):
h=hex_.lstrip('#'); r,g,b=(int(h[i:i+2],16)/255 for i in (0,2,4))
f=lambda c:(c/12.92) if c<=0.03928 else ((c+0.055)/1.055)**2.4
R,G,B=f(r),f(g),f(b)
return 0.2126*R+0.7152*G+0.0722*B
def ratio(a,b):
la,lb=lum(a),lum(b); hi,lo=max(la,lb),min(la,lb)
return (hi+0.05)/(lo+0.05)
print("primary/onPrimary:", round(ratio('`#212121`','`#050608`'),2))
PYRepository: StacDev/stac
Length of output: 80
Fix onPrimary contrast against primary
In examples/movie_app/stac/app_theme.dart (primary #212121, onPrimary #050608), the WCAG contrast ratio is ~1.26:1, far below AA (4.5:1 normal / 3:1 large). Update onPrimary to a significantly lighter content color (or adjust primary) so text/icons on primary surfaces remain readable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/movie_app/stac/app_theme.dart` around lines 8 - 9, The current color
pair primary: '`#212121`' and onPrimary: '`#050608`' fails WCAG contrast; update the
onPrimary value (or adjust primary) so the contrast ratio is >= 4.5:1 for normal
text (or >= 3:1 for large text). Locate the primary and onPrimary declarations
in app_theme.dart and replace onPrimary with a much lighter color (e.g.,
'`#FFFFFF`' or a light gray like '`#F5F5F5`') or compute a contrast-safe foreground
dynamically; ensure the chosen onPrimary yields at least 4.5:1 contrast against
'`#212121`' before committing.
| server.listen( | ||
| (request) => | ||
| unawaited(_handleRequest(request: request, outputDir: outputDir)), | ||
| onError: (Object error, StackTrace stackTrace) { | ||
| ConsoleLogger.error('Server error: $error'); | ||
| }, | ||
| ); | ||
|
|
||
| await done.future; | ||
| } | ||
|
|
||
| Future<void> _handleRequest({ | ||
| required HttpRequest request, | ||
| required String outputDir, | ||
| }) async { | ||
| final response = request.response; | ||
| _setCorsHeaders(response); | ||
|
|
||
| if (request.method == 'OPTIONS') { | ||
| response.statusCode = HttpStatus.noContent; | ||
| await response.close(); | ||
| return; | ||
| } | ||
|
|
||
| if (request.method != 'GET') { | ||
| await _writeJson(response, HttpStatus.methodNotAllowed, { | ||
| 'error': 'Only GET is supported by stac dev.', | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| switch (request.uri.path) { | ||
| case '/': | ||
| case '/health': | ||
| await _writeJson(response, HttpStatus.ok, {'status': 'ok'}); | ||
| return; | ||
| case '/screens': | ||
| await _serveArtifact( | ||
| response: response, | ||
| outputDir: outputDir, | ||
| artifactDirName: 'screens', | ||
| artifactName: request.uri.queryParameters['screenName'], | ||
| missingNameMessage: 'Missing screenName query parameter.', | ||
| ); | ||
| return; | ||
| case '/themes': | ||
| await _serveArtifact( | ||
| response: response, | ||
| outputDir: outputDir, | ||
| artifactDirName: 'themes', | ||
| artifactName: request.uri.queryParameters['themeName'], | ||
| missingNameMessage: 'Missing themeName query parameter.', | ||
| ); | ||
| return; | ||
| default: | ||
| await _writeJson(response, HttpStatus.notFound, { | ||
| 'error': 'Unknown stac dev endpoint: ${request.uri.path}', | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Unhandled errors in _handleRequest leak open connections.
_handleRequest is dispatched via unawaited(...), and server.listen's onError only catches stream errors — not rejections from the handler future. If anything inside throws after CORS headers are set (e.g. artifactFile.readAsString() or stat() on Line 217-218 failing on a permission/IO error), the exception becomes an unhandled async error and the HttpResponse is never closed, so the client hangs until timeout.
Wrap the handler body so the response is always finalized.
🛡️ Proposed fix to guarantee the response is closed
Future<void> _handleRequest({
required HttpRequest request,
required String outputDir,
}) async {
final response = request.response;
- _setCorsHeaders(response);
-
- if (request.method == 'OPTIONS') {
- response.statusCode = HttpStatus.noContent;
- await response.close();
- return;
- }
-
- if (request.method != 'GET') {
- await _writeJson(response, HttpStatus.methodNotAllowed, {
- 'error': 'Only GET is supported by stac dev.',
- });
- return;
- }
-
- switch (request.uri.path) {
- case '/':
- case '/health':
- await _writeJson(response, HttpStatus.ok, {'status': 'ok'});
- return;
- case '/screens':
- await _serveArtifact(
- response: response,
- outputDir: outputDir,
- artifactDirName: 'screens',
- artifactName: request.uri.queryParameters['screenName'],
- missingNameMessage: 'Missing screenName query parameter.',
- );
- return;
- case '/themes':
- await _serveArtifact(
- response: response,
- outputDir: outputDir,
- artifactDirName: 'themes',
- artifactName: request.uri.queryParameters['themeName'],
- missingNameMessage: 'Missing themeName query parameter.',
- );
- return;
- default:
- await _writeJson(response, HttpStatus.notFound, {
- 'error': 'Unknown stac dev endpoint: ${request.uri.path}',
- });
- }
+ try {
+ _setCorsHeaders(response);
+
+ if (request.method == 'OPTIONS') {
+ response.statusCode = HttpStatus.noContent;
+ await response.close();
+ return;
+ }
+
+ if (request.method != 'GET') {
+ await _writeJson(response, HttpStatus.methodNotAllowed, {
+ 'error': 'Only GET is supported by stac dev.',
+ });
+ return;
+ }
+
+ switch (request.uri.path) {
+ case '/':
+ case '/health':
+ await _writeJson(response, HttpStatus.ok, {'status': 'ok'});
+ return;
+ case '/screens':
+ await _serveArtifact(
+ response: response,
+ outputDir: outputDir,
+ artifactDirName: 'screens',
+ artifactName: request.uri.queryParameters['screenName'],
+ missingNameMessage: 'Missing screenName query parameter.',
+ );
+ return;
+ case '/themes':
+ await _serveArtifact(
+ response: response,
+ outputDir: outputDir,
+ artifactDirName: 'themes',
+ artifactName: request.uri.queryParameters['themeName'],
+ missingNameMessage: 'Missing themeName query parameter.',
+ );
+ return;
+ default:
+ await _writeJson(response, HttpStatus.notFound, {
+ 'error': 'Unknown stac dev endpoint: ${request.uri.path}',
+ });
+ }
+ } catch (e) {
+ ConsoleLogger.error('Request handling failed: $e');
+ // Headers may already be sent; best-effort close to avoid a hung client.
+ await response.close().catchError((_) {});
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| server.listen( | |
| (request) => | |
| unawaited(_handleRequest(request: request, outputDir: outputDir)), | |
| onError: (Object error, StackTrace stackTrace) { | |
| ConsoleLogger.error('Server error: $error'); | |
| }, | |
| ); | |
| await done.future; | |
| } | |
| Future<void> _handleRequest({ | |
| required HttpRequest request, | |
| required String outputDir, | |
| }) async { | |
| final response = request.response; | |
| _setCorsHeaders(response); | |
| if (request.method == 'OPTIONS') { | |
| response.statusCode = HttpStatus.noContent; | |
| await response.close(); | |
| return; | |
| } | |
| if (request.method != 'GET') { | |
| await _writeJson(response, HttpStatus.methodNotAllowed, { | |
| 'error': 'Only GET is supported by stac dev.', | |
| }); | |
| return; | |
| } | |
| switch (request.uri.path) { | |
| case '/': | |
| case '/health': | |
| await _writeJson(response, HttpStatus.ok, {'status': 'ok'}); | |
| return; | |
| case '/screens': | |
| await _serveArtifact( | |
| response: response, | |
| outputDir: outputDir, | |
| artifactDirName: 'screens', | |
| artifactName: request.uri.queryParameters['screenName'], | |
| missingNameMessage: 'Missing screenName query parameter.', | |
| ); | |
| return; | |
| case '/themes': | |
| await _serveArtifact( | |
| response: response, | |
| outputDir: outputDir, | |
| artifactDirName: 'themes', | |
| artifactName: request.uri.queryParameters['themeName'], | |
| missingNameMessage: 'Missing themeName query parameter.', | |
| ); | |
| return; | |
| default: | |
| await _writeJson(response, HttpStatus.notFound, { | |
| 'error': 'Unknown stac dev endpoint: ${request.uri.path}', | |
| }); | |
| } | |
| } | |
| Future<void> _handleRequest({ | |
| required HttpRequest request, | |
| required String outputDir, | |
| }) async { | |
| final response = request.response; | |
| try { | |
| _setCorsHeaders(response); | |
| if (request.method == 'OPTIONS') { | |
| response.statusCode = HttpStatus.noContent; | |
| await response.close(); | |
| return; | |
| } | |
| if (request.method != 'GET') { | |
| await _writeJson(response, HttpStatus.methodNotAllowed, { | |
| 'error': 'Only GET is supported by stac dev.', | |
| }); | |
| return; | |
| } | |
| switch (request.uri.path) { | |
| case '/': | |
| case '/health': | |
| await _writeJson(response, HttpStatus.ok, {'status': 'ok'}); | |
| return; | |
| case '/screens': | |
| await _serveArtifact( | |
| response: response, | |
| outputDir: outputDir, | |
| artifactDirName: 'screens', | |
| artifactName: request.uri.queryParameters['screenName'], | |
| missingNameMessage: 'Missing screenName query parameter.', | |
| ); | |
| return; | |
| case '/themes': | |
| await _serveArtifact( | |
| response: response, | |
| outputDir: outputDir, | |
| artifactDirName: 'themes', | |
| artifactName: request.uri.queryParameters['themeName'], | |
| missingNameMessage: 'Missing themeName query parameter.', | |
| ); | |
| return; | |
| default: | |
| await _writeJson(response, HttpStatus.notFound, { | |
| 'error': 'Unknown stac dev endpoint: ${request.uri.path}', | |
| }); | |
| } | |
| } catch (e) { | |
| ConsoleLogger.error('Request handling failed: $e'); | |
| // Headers may already be sent; best-effort close to avoid a hung client. | |
| await response.close().catchError((_) {}); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/stac_cli/lib/src/services/dev_service.dart` around lines 122 - 181,
The handler dispatched via unawaited(server.listen(...)) can throw after CORS
headers are set and leave the HttpResponse open; update _handleRequest to wrap
its entire body in a try/catch/finally (or use try { ... } on success paths }
catch (e, st) { await _writeJson(response, HttpStatus.internalServerError,
{'error': 'Internal server error'}); ConsoleLogger.error('...', e, st); }
finally { if (!response.headersSent && response.contentLength == 0) { /* ensure
close even if nothing written */ } await response.close(); }) so that any
exception (including those from _serveArtifact, stat(), or file.readAsString())
is caught and the response is always closed; use _writeJson for a 500 where
feasible and reference _handleRequest, _serveArtifact, _writeJson, and
_setCorsHeaders when making the change.
What changed
stac devCLI command.stac/.build./screens?screenName=<screen_name>/themes?themeName=<theme_name>/healthstac/directory and rebuilds on save.StacOptions.apiBaseUrlandcopyWith()so debug builds can point to the local server.apiBaseUrl..codex.Why
Developers should be able to iterate on Stac screens locally without deploying every change to the cloud. This makes the Stac development loop faster and gives a cleaner debug workflow for simulator, emulator, web, and physical-device testing.
Testing
dart analyzeinpackages/stac_cli.stac dev --help.examples/movie_app..buildoutput.--host 0.0.0.0.Summary by CodeRabbit
New Features
stac devcommand to run a local development server for screens and themesapiBaseUrlconfiguration option to override API endpoints for local developmentDocumentation
devcommand, available options (--project,--host,--port,--skip-build,--watch), and usage examples