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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ jobs:

- name: Run tests
run: |
# Run pure Dart tests
melos exec --dir-exists=test --no-flutter -- "dart test"
# Run Flutter tests
melos exec --dir-exists=test --flutter -- "flutter test"

- name: Upload failure artifacts
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
> Alternative to purely implicit write-back. Lets developers explicitly declare which icons they want available.

- [ ] `[AGENT]` Implement `iconify add <prefix:name> [<prefix:name>...]` CLI command
- [ ] Fetches icon data from local synced files or starter — falls back to GitHub raw
- [ ] Fetches icon data from local synced files falls back to GitHub raw
- [ ] Writes directly into `used_icons.json` without needing to run the app first
- [ ] Useful for CI/offline environments and for adding icons before writing the widget code
- [ ] `--collection mdi` flag: adds all icons from a local synced collection
Expand Down
9 changes: 9 additions & 0 deletions docs/guides/production-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,12 @@ void main() {
```

By default, **release builds block all remote network calls**. By following this workflow, your app is now 100% offline-ready and perfectly optimized.

## Step 6: Security & CI

To ensure your supply chain is secure and that you are complying with icon licenses, you can add these steps to your CI pipeline:

1. **Verify Integrity**: Run `dart run iconify verify` to ensure your local icon snapshots match the upstream versions and have not been tampered with.
2. **Enforce Licenses**: Run `dart run iconify generate --strict-licenses` as a pre-build step. This will cause the CI to fail if any used icons require attribution, ensuring you don't accidentally ship icons that violate their license terms without proper attribution.
3. **Audit Attribution**: Check the auto-generated `ICON_ATTRIBUTION.md` in your project root to see a consolidated list of all icons requiring attribution.

10 changes: 10 additions & 0 deletions examples/basic/ICON_ATTRIBUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Icon Attribution

The following icon collections used in this project require attribution:

## Font Awesome 6 Solid
- **Prefix**: `fa6-solid`
- **Author**: Dave Gandy
- **License**: CC BY 4.0 (CC-BY-4.0)
- **License URL**: https://creativecommons.org/licenses/by/4.0/

6,893 changes: 6,893 additions & 0 deletions examples/basic/assets/iconify/fa6-solid.json

Large diffs are not rendered by default.

5,959 changes: 5,959 additions & 0 deletions examples/basic/assets/iconify/lucide.json

Large diffs are not rendered by default.

56,778 changes: 56,778 additions & 0 deletions examples/basic/assets/iconify/mdi.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/basic/assets/iconify/used_icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"schemaVersion": 1, "generated": "2026-03-15T18:14:30.277432Z", "icons": {}}
17 changes: 17 additions & 0 deletions examples/basic/iconify.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"mdi": {
"sha256": "cf9b89dda466f1c384af19d3c7bf835c3c1e75eccf313f7821ac48c14f2c8c7b",
"syncedAt": "2026-03-15T18:16:30.145294Z",
"commitRef": "4c8fcd16dc9426342253b3bfa1481bdf0d486634"
},
"lucide": {
"sha256": "3327086d484a4cd05f28f853d99a64a65dda40d68ed62b215b7d11c80b122015",
"syncedAt": "2026-03-15T18:16:30.468933Z",
"commitRef": "4c8fcd16dc9426342253b3bfa1481bdf0d486634"
},
"fa6-solid": {
"sha256": "61a114ab51362bfe3852bf2cf265375a9a72df62296a8962ce7fc9fb2e9a48fd",
"syncedAt": "2026-03-15T18:17:53.156086Z",
"commitRef": "4c8fcd16dc9426342253b3bfa1481bdf0d486634"
}
}
13 changes: 13 additions & 0 deletions examples/basic/iconify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Iconify SDK Configuration
# See docs at https://github.com/ampslabs/iconify

sets:
- mdi:*
- lucide:*
- fa6-solid:*

data_dir: assets/iconify
output: lib/icons.g.dart
mode: auto
license_policy: warn
fail_on_missing: false
5 changes: 5 additions & 0 deletions examples/basic/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class BasicExample extends StatelessWidget {

const SizedBox(height: 20),

// Font Awesome icon (requires attribution)
IconifyIcon('fa6-solid:circle-info', size: 32, color: Colors.blue),

const SizedBox(height: 20),

// Multi-color icon
IconifyIcon('logos:flutter', size: 48),

Expand Down
23 changes: 23 additions & 0 deletions melos.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
name: iconify_sdk
packages:
- packages/*
- examples/*

scripts:
test:
name: "Test"
description: "Run tests for all packages."
run: melos exec -- "flutter test || dart test"
# Note: flutter test handles pure dart packages too, but usually it's faster
# to let them fall through or use a more specific filter.
# Alternatively, we can use filters:
# run: melos exec --dir-exists=test -- "dart test"

test:pure:
run: melos exec --dir-exists=test --ignore="*flutter*" --ignore="iconify_sdk" -- "dart test"
description: "Run pure Dart tests."

test:flutter:
run: melos exec --dir-exists=test --scope="iconify_sdk" -- "flutter test"
description: "Run Flutter tests."

analyze:
run: melos exec -- "dart analyze --fatal-infos"
description: "Run static analysis."
9 changes: 5 additions & 4 deletions packages/builder/lib/src/generator/icon_code_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class IconCodeGenerator {

// 1. Generate namespace classes
for (final prefix in groupedByPrefix.keys) {
final className = 'Icons${_capitalize(prefix)}';
final className = 'Icons${_capitalize(_toCamelCase(prefix))}';
buffer.writeln('/// Icon set: $prefix');
buffer.writeln('class $className {');
buffer.writeln(' $className._();');
Expand Down Expand Up @@ -56,9 +56,10 @@ class IconCodeGenerator {
'/// Injects all generated icons into a [MemoryIconifyProvider].');
buffer.writeln('void initGeneratedIcons(MemoryIconifyProvider provider) {');
for (final fullName in usedIconNames) {
final prefix = fullName.split(':').first;
final iconName = fullName.split(':').last;
final className = 'Icons${_capitalize(prefix)}';
final parts = fullName.split(':');
final prefix = parts.first;
final iconName = parts.last;
final className = 'Icons${_capitalize(_toCamelCase(prefix))}';
final varName = _toCamelCase(iconName);

if (iconDataMap.containsKey(fullName)) {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
concurrency: 1
1 change: 1 addition & 0 deletions packages/cli/iconify.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
2 changes: 2 additions & 0 deletions packages/cli/lib/src/cli_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'commands/init_command.dart';
import 'commands/licenses_command.dart';
import 'commands/prune_command.dart';
import 'commands/sync_command.dart';
import 'commands/verify_command.dart';

class IconifyCommandRunner extends CommandRunner<int> {
IconifyCommandRunner({Logger? logger})
Expand All @@ -23,6 +24,7 @@ class IconifyCommandRunner extends CommandRunner<int> {

addCommand(InitCommand(logger: _logger));
addCommand(SyncCommand(logger: _logger));
addCommand(VerifyCommand(logger: _logger));
addCommand(GenerateCommand(logger: _logger));
addCommand(DoctorCommand(logger: _logger));
addCommand(LicensesCommand(logger: _logger));
Expand Down
66 changes: 62 additions & 4 deletions packages/cli/lib/src/commands/generate_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ class GenerateCommand extends Command<int> {
help: 'Show what would be generated without writing to disk.',
negatable: false,
);
argParser.addFlag(
'strict-licenses',
help: 'Exit with error if any attribution-required icons are detected.',
negatable: false,
);
argParser.addOption(
'attribution-output',
abbr: 'a',
help: 'Path to generate ICON_ATTRIBUTION.md.',
defaultsTo: 'ICON_ATTRIBUTION.md',
);
}

@override
Expand Down Expand Up @@ -68,6 +79,7 @@ class GenerateCommand extends Command<int> {
// 2. Resolve data from local snapshots
final iconDataMap = <String, IconifyIconData>{};
final collections = <String, ParsedCollection>{};
final attributionRequired = <String, IconifyCollectionInfo>{};

for (final fullName in usedIcons) {
final parts = fullName.split(':');
Expand All @@ -80,8 +92,12 @@ class GenerateCommand extends Command<int> {
final dataFile = File('${config.dataDir}/$prefix.json');
if (dataFile.existsSync()) {
final jsonStr = await dataFile.readAsString();
collections[prefix] =
IconifyJsonParser.parseCollectionString(jsonStr);
final collection = IconifyJsonParser.parseCollectionString(jsonStr);
collections[prefix] = collection;

if (collection.info.requiresAttribution) {
attributionRequired[prefix] = collection.info;
}
} else {
_logger.warn('Snapshot missing for $prefix at ${dataFile.path}');
continue;
Expand All @@ -94,9 +110,24 @@ class GenerateCommand extends Command<int> {
}
}

// 3. License Enforcement
if (attributionRequired.isNotEmpty) {
_logger.warn('⚠️ Some used icons require attribution:');
for (final info in attributionRequired.values) {
_logger.info(
' - ${info.name} (${info.prefix}): ${info.license?.title ?? 'Custom'}');
}

if (argResults?['strict-licenses'] == true) {
progress.fail(
'Strict license check failed: Attribution-required icons detected.');
return ExitCode.software.code;
}
}

progress.update('Generating code...');

// 3. Generate Dart code
// 4. Generate Dart code
final outputContent = IconCodeGenerator.generate(
usedIconNames: usedIcons,
iconDataMap: iconDataMap,
Expand All @@ -111,13 +142,40 @@ class GenerateCommand extends Command<int> {
return ExitCode.success.code;
}

// 4. Write to disk
// 5. Write to disk
final outputFile = File(config.output);
if (!outputFile.parent.existsSync()) {
outputFile.parent.createSync(recursive: true);
}
await outputFile.writeAsString(outputContent);

// 6. Generate Attribution File
if (attributionRequired.isNotEmpty) {
final attributionPath = argResults?['attribution-output'] as String;
final attributionFile = File(attributionPath);
final buffer = StringBuffer();
buffer.writeln('# Icon Attribution');
buffer.writeln();
buffer.writeln(
'The following icon collections used in this project require attribution:');
buffer.writeln();
for (final info in attributionRequired.values) {
buffer.writeln('## ${info.name}');
buffer.writeln('- **Prefix**: `${info.prefix}`');
if (info.author != null) {
buffer.writeln('- **Author**: ${info.author}');
}
buffer.writeln(
'- **License**: ${info.license?.title ?? 'Custom'} (${info.license?.spdx ?? 'N/A'})');
if (info.license?.url != null) {
buffer.writeln('- **License URL**: ${info.license?.url}');
}
buffer.writeln();
}
await attributionFile.writeAsString(buffer.toString());
_logger.info('✅ Generated $attributionPath');
}

progress.complete(
'Successfully generated ${iconDataMap.length} icons into ${config.output}');
return ExitCode.success.code;
Expand Down
Loading
Loading