Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions examples/basic/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,15 @@ packages:
path: "../../packages/sdk"
relative: true
source: path
version: "0.1.0"
version: "0.2.0"
iconify_sdk_core:
dependency: transitive
description:
name: iconify_sdk_core
sha256: "2f246aee51c7867fc9333a142b457685c6623175301c89267a5e5a98c8f1c6fe"
sha256: "86eb36da4b2967fac9ca2b8c0457cccd13dd9de11d4bb5e733e735d2b23e77d5"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
version: "0.2.0"
leak_tracker:
dependency: transitive
description:
Expand Down
8 changes: 4 additions & 4 deletions examples/bundled/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -249,22 +249,22 @@ packages:
path: "../../packages/sdk"
relative: true
source: path
version: "0.1.0"
version: "0.2.0"
iconify_sdk_builder:
dependency: "direct dev"
description:
path: "../../packages/builder"
relative: true
source: path
version: "0.1.0"
version: "0.2.0"
iconify_sdk_core:
dependency: transitive
description:
name: iconify_sdk_core
sha256: "2f246aee51c7867fc9333a142b457685c6623175301c89267a5e5a98c8f1c6fe"
sha256: "86eb36da4b2967fac9ca2b8c0457cccd13dd9de11d4bb5e733e735d2b23e77d5"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
version: "0.2.0"
io:
dependency: transitive
description:
Expand Down
4 changes: 2 additions & 2 deletions packages/builder/lib/src/generator/icon_code_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class IconCodeGenerator {
buffer.writeln(
'// ignore_for_file: constant_identifier_names, library_private_types_in_public_api');
buffer.writeln();
buffer.writeln("import 'package:iconify_sdk_core/iconify_sdk_core.dart';");
buffer.writeln("import 'package:iconify_sdk/iconify_sdk.dart';");
buffer.writeln();

// Group icons by prefix for organization
Expand All @@ -40,7 +40,7 @@ class IconCodeGenerator {
final varName = _toCamelCase(iconName);
buffer.writeln(' /// $fullName');
buffer.writeln(' static const $varName = IconifyIconData(');
buffer.writeln(" body: r'${data.body}',");
buffer.writeln(" body: r'''${data.body}''',");
buffer.writeln(' width: ${data.width},');
buffer.writeln(' height: ${data.height},');
buffer.writeln(' );');
Expand Down
4 changes: 2 additions & 2 deletions packages/builder/lib/src/scanner/icon_name_scanner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class IconNameScanner {
r'([a-z0-9][a-z0-9\-]*:[a-z0-9][a-z0-9\-]*)'
r"['"
r'"]'
r'\s*\)',
r'[\s\S]*?\)',
caseSensitive: false,
);

Expand All @@ -28,7 +28,7 @@ class IconNameScanner {
r'([a-z0-9][a-z0-9\-]*)'
r"['"
r'"]'
r'\s*\)',
r'[\s\S]*?\)',
caseSensitive: false,
);

Expand Down
4 changes: 4 additions & 0 deletions packages/cli/lib/src/cli_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import 'package:args/command_runner.dart';
import 'package:iconify_sdk_core/iconify_sdk_core.dart';
import 'package:mason_logger/mason_logger.dart';

import 'commands/add_command.dart';
import 'commands/doctor_command.dart';
import 'commands/generate_command.dart';
import 'commands/init_command.dart';
import 'commands/licenses_command.dart';
import 'commands/prune_command.dart';
import 'commands/sync_command.dart';

class IconifyCommandRunner extends CommandRunner<int> {
Expand All @@ -24,6 +26,8 @@ class IconifyCommandRunner extends CommandRunner<int> {
addCommand(GenerateCommand(logger: _logger));
addCommand(DoctorCommand(logger: _logger));
addCommand(LicensesCommand(logger: _logger));
addCommand(PruneCommand(logger: _logger));
addCommand(AddCommand(logger: _logger));
}

final Logger _logger;
Expand Down
189 changes: 189 additions & 0 deletions packages/cli/lib/src/commands/add_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import 'dart:convert';
import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:http/http.dart' as http;
import 'package:iconify_sdk_builder/iconify_sdk_builder.dart';
import 'package:iconify_sdk_core/iconify_sdk_core.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;

class AddCommand extends Command<int> {
AddCommand({required Logger logger}) : _logger = logger {
argParser.addOption(
'collection',
abbr: 'c',
help:
'Add all icons from a specific collection (requires local snapshot).',
);
}

@override
String get name => 'add';

@override
String get description => 'Explicitly add icons to used_icons.json.';

@override
String get invocation => 'iconify add <prefix:name> [<prefix:name>...]';

final Logger _logger;

@override
Future<int> run() async {
final configFile = File('iconify.yaml');
if (!configFile.existsSync()) {
_logger.err('iconify.yaml not found. Run "iconify init" first.');
return ExitCode.config.code;
}

final IconifyBuildConfig config;
try {
final content = await configFile.readAsString();
config = IconifyBuildConfig.fromYaml(content);
} catch (e) {
_logger.err('Error parsing iconify.yaml: $e');
return ExitCode.software.code;
}

final cachePath = p.join(config.dataDir, 'used_icons.json');
final cacheFile = File(cachePath);

if (!cacheFile.parent.existsSync()) {
cacheFile.parent.createSync(recursive: true);
}

Map<String, dynamic> cacheJson;
if (cacheFile.existsSync()) {
try {
final content = await cacheFile.readAsString();
final decoded = jsonDecode(content);
if (decoded is Map) {
cacheJson = Map<String, dynamic>.from(decoded);
} else {
cacheJson = _createEmptyCache();
}
} catch (e) {
_logger.err('Failed to parse used_icons.json: $e');
return ExitCode.software.code;
}
} else {
cacheJson = _createEmptyCache();
}

final iconsJson =
Map<String, dynamic>.from(cacheJson['icons'] as Map? ?? {});
final collections = <String, ParsedCollection>{};

final iconsToAdd = <String>[];

final collectionOption = argResults?['collection'] as String?;
if (collectionOption != null) {
final snapshotFile = File('${config.dataDir}/$collectionOption.json');
if (!snapshotFile.existsSync()) {
_logger.err(
'Snapshot for "$collectionOption" not found. Run "iconify sync" first.');
return ExitCode.noInput.code;
}

try {
final jsonStr = await snapshotFile.readAsString();
final collection = IconifyJsonParser.parseCollectionString(jsonStr);
for (final iconName in collection.allNames) {
iconsToAdd.add('$collectionOption:$iconName');
collections[collectionOption] = collection;
}
} catch (e) {
_logger.err('Failed to parse snapshot for "$collectionOption": $e');
return ExitCode.software.code;
}
}

iconsToAdd.addAll(argResults?.rest ?? []);

if (iconsToAdd.isEmpty) {
_logger.err('No icons specified. Usage: iconify add <prefix:name>');
return ExitCode.usage.code;
}

final progress = _logger.progress('Adding ${iconsToAdd.length} icons...');
var addedCount = 0;

final httpClient = http.Client();

try {
for (final fullName in iconsToAdd) {
if (iconsJson.containsKey(fullName)) continue;

final parts = fullName.split(':');
if (parts.length < 2) {
_logger.warn(' ⚠️ Invalid icon name: $fullName');
continue;
}

final prefix = parts[0];
final iconName = parts[1];

if (!collections.containsKey(prefix)) {
final snapshotFile = File('${config.dataDir}/$prefix.json');
if (snapshotFile.existsSync()) {
try {
final jsonStr = await snapshotFile.readAsString();
collections[prefix] =
IconifyJsonParser.parseCollectionString(jsonStr);
} catch (_) {}
}
}

IconifyIconData? data = collections[prefix]?.getIcon(iconName);

if (data == null) {
try {
final uri = Uri.parse(
'https://raw.githubusercontent.com/iconify/icon-sets/master/json/$prefix.json');
final response =
await httpClient.get(uri).timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final collection =
IconifyJsonParser.parseCollectionString(response.body);
collections[prefix] = collection;
data = collection.getIcon(iconName);
}
} catch (_) {}
}

if (data != null) {
final json = data.toJson();
json['source'] = 'added';
iconsJson[fullName] = json;
addedCount++;
} else {
_logger.warn(' ⚠️ Could not find data for "$fullName"');
}
}
} finally {
httpClient.close();
}

if (addedCount > 0) {
cacheJson['generated'] = DateTime.now().toUtc().toIso8601String();
cacheJson['icons'] = iconsJson;
await cacheFile
.writeAsString(const JsonEncoder.withIndent(' ').convert(cacheJson));
progress
.complete('Successfully added $addedCount icons to used_icons.json');
} else {
progress.complete('No new icons were added.');
}

return ExitCode.success.code;
}

Map<String, dynamic> _createEmptyCache() {
return {
'schemaVersion': 1,
'generated': DateTime.now().toUtc().toIso8601String(),
'icons': <String, dynamic>{},
};
}
}
43 changes: 43 additions & 0 deletions packages/cli/lib/src/commands/doctor_command.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'dart:convert';
import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:iconify_sdk_builder/iconify_sdk_builder.dart';
import 'package:iconify_sdk_core/iconify_sdk_core.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;

class DoctorCommand extends Command<int> {
DoctorCommand({required Logger logger}) : _logger = logger;
Expand Down Expand Up @@ -66,6 +68,47 @@ class DoctorCommand extends Command<int> {
}
}
}

// 5. Check Living Cache (used_icons.json)
final cachePath = p.join(config.dataDir, 'used_icons.json');
final cacheFile = File(cachePath);
if (cacheFile.existsSync()) {
try {
final cacheJson = jsonDecode(await cacheFile.readAsString())
as Map<String, dynamic>;
final cachedIcons =
(cacheJson['icons'] as Map<String, dynamic>? ?? {})
.keys
.toSet();

if (cachedIcons.isNotEmpty) {
// Quick scan for stale icons
final usedIcons = <String>{};
final libDir = Directory('lib');
if (libDir.existsSync()) {
final entities = libDir.listSync(recursive: true);
for (final entity in entities) {
if (entity is File && entity.path.endsWith('.dart')) {
if (entity.path.endsWith(config.output)) continue;
final content = await entity.readAsString();
final scanner = IconNameScanner()..scan(content);
usedIcons.addAll(scanner.iconNames);
}
}

final staleIcons = cachedIcons.difference(usedIcons);
if (staleIcons.isNotEmpty) {
_logger.warn(
' ⚠️ Found ${staleIcons.length} stale icons in used_icons.json. Run "iconify prune" to clean up.');
hasWarnings = true;
} else {
_logger.success(
' ✅ used_icons.json is healthy (${cachedIcons.length} icons).');
}
}
}
} catch (_) {}
}
} catch (e) {
_logger.err(' ❌ Failed to parse iconify.yaml: $e');
hasIssues = true;
Expand Down
13 changes: 12 additions & 1 deletion packages/cli/lib/src/commands/init_command.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;

class InitCommand extends Command<int> {
InitCommand({required Logger logger}) : _logger = logger {
Expand Down Expand Up @@ -77,14 +78,24 @@ class InitCommand extends Command<int> {
dir.createSync(recursive: true);
}

// 6. Create initial used_icons.json
final cachePath = p.join(dataDir, 'used_icons.json');
final cacheFile = File(cachePath);
if (!cacheFile.existsSync()) {
await cacheFile.writeAsString(
'{"schemaVersion": 1, "generated": "${DateTime.now().toUtc().toIso8601String()}", "icons": {}}',
);
}

_logger.success('✅ Created iconify.yaml');
_logger.success('✅ Initialized $cachePath');
_logger.info('\nNext steps:');
_logger.info(
'1. Run ${lightCyan.wrap('dart run iconify_sdk_cli sync')} to download icon data.');
_logger
.info('2. Add ${lightCyan.wrap('IconifyIcon')} widgets to your app.');
_logger.info(
'3. Run ${lightCyan.wrap('dart run build_runner build')} to bundle icons for production.');
'3. Run ${lightCyan.wrap('iconify prune')} to clean up stale icons.');

return ExitCode.success.code;
}
Expand Down
Loading
Loading