Skip to content

Commit 43ef866

Browse files
committed
fix: make magic install produce a working app on all platforms
- Fix cache config stub to use FileStore() instance instead of string - Export FileStore from barrel file for user-facing config - Register .env as Flutter asset in pubspec.yaml during install - Download sqlite3.wasm for web platform support during install - Add tests for all three fixes (12 new tests)
1 parent 347fe51 commit 43ef866

4 files changed

Lines changed: 436 additions & 5 deletions

File tree

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import 'package:magic/magic.dart';
2+
13
/// Cache Configuration.
24
///
3-
/// - `driver`: `'file'` for persistent disk caching, `'memory'` for session-only.
5+
/// - `driver`: `FileStore()` for persistent disk caching.
46
/// - `ttl`: default time-to-live in seconds.
57
Map<String, dynamic> get cacheConfig => {
68
'cache': {
7-
'driver': 'file',
9+
'driver': FileStore(),
810
'ttl': 3600,
911
},
1012
};

lib/src/commands/install_command.dart

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import 'dart:io';
2+
13
import 'package:args/args.dart';
24
import 'package:path/path.dart' as path;
5+
import 'package:yaml/yaml.dart';
6+
import 'package:yaml_edit/yaml_edit.dart';
37

48
import '../console/command.dart';
59
import '../helpers/file_helper.dart';
@@ -123,6 +127,12 @@ class InstallCommand extends Command {
123127

124128
_createEnvFiles(root);
125129

130+
_registerEnvAsset(root);
131+
132+
if (!withoutDatabase) {
133+
await _setupWebSupport(root);
134+
}
135+
126136
success('Magic installed successfully!');
127137
}
128138

@@ -409,6 +419,163 @@ class InstallCommand extends Command {
409419
return 'My App';
410420
}
411421

