|
| 1 | +import 'dart:io'; |
| 2 | + |
1 | 3 | import 'package:args/args.dart'; |
2 | 4 | import 'package:path/path.dart' as path; |
| 5 | +import 'package:yaml/yaml.dart'; |
| 6 | +import 'package:yaml_edit/yaml_edit.dart'; |
3 | 7 |
|
4 | 8 | import '../console/command.dart'; |
5 | 9 | import '../helpers/file_helper.dart'; |
@@ -123,6 +127,12 @@ class InstallCommand extends Command { |
123 | 127 |
|
124 | 128 | _createEnvFiles(root); |
125 | 129 |
|
| 130 | + _registerEnvAsset(root); |
| 131 | + |
| 132 | + if (!withoutDatabase) { |
| 133 | + await _setupWebSupport(root); |
| 134 | + } |
| 135 | + |
126 | 136 | success('Magic installed successfully!'); |
127 | 137 | } |
128 | 138 |
|
@@ -409,6 +419,163 @@ class InstallCommand extends Command { |
409 | 419 | return 'My App'; |
410 | 420 | } |
411 | 421 |
|
| 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 | + |
412 | 579 | /// Writes [content] to [filePath] only if the file does not already exist. |
413 | 580 | /// |
414 | 581 | /// Preserves any customisations the developer may have made after the initial |
|
0 commit comments