blob: fb602e07b9a2e9b1f92293f98c58745266a0ac0d [file]
/// 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)');
}
}
}
}