422+
// ---------------------------------------------------------------------------
423+
// Asset & Web Setup
424+
// ---------------------------------------------------------------------------
425+
426+
/// Adds `.env` to the `flutter.assets` list in `pubspec.yaml`.
427+
///
428+
/// Ensures the `flutter` section and `assets` list exist. Skips if `.env`
429+
/// is already registered. Uses `yaml_edit` for safe YAML manipulation.
430+
///
431+
/// [root] — absolute path to the Flutter project root.
432+
void _registerEnvAsset(String root) {
433+
final pubspecPath = path.join(root, 'pubspec.yaml');
434+
435+
if (!FileHelper.fileExists(pubspecPath)) {
436+
return;
437+
}
438+
439+
final content = FileHelper.readFile(pubspecPath);
440+
final doc = loadYaml(content);
441+
442+
// 1. Check if .env is already registered — skip if so.
443+
if (doc is Map && doc['flutter'] is Map) {
444+
final assets = doc['flutter']['assets'];
445+
if (assets is List && assets.contains('.env')) {
446+
return;
447+
}
448+
}
449+
450+
// 2. Build the updated assets list.
451+
final existingAssets = <String>[];
452+
if (doc is Map && doc['flutter'] is Map) {
453+
final assets = doc['flutter']['assets'];
454+
if (assets is List) {
455+
existingAssets.addAll(assets.cast<String>());
456+
}
457+
}
458+
existingAssets.add('.env');
459+
460+
// 3. Write back using yaml_edit.
461+
final editor = YamlEditor(content);
462+
463+
try {
464+
editor.parseAt(['flutter', 'assets']);
465+
// Path exists — update it.
466+
editor.update(['flutter', 'assets'], existingAssets);
467+
} catch (_) {
468+
// Path doesn't exist — create it.
469+
try {
470+
editor.parseAt(['flutter']);
471+
// flutter key exists but no assets.
472+
editor.update(['flutter', 'assets'], existingAssets);
473+
} catch (_) {
474+
// flutter key doesn't exist at all.
475+
editor.update(['flutter'], {'assets': existingAssets});
476+
}
477+
}
478+
479+
FileHelper.writeFile(pubspecPath, editor.toString());
480+
}
481+
482+
/// Downloads `sqlite3.wasm` to the `web/` directory for web platform support.
483+
///
484+
/// Reads the resolved sqlite3 version from `pubspec.lock` and downloads the
485+
/// matching WASM binary from GitHub releases. Skips if the file already exists.
486+
/// Prints a warning with a manual download URL on failure.
487+
///
488+
/// [root] — absolute path to the Flutter project root.
489+
Future<void> _setupWebSupport(String root) async {
490+
final targetPath = path.join(root, 'web', 'sqlite3.wasm');
491+
492+
if (FileHelper.fileExists(targetPath)) {
493+
return;
494+
}
495+
496+
final version = _resolveSqliteVersion(root);
497+
final url = Uri.parse(
498+
'https://github.com/simolus3/sqlite3.dart'
499+
'/releases/download/sqlite3-$version/sqlite3.wasm',
500+
);
501+
502+
info('Downloading sqlite3.wasm ($version) for web support...');
503+
504+
final downloaded = await downloadFile(url, targetPath);
505+
506+
if (downloaded) {
507+
info('sqlite3.wasm downloaded to web/');
508+
} else {
509+
warn('Could not download sqlite3.wasm automatically.');
510+
warn('Download manually from: $url');
511+
}
512+
}
513+
514+
/// Resolves the sqlite3 package version from `pubspec.lock`.
515+
///
516+
/// Falls back to a known-good version if `pubspec.lock` is missing or does
517+
/// not contain the sqlite3 package (e.g. before `flutter pub get`).
518+
///
519+
/// [root] — absolute path to the Flutter project root.
520+
/// Returns the version string (e.g. `'2.4.6'`).
521+
String _resolveSqliteVersion(String root) {
522+
const fallback = '2.4.6';
523+
final lockPath = path.join(root, 'pubspec.lock');
524+
525+
if (!FileHelper.fileExists(lockPath)) {
526+
return fallback;
527+
}
528+
529+
try {
530+
final content = FileHelper.readFile(lockPath);
531+
final yaml = loadYaml(content) as YamlMap?;
532+
final packages = yaml?['packages'] as YamlMap?;
533+
final sqlite3 = packages?['sqlite3'] as YamlMap?;
534+
final version = sqlite3?['version'] as String?;
535+
536+
return version ?? fallback;
537+
} catch (_) {
538+
return fallback;
539+
}
540+
}
541+
542+
/// Downloads a file from [url] to [targetPath].
543+
///
544+
/// Returns `true` on success, `false` on failure. Creates parent directories
545+
/// if they do not exist. Overridable in tests to avoid real HTTP requests.
546+
///
547+
/// [url] — the remote URL to download from.
548+
/// [targetPath] — the local file path to write to.
549+
Future<bool> downloadFile(Uri url, String targetPath) async {
550+
final client = HttpClient();
551+
552+
try {
553+
// 1. Follow redirects and fetch the response.
554+
final request = await client.getUrl(url);
555+
final response = await request.close();
556+
557+
if (response.statusCode != 200) {
558+
await response.drain<void>();
559+
return false;
560+
}
561+
562+
// 2. Ensure target directory exists.
563+
final file = File(targetPath);
564+
if (!file.parent.existsSync()) {
565+
file.parent.createSync(recursive: true);
566+
}
567+
568+
// 3. Stream response body to disk.
569+
await response.pipe(file.openWrite());
570+
571+
return true;
572+
} catch (_) {
573+
return false;
574+
} finally {
575+
client.close();
576+
}
577+
}
578+
412579
/// Writes [content] to [filePath] only if the file does not already exist.
413580
///
414581
/// Preserves any customisations the developer may have made after the initial

lib/src/stubs/install_stubs.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ class InstallStubs {
102102
return StubLoader.load('install/view_config');
103103
}
104104

105-
/// Generates `lib/config/cache.dart` with file driver and default TTL.
105+
/// Generates `lib/config/cache.dart` with `FileStore()` driver and default TTL.
106106
///
107-
/// Uses the string `'file'` for the driver (not `FileStore()`) so the
108-
/// generated app does not need to import internal Magic driver classes.
107+
/// Uses `FileStore()` instance as the driver value, matching the framework's
108+
/// own `lib/config/cache.dart` default. Requires `package:magic/magic.dart`.
109109
static String cacheConfigContent() {
110110
return StubLoader.load('install/cache_config');
111111
}

0 commit comments

Comments
 (0)