| /// Bundles example projects from `examples/` into ZIP files and copies them |
| /// together with the manifest (`examples.json`) into the frontend's |
| /// `web/examples/` directory so they are served as static assets. |
| /// |
| /// Run from the project root: |
| /// dart run tool/bundle_examples.dart |
| /// |
| /// Only examples whose contents have changed since the last run are re-zipped. |
| /// A `.checksums.json` file in the output directory tracks content hashes. |
| library; |
| |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:typed_data'; |
| |
| import 'package:archive/archive.dart'; |
| import 'package:crypto/crypto.dart'; |
| |
| const _examplesDir = 'examples'; |
| const _outputDir = 'packages/frontend/web/examples'; |
| const _manifestName = 'examples.json'; |
| const _checksumsName = '.checksums.json'; |
| |
| void main() { |
| final manifest = _readManifest(); |
| final outputDir = _ensureOutputDir(); |
| final oldChecksums = _loadChecksums(outputDir); |
| final newChecksums = <String, String>{}; |
| |
| File('$_examplesDir/$_manifestName').copySync('${outputDir.path}/$_manifestName'); |
| |
| var bundled = 0; |
| var skipped = 0; |
| |
| for (final entry in manifest) { |
| final id = entry['id'] as String; |
| _validateExample(id); |
| |
| final checksum = _computeChecksum(Directory('$_examplesDir/$id')); |
| newChecksums[id] = checksum; |
| |
| if (oldChecksums[id] == checksum && File('${outputDir.path}/$id.zip').existsSync()) { |
| skipped++; |
| } else { |
| _createZip(Directory('$_examplesDir/$id'), '${outputDir.path}/$id.zip'); |
| stdout.writeln('✓ Bundled $id'); |
| bundled++; |
| } |
| } |
| |
| _removeStaleZips(oldChecksums, newChecksums, outputDir); |
| _saveChecksums(outputDir, newChecksums); |
| |
| stdout.writeln('\nDone. $bundled bundled, $skipped unchanged.'); |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Manifest & validation |
| // --------------------------------------------------------------------------- |
| |
| List<Map<String, dynamic>> _readManifest() { |
| final file = File('$_examplesDir/$_manifestName'); |
| if (!file.existsSync()) { |
| stderr.writeln('ERROR: $_examplesDir/$_manifestName not found.'); |
| exit(1); |
| } |
| final list = jsonDecode(file.readAsStringSync()) as List<dynamic>; |
| if (list.isEmpty) { |
| stderr.writeln('WARNING: $_manifestName is empty — nothing to bundle.'); |
| exit(0); |
| } |
| return list.cast<Map<String, dynamic>>(); |
| } |
| |
| void _validateExample(String id) { |
| final dir = Directory('$_examplesDir/$id'); |
| if (!dir.existsSync()) { |
| stderr.writeln('ERROR: Directory $_examplesDir/$id/ does not exist.'); |
| exit(1); |
| } |
| if (!File('${dir.path}/pubspec.yaml').existsSync()) { |
| stderr.writeln( |
| 'ERROR: $_examplesDir/$id/pubspec.yaml not found. ' |
| 'Every example must have a pubspec.yaml in its root.', |
| ); |
| exit(1); |
| } |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Checksums |
| // --------------------------------------------------------------------------- |
| |
| Map<String, String> _loadChecksums(Directory outputDir) { |
| final file = File('${outputDir.path}/$_checksumsName'); |
| if (!file.existsSync()) { |
| return {}; |
| } |
| return (jsonDecode(file.readAsStringSync()) as Map<String, dynamic>).cast<String, String>(); |
| } |
| |
| void _saveChecksums(Directory outputDir, Map<String, String> checksums) { |
| File('${outputDir.path}/$_checksumsName').writeAsStringSync( |
| const JsonEncoder.withIndent(' ').convert(checksums), |
| ); |
| } |
| |
| /// Computes a single SHA-256 hash over all file contents in [dir], |
| /// sorted by relative path for determinism. |
| String _computeChecksum(Directory dir) { |
| final files = |
| dir |
| .listSync(recursive: true) |
| .whereType<File>() |
| .map( |
| (f) => ( |
| path: f.path.substring(dir.path.length + 1).replaceAll('\\', '/'), |
| file: f, |
| ), |
| ) |
| .toList() |
| ..sort((a, b) => a.path.compareTo(b.path)); |
| |
| final allBytes = BytesBuilder(); |
| for (final entry in files) { |
| allBytes.add(utf8.encode(entry.path)); |
| allBytes.add(entry.file.readAsBytesSync()); |
| } |
| |
| return sha256.convert(allBytes.toBytes()).toString(); |
| } |
| |
| // --------------------------------------------------------------------------- |
| // ZIP creation & cleanup |
| // --------------------------------------------------------------------------- |
| |
| Directory _ensureOutputDir() { |
| final dir = Directory(_outputDir); |
| if (!dir.existsSync()) { |
| dir.createSync(recursive: true); |
| } |
| return dir; |
| } |
| |
| void _createZip(Directory sourceDir, String outputPath) { |
| final archive = Archive(); |
| |
| for (final file in sourceDir.listSync(recursive: true).whereType<File>()) { |
| final relativePath = file.path.substring(sourceDir.path.length + 1).replaceAll('\\', '/'); |
| archive.addFile(ArchiveFile.bytes(relativePath, file.readAsBytesSync())); |
| } |
| |
| File(outputPath).writeAsBytesSync(ZipEncoder().encode(archive)); |
| } |
| |
| void _removeStaleZips( |
| Map<String, String> oldChecksums, |
| Map<String, String> newChecksums, |
| Directory outputDir, |
| ) { |
| for (final oldId in oldChecksums.keys) { |
| if (!newChecksums.containsKey(oldId)) { |
| final staleZip = File('${outputDir.path}/$oldId.zip'); |
| if (staleZip.existsSync()) { |
| staleZip.deleteSync(); |
| stdout.writeln('✗ Removed $oldId (no longer in manifest)'); |
| } |
| } |
| } |
| } |