From 25eddea4295d5aec2cd4d5fa88e0a2056d8e7322 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Thu, 5 Nov 2026 05:44:50 -0500 Subject: [PATCH 01/26] Scope runtime limits to Ichava's own operations The provider applied memory_limit and max_execution_time while it registered, so every host request ran without its own time limit, and it read the runtime config before the config had merged. RuntimeLimits now applies them around the seed, cache and watch operations only, from the merged config, and restores the host's values after (C-01). --- CHANGELOG.md | 1 + config/ichava-core.php | 4 +- docs/architecture.md | 16 ++---- src/Commands/CacheCommand.php | 8 +-- src/Commands/DatabaseCommand.php | 8 +-- src/Commands/WatchIconFilesCommand.php | 12 ++++- src/Jobs/SeedIconsJob.php | 18 ++++--- src/Providers/IchavaServiceProvider.php | 40 +++++---------- src/Support/RuntimeLimits.php | 57 +++++++++++++++++++++ src/Support/Seeder/IchavaSeeder.php | 4 +- tests/Feature/RuntimeLimitsScopeTest.php | 65 ++++++++++++++++++++++++ 11 files changed, 178 insertions(+), 55 deletions(-) create mode 100644 src/Support/RuntimeLimits.php create mode 100644 tests/Feature/RuntimeLimitsScopeTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c69e7bf..e6d1989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan ### Changed +- **Ichava no longer changes the host's PHP limits** (C-01). The provider raised `memory_limit` and lifted `max_execution_time` while it registered, so every request and command in a host application ran without its own time limit. It also read `runtime.*` before the config had merged, so the shipped `2G` default never took effect and a code fallback of `1G` did. The limits now apply only inside Ichava's own seed, cache and watch commands, `IchavaSeeder::run()` and `SeedIconsJob`, through the new `Support\RuntimeLimits::scope()`, which reads the merged config and restores the previous values when the operation returns or throws. A `SeedIconsJob` now runs under `runtime.memory_limit` (default `2G`) with its own `timeout`, where it set a fixed `512M`. A host that relied on Ichava lifting its limits sets them itself. `tests/Feature/RuntimeLimitsScopeTest.php`. - **The icon-sets catalog lists `ichava/icon-sets-openmoji` and counts emoji as Twemoji only.** `icon-sets.json` gains the openmoji set (8,568 icons, `latest_version` null until the pack is tagged), and the emoji set's count, description and licence follow icon-sets-emoji#99 (3,435 Twemoji icons; OpenMoji moved out). `docs/installation.md`'s pack table follows, as `InstallationPackTableTest` requires. Edited by hand: `bin/sync-icon-sets.php` counts a pack at its latest tag, and emoji's `v0.1.0` still carries the OpenMoji sets until the release re-squash, so the nightly sync will propose emoji's old count back until then. - **The sanitiser keeps `display`, `color` and `marker-*`, each behind a value guard** (parity with ichava/icon-sets-sync#85). A dry run of OpenMoji 17 stripped `display="none"` from 13 subdivision flags, so their hidden shapes would render. `resources/security/svg-policy.json` now matches icon-sets-sync's copy byte for byte (sha256 `7a7c07fd…`): it allows the five names and adds `_allowedAttributes_addendum_presentation`. `display` must be bare keywords, `color` must pass `SvgCssGuard`, and `marker-start`/`marker-mid`/`marker-end` must be `none` or `url(#id)`; `display="url(x)"`, `marker-end="url(https://e/x)"`, `color: expression(…)` and `sodipodi:docname` are still stripped. The sanitiser also drops non-whitespace text and CDATA directly under `` or `` and keeps the indentation; text in ``, ``, `` and `` is untouched. Across 142,247 pack SVGs, 36 outputs changed: 17 for `display`, 17 for `color`, and 2 for stray text (OpenMoji's 1F684 high-speed train, eos-icons' link-off). `RENDER_PIPELINE_VERSION` is now 4, because the stray-text rule changes output that the attribute hash does not see. `tests/Unit/SvgPresentationParityTest.php`, with `tests/fixtures/svg/presentation-vectors.json` shared with react-icon-browser and js-icon-components. - **Doctor's `--format=json` prints a report** instead of the table: `{passed, checks: [...]}`. The table output and the exit code are unchanged. diff --git a/config/ichava-core.php b/config/ichava-core.php index fabc42b..fcb7a22 100644 --- a/config/ichava-core.php +++ b/config/ichava-core.php @@ -118,8 +118,8 @@ |-------------------------------------------------------------------------- | PHP runtime |-------------------------------------------------------------------------- - | Applied at provider registration so heavy operations (seeding 100k+ - | icons, batch processing) get the headroom they need. + | Applied only around Ichava's own seed, watch and cache operations, and + | restored when each returns. The host's limits are never changed. */ 'runtime' => [ 'memory_limit' => env('ICHAVA_MEMORY_LIMIT', '2G'), diff --git a/docs/architecture.md b/docs/architecture.md index a9310b3..7ee99d6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -249,23 +249,17 @@ Migrations are auto-discovered through `discoversMigrations()`, which uses the s Public asset publishing for the SPA happens in `IconBrowserServiceProvider::bootingPackage()`, not core. The browser package's `public/` directory is copied to `public/vendor/ichava/` under the `ichava::icon-browser-assets` tag. -## RuntimeConfigurator (PHP runtime tuning) +## Runtime limits -Heavy operations (seeding 100k+ icons, batch processing) need higher memory limits and longer timeouts. Core uses the `RuntimeConfigurator` utility from `laranail/package-tools`: +Heavy operations (seeding 100k+ icons, watching, cache builds) need a higher memory limit, a longer time limit and Telescope paused. Core applies them only around its own operations, never to the host: `Support\RuntimeLimits::scope()` applies `ichava.ichava-core.runtime.memory_limit`, `runtime.max_execution_time` and `runtime.disable_telescope_in_queue` from the merged config, runs the operation, and restores the previous values when it returns or throws. ```php -use Simtabi\Laranail\PackageTools\Support\RuntimeConfigurator; +use Simtabi\Laranail\Ichava\Support\RuntimeLimits; -RuntimeConfigurator::make() - ->memory('2G') - ->timeout(0) - ->disableTelescope() - ->apply(); +app(RuntimeLimits::class)->scope(fn () => $seeder->seedAllPackages()); ``` -`IchavaServiceProvider::configureRuntimeSettings()` calls this automatically during `registeringPackage()`, reading `ichava.ichava-core.runtime.memory_limit`, `ichava.ichava-core.runtime.max_execution_time`, and `ichava.ichava-core.runtime.disable_telescope_in_queue` from config. No manual setup required. - -See `Simtabi\Laranail\Packager\Package\Support\RuntimeConfigurator` for the full fluent API (preset factories like `forQueueJob()`, `forBatchProcessing()`, scoped `scope()` callbacks, Xdebug and Debugbar toggles). +The seed, cache and watch commands, `IchavaSeeder::run()` and each `SeedIconsJob` run inside it (a job passes its own `timeout`). A host request or command that never calls them keeps its own limits. The configurator is `Simtabi\Laranail\Package\Tools\Support\RuntimeConfigurator` from `laranail/package-tools`. ## IchavaRegistrar (bulk registration helper) diff --git a/src/Commands/CacheCommand.php b/src/Commands/CacheCommand.php index 80fee7e..2adf353 100644 --- a/src/Commands/CacheCommand.php +++ b/src/Commands/CacheCommand.php @@ -4,6 +4,7 @@ namespace Simtabi\Laranail\Ichava\Commands; +use Simtabi\Laranail\Ichava\Support\RuntimeLimits; use function Laravel\Prompts\info; use function Laravel\Prompts\note; use function Laravel\Prompts\spin; @@ -60,7 +61,7 @@ public function __construct( parent::__construct(); } - public function handle(): int + public function handle(RuntimeLimits $limits): int { $action = Coerce::string($this->argument('action')); @@ -81,7 +82,8 @@ public function handle(): int ); } - return match ($action) { + // Cache builds run under Ichava's limits, restored when they return (C-01). + return $limits->scope(fn (): int => match ($action) { 'clear' => $this->handleClear(), 'rebuild' => $this->handleRebuild(), 'refresh' => $this->handleRefresh(), @@ -89,7 +91,7 @@ public function handle(): int 'manifest' => $this->handleManifest(), 'stats' => $this->handleStats(), default => $this->handleInvalidAction((string) $action, $this->validActions), - }; + }); } /** diff --git a/src/Commands/DatabaseCommand.php b/src/Commands/DatabaseCommand.php index f67d562..7c7163d 100644 --- a/src/Commands/DatabaseCommand.php +++ b/src/Commands/DatabaseCommand.php @@ -4,6 +4,7 @@ namespace Simtabi\Laranail\Ichava\Commands; +use Simtabi\Laranail\Ichava\Support\RuntimeLimits; use function Laravel\Prompts\info; use function Laravel\Prompts\note; use function Laravel\Prompts\spin; @@ -68,7 +69,7 @@ public function __construct( parent::__construct(); } - public function handle(): int + public function handle(RuntimeLimits $limits): int { $action = Coerce::string($this->argument('action')); @@ -101,7 +102,8 @@ public function handle(): int return self::FAILURE; } - return match ($action) { + // Seeding runs under Ichava's limits, restored when it returns (C-01). + return $limits->scope(fn (): int => match ($action) { 'seed' => $this->handleSeed(), 'seed:icons' => $this->handleSeedIcons(), 'seed:terms' => $this->handleSeedTerms(), @@ -110,7 +112,7 @@ public function handle(): int 'truncate' => $this->handleTruncate(), 'stats' => $this->handleStats(), default => $this->handleInvalidAction((string) $action, $this->validActions), - }; + }); } /** diff --git a/src/Commands/WatchIconFilesCommand.php b/src/Commands/WatchIconFilesCommand.php index 223f8f1..048fb79 100644 --- a/src/Commands/WatchIconFilesCommand.php +++ b/src/Commands/WatchIconFilesCommand.php @@ -4,6 +4,7 @@ namespace Simtabi\Laranail\Ichava\Commands; +use Simtabi\Laranail\Ichava\Support\RuntimeLimits; use function Laravel\Prompts\info; use function Laravel\Prompts\spin; use function Laravel\Prompts\intro; @@ -31,7 +32,16 @@ class WatchIconFilesCommand extends BaseCommand protected $description = 'Watch icon files for changes and auto-sync database'; - public function handle(IconWatcherService $watcher): int + public function handle(IconWatcherService $watcher, RuntimeLimits $limits): int + { + // The scan runs under Ichava's limits, restored when it returns (C-01). + return $limits->scope(fn (): int => $this->scan($watcher)); + } + + /** + * Rescan one pack, or scan every pack, and report what changed. + */ + private function scan(IconWatcherService $watcher): int { intro(__('ichava/ichava-core::commands.watch.intro')); diff --git a/src/Jobs/SeedIconsJob.php b/src/Jobs/SeedIconsJob.php index 83ac778..00d337d 100644 --- a/src/Jobs/SeedIconsJob.php +++ b/src/Jobs/SeedIconsJob.php @@ -23,11 +23,11 @@ use Simtabi\Laranail\Ichava\Enums\KindSource; use Simtabi\Laranail\DbTools\Query\ChunkedWriter; use Simtabi\Laranail\Ichava\Services\IchavaLogger; +use Simtabi\Laranail\Ichava\Support\RuntimeLimits; use Simtabi\Laranail\Ichava\Services\IconRegistry; use Simtabi\Laranail\Ichava\Support\IconClassifier; use Simtabi\Laranail\Ichava\Support\Seeder\IchavaSeeder; use Simtabi\Laranail\Ichava\Support\Seeder\IconSeederHelpers; -use Simtabi\Laranail\Package\Tools\Support\RuntimeConfigurator; use Simtabi\Laranail\Package\Tools\Services\Database\SeederRunTracker; /** @@ -89,7 +89,7 @@ public function __construct( /** * Execute the job. */ - public function handle(IchavaLogger $logger): void + public function handle(IchavaLogger $logger, RuntimeLimits $limits): void { // Check if batch was cancelled if ($this->batch()?->cancelled()) { @@ -101,12 +101,16 @@ public function handle(IchavaLogger $logger): void return; } - RuntimeConfigurator::make() - ->memory('512M') - ->timeout($this->timeout) - ->disableTelescope() - ->apply(); + // The job's own limits, restored when it returns, so a worker that runs + // other jobs after this one keeps the limits it started with (C-01). + $limits->scope(fn () => $this->seedChunk($logger), timeout: $this->timeout); + } + /** + * Seed this job's chunk and report its progress. + */ + private function seedChunk(IchavaLogger $logger): void + { $fileCount = count($this->files); $logger->info("Processing job {$this->jobIndex}/{$this->totalJobs}", [ 'package' => $this->packageName, diff --git a/src/Providers/IchavaServiceProvider.php b/src/Providers/IchavaServiceProvider.php index 70cebe3..ecbc5a4 100644 --- a/src/Providers/IchavaServiceProvider.php +++ b/src/Providers/IchavaServiceProvider.php @@ -25,6 +25,7 @@ use Simtabi\Laranail\Ichava\Facades\IchavaFacade; use Simtabi\Laranail\Ichava\Support\IconRenderer; use Simtabi\Laranail\Ichava\Support\PathResolver; +use Simtabi\Laranail\Ichava\Support\RuntimeLimits; use Simtabi\Laranail\Ichava\Commands\CacheCommand; use Simtabi\Laranail\Ichava\Services\IchavaLogger; use Simtabi\Laranail\Ichava\Services\IconRegistry; @@ -133,8 +134,10 @@ public function configurePackage(Package $packager): void */ public function registeringPackage(): void { - // Configure runtime settings for heavy operations (seeding, processing) - $this->configureRuntimeSettings(); + // No runtime limits here. A library must not change the host's + // process-wide memory or time limits, and the config has not merged + // yet in this phase. Ichava's own seed, watch and cache operations + // run inside RuntimeLimits::scope(), which restores them after (C-01). // Register morph map for polymorphic relationships Relation::morphMap([ @@ -645,34 +648,17 @@ protected function horizonSupervisorConfig(): array } /** - * Configure PHP runtime settings for heavy Ichava operations. + * The PHP runtime settings Ichava's heavy operations run under. * - * Ichava processes large icon sets (100k+ SVGs) which requires: - * - Higher memory limits for bulk operations - * - Extended execution time for seeding/processing - * - Disabled Telescope during queue jobs to prevent memory leaks - * - * Settings are configurable via ichava.php config file. + * Ichava processes large icon sets (100k+ SVGs), which needs a higher + * memory limit, a longer execution time and Telescope paused. This builds + * that configurator from the merged config and applies nothing: the + * operations call RuntimeLimits::scope(), so the host keeps its limits + * everywhere else. */ - protected function configureRuntimeSettings(): void + protected function configureRuntimeSettings(): RuntimeConfigurator { - // Get configured values with sensible defaults - $memoryLimit = Coerce::string(config('ichava.ichava-core.runtime.memory_limit', '1G')); - $maxExecutionTime = Coerce::int(config('ichava.ichava-core.runtime.max_execution_time', 0)); - $disableTelescopeInQueue = config('ichava.ichava-core.runtime.disable_telescope_in_queue', true); - - // Build runtime configurator with settings from config - $configurator = RuntimeConfigurator::make() - ->memory($memoryLimit) - ->timeout($maxExecutionTime); - - // Disable Telescope during queue processing to prevent memory exhaustion - if ($disableTelescopeInQueue && $this->isIchavaQueueCommand()) { - $configurator->disableTelescope(); - } - - // Apply all settings - $configurator->apply(); + return $this->app->make(RuntimeLimits::class)->configurator(); } /** diff --git a/src/Support/RuntimeLimits.php b/src/Support/RuntimeLimits.php new file mode 100644 index 0000000..d6c311e --- /dev/null +++ b/src/Support/RuntimeLimits.php @@ -0,0 +1,57 @@ +memory(Coerce::string($this->config->get('ichava.ichava-core.runtime.memory_limit', '2G'))) + ->timeout($timeout ?? Coerce::int($this->config->get('ichava.ichava-core.runtime.max_execution_time', 0))); + + if ($this->config->get('ichava.ichava-core.runtime.disable_telescope_in_queue', true) && $this->app->runningInConsole()) { + $configurator->disableTelescope(); + } + + return $configurator; + } + + /** + * Run $operation under Ichava's limits and restore the previous values after. + * + * @template TResult + * + * @param callable(): TResult $operation + * + * @return TResult + */ + public function scope(callable $operation, ?int $timeout = null): mixed + { + return $this->configurator($timeout)->scope($operation); + } +} diff --git a/src/Support/Seeder/IchavaSeeder.php b/src/Support/Seeder/IchavaSeeder.php index 83a87fd..baa5afb 100644 --- a/src/Support/Seeder/IchavaSeeder.php +++ b/src/Support/Seeder/IchavaSeeder.php @@ -28,6 +28,7 @@ use Simtabi\Laranail\Ichava\Models\Icon; use Simtabi\Laranail\Ichava\Jobs\SeedIconsJob; use Simtabi\Laranail\Ichava\Support\CommandName; +use Simtabi\Laranail\Ichava\Support\RuntimeLimits; use Simtabi\Laranail\Console\Tools\Support\Status; use Simtabi\Laranail\Ichava\Services\IchavaLogger; use Simtabi\Laranail\Ichava\Services\IconRegistry; @@ -362,7 +363,8 @@ public function run(): void return; } - $this->seedAllPackages(); + // Seeding runs under Ichava's limits, restored when it returns (C-01). + app(RuntimeLimits::class)->scope(fn () => $this->seedAllPackages()); $this->displayFooter(); } diff --git a/tests/Feature/RuntimeLimitsScopeTest.php b/tests/Feature/RuntimeLimitsScopeTest.php new file mode 100644 index 0000000..fd5d6d8 --- /dev/null +++ b/tests/Feature/RuntimeLimitsScopeTest.php @@ -0,0 +1,65 @@ +hostMemory = ini_get('memory_limit'); + $this->hostTime = ini_get('max_execution_time'); +}); + +afterEach(function (): void { + ini_set('memory_limit', (string) $this->hostMemory); + ini_set('max_execution_time', (string) $this->hostTime); +}); + +it('leaves the host memory and time limits alone when the provider boots', function (): void { + ini_set('memory_limit', '3G'); + ini_set('max_execution_time', '45'); + + $this->refreshApplication(); + + expect(ini_get('memory_limit'))->toBe('3G') + ->and(ini_get('max_execution_time'))->toBe('45'); +}); + +it('applies the merged config inside an ichava operation and restores the host values after', function (): void { + config()->set('ichava.ichava-core.runtime.memory_limit', '2G'); + config()->set('ichava.ichava-core.runtime.max_execution_time', 0); + ini_set('memory_limit', '3G'); + ini_set('max_execution_time', '45'); + + $inside = app(RuntimeLimits::class)->scope(fn (): array => [ini_get('memory_limit'), ini_get('max_execution_time')]); + + expect($inside)->toBe(['2G', '0']) + ->and(ini_get('memory_limit'))->toBe('3G') + ->and(ini_get('max_execution_time'))->toBe('45'); +}); + +it('restores the host values when the operation throws', function (): void { + ini_set('memory_limit', '3G'); + + expect(fn () => app(RuntimeLimits::class)->scope(function (): never { + throw new RuntimeException('boom'); + }))->toThrow(RuntimeException::class); + + expect(ini_get('memory_limit'))->toBe('3G'); +}); + +it('reads the shipped config default, not a code fallback', function (): void { + expect(app(RuntimeLimits::class)->configurator()->getPending()) + ->toMatchArray(['memory_limit' => '2G', 'max_execution_time' => 0]); +}); + +it('lets one operation override the timeout without touching the config', function (): void { + expect(app(RuntimeLimits::class)->configurator(timeout: 120)->getPending()['max_execution_time'])->toBe(120); +}); From 5e31fbf87171c8603cc1d4ccc9091eb4ac6d59b8 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Thu, 5 Nov 2026 05:46:32 -0500 Subject: [PATCH 02/26] Scope Icon's morph alias to ichava-icon and guard it at boot The morph map is flat, so a host or package mapping its own model to the bare alias 'icon' replaced core's entry and stored termables rows loaded that model. A reversible migration moves the rows, and boot refuses a map where the alias resolves to another class (C-02). --- CHANGELOG.md | 1 + ...1_05_000001_namespace_icon_morph_alias.php | 48 ++++++++++++++++ src/Models/Icon.php | 6 +- src/Providers/IchavaServiceProvider.php | 27 +++++++++ tests/Feature/IconMorphAliasTest.php | 56 +++++++++++++++++++ tests/Feature/TermSearchMorphTypeTest.php | 6 +- .../ClaimsIchavaMorphAliasProvider.php | 22 ++++++++ tests/TestCase.php | 8 +++ 8 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 database/migrations/2026_11_05_000001_namespace_icon_morph_alias.php create mode 100644 tests/Feature/IconMorphAliasTest.php create mode 100644 tests/Support/ClaimsIchavaMorphAliasProvider.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d1989..3ab6538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan ### Changed +- **Icon's morph alias is `ichava-icon`, not `icon`** (C-02). The morph map is one flat map per application, so a host or package mapping its own model to `icon` silently replaced core's entry, and every `ichava_icon_termables` row then loaded the wrong model. `Icon::MORPH_ALIAS` is now `ichava-icon`. Migration `2026_11_05_000001_namespace_icon_morph_alias` moves stored `termable_type = 'icon'` rows to it and reinstalls the PostgreSQL search-text functions; `down()` reverses both. Once every provider has registered, boot throws `IchavaException::invalidConfiguration` if the live map resolves `ichava-icon` to any class other than `Icon`. There is no fallback to `icon`. Run `php artisan migrate` after upgrading. `tests/Feature/IconMorphAliasTest.php`. - **Ichava no longer changes the host's PHP limits** (C-01). The provider raised `memory_limit` and lifted `max_execution_time` while it registered, so every request and command in a host application ran without its own time limit. It also read `runtime.*` before the config had merged, so the shipped `2G` default never took effect and a code fallback of `1G` did. The limits now apply only inside Ichava's own seed, cache and watch commands, `IchavaSeeder::run()` and `SeedIconsJob`, through the new `Support\RuntimeLimits::scope()`, which reads the merged config and restores the previous values when the operation returns or throws. A `SeedIconsJob` now runs under `runtime.memory_limit` (default `2G`) with its own `timeout`, where it set a fixed `512M`. A host that relied on Ichava lifting its limits sets them itself. `tests/Feature/RuntimeLimitsScopeTest.php`. - **The icon-sets catalog lists `ichava/icon-sets-openmoji` and counts emoji as Twemoji only.** `icon-sets.json` gains the openmoji set (8,568 icons, `latest_version` null until the pack is tagged), and the emoji set's count, description and licence follow icon-sets-emoji#99 (3,435 Twemoji icons; OpenMoji moved out). `docs/installation.md`'s pack table follows, as `InstallationPackTableTest` requires. Edited by hand: `bin/sync-icon-sets.php` counts a pack at its latest tag, and emoji's `v0.1.0` still carries the OpenMoji sets until the release re-squash, so the nightly sync will propose emoji's old count back until then. - **The sanitiser keeps `display`, `color` and `marker-*`, each behind a value guard** (parity with ichava/icon-sets-sync#85). A dry run of OpenMoji 17 stripped `display="none"` from 13 subdivision flags, so their hidden shapes would render. `resources/security/svg-policy.json` now matches icon-sets-sync's copy byte for byte (sha256 `7a7c07fd…`): it allows the five names and adds `_allowedAttributes_addendum_presentation`. `display` must be bare keywords, `color` must pass `SvgCssGuard`, and `marker-start`/`marker-mid`/`marker-end` must be `none` or `url(#id)`; `display="url(x)"`, `marker-end="url(https://e/x)"`, `color: expression(…)` and `sodipodi:docname` are still stripped. The sanitiser also drops non-whitespace text and CDATA directly under `` or `` and keeps the indentation; text in ``, ``, `` and `` is untouched. Across 142,247 pack SVGs, 36 outputs changed: 17 for `display`, 17 for `color`, and 2 for stray text (OpenMoji's 1F684 high-speed train, eos-icons' link-off). `RENDER_PIPELINE_VERSION` is now 4, because the stray-text rule changes output that the attribute hash does not see. `tests/Unit/SvgPresentationParityTest.php`, with `tests/fixtures/svg/presentation-vectors.json` shared with react-icon-browser and js-icon-components. diff --git a/database/migrations/2026_11_05_000001_namespace_icon_morph_alias.php b/database/migrations/2026_11_05_000001_namespace_icon_morph_alias.php new file mode 100644 index 0000000..773373d --- /dev/null +++ b/database/migrations/2026_11_05_000001_namespace_icon_morph_alias.php @@ -0,0 +1,48 @@ +move(self::PREVIOUS_ALIAS, Icon::MORPH_ALIAS); + } + + public function down(): void + { + $this->move(Icon::MORPH_ALIAS, self::PREVIOUS_ALIAS); + } + + private function move(string $from, string $to): void + { + DB::table('ichava_icon_termables')->where('termable_type', $from)->update(['termable_type' => $to]); + + if (! Helpers::dbDriverIsPgSql()) { + return; + } + + IconSearchTextFunctions::install($to); + + if (! env('ICHAVA_SKIP_FTS_BACKFILL', false)) { + IconSearchTextFunctions::rebuildAll(); + } + } +}; diff --git a/src/Models/Icon.php b/src/Models/Icon.php index 724fdb1..65b475a 100644 --- a/src/Models/Icon.php +++ b/src/Models/Icon.php @@ -76,8 +76,12 @@ final class Icon extends Model /** * The morph alias core registers for this model, and so the value every * `termable_type` row holds. A literal so raw search SQL can quote it. + * + * Vendor-scoped: the morph map is one flat map per application, and a bare + * `icon` is a name a host or another package may claim for its own model. + * Migration 2026_11_05_000001 moves rows stored under `icon` (C-02). */ - public const MORPH_ALIAS = 'icon'; + public const MORPH_ALIAS = 'ichava-icon'; /** @var string */ protected $table = self::TABLE; diff --git a/src/Providers/IchavaServiceProvider.php b/src/Providers/IchavaServiceProvider.php index ecbc5a4..d160738 100644 --- a/src/Providers/IchavaServiceProvider.php +++ b/src/Providers/IchavaServiceProvider.php @@ -23,6 +23,7 @@ use Simtabi\Laranail\DbTools\Query\PortableQuery; use Simtabi\Laranail\Ichava\Commands\InfoCommand; use Simtabi\Laranail\Ichava\Facades\IchavaFacade; +use Simtabi\Laranail\Ichava\Exceptions\IchavaException; use Simtabi\Laranail\Ichava\Support\IconRenderer; use Simtabi\Laranail\Ichava\Support\PathResolver; use Simtabi\Laranail\Ichava\Support\RuntimeLimits; @@ -364,6 +365,13 @@ public function bootingPackage(): void // Schedule automatic log cleanup $this->scheduleLogCleanup(); + // The morph map is flat: a provider registered after core can map + // Icon's alias to its own model, and stored termable rows would then + // load the wrong class. Checked once every provider has registered. + $this->app->booted(function (): void { + $this->assertIconMorphAlias(); + }); + // Default-deny Gates, defined after every provider has booted so a // host's own definitions are seen first and never overwritten. $this->app->booted(function (): void { @@ -373,6 +381,25 @@ public function bootingPackage(): void }); } + /** + * Fail at boot when the live morph map resolves Icon's alias to another class. + * + * @throws IchavaException + */ + protected function assertIconMorphAlias(): void + { + $model = Relation::getMorphedModel(Icon::MORPH_ALIAS); + + if ($model !== Icon::class) { + throw IchavaException::invalidConfiguration(sprintf( + "the morph alias '%s' resolves to %s, not %s. Another provider has claimed it; map that model under its own alias.", + Icon::MORPH_ALIAS, + $model ?? 'nothing', + Icon::class, + )); + } + } + /** * Register core's bundled test set, unless `test.enabled` is false; * `test.path` points it at another directory. diff --git a/tests/Feature/IconMorphAliasTest.php b/tests/Feature/IconMorphAliasTest.php new file mode 100644 index 0000000..fdbf8b9 --- /dev/null +++ b/tests/Feature/IconMorphAliasTest.php @@ -0,0 +1,56 @@ + Icon::class]); +}); + +it('registers a vendor-scoped morph alias for Icon', function (): void { + expect(Icon::MORPH_ALIAS)->toBe('ichava-icon') + ->and((new Icon)->getMorphClass())->toBe('ichava-icon') + ->and(Relation::getMorphedModel('ichava-icon'))->toBe(Icon::class) + ->and(Relation::getMorphedModel('icon'))->toBeNull(); +}); + +it('moves stored termable rows to the scoped alias and back', function (): void { + $migration = require dirname(__DIR__, 2) . '/database/migrations/2026_11_05_000001_namespace_icon_morph_alias.php'; + + $terms = collect(['first', 'second'])->map(fn (string $slug): int => IconTerm::query()->create([ + 'type' => IconTerm::TYPE_CATEGORY, 'package' => 'ichava/test-icons', 'name' => $slug, 'slug' => $slug, + ])->id)->all(); + + DB::table('ichava_icon_termables')->insert([ + ['term_id' => $terms[0], 'termable_id' => 10, 'termable_type' => 'icon'], + ['term_id' => $terms[1], 'termable_id' => 11, 'termable_type' => 'App\\Models\\Post'], + ]); + + $migration->up(); + expect(DB::table('ichava_icon_termables')->orderBy('term_id')->pluck('termable_type')->all()) + ->toBe(['ichava-icon', 'App\\Models\\Post']); + + $migration->down(); + expect(DB::table('ichava_icon_termables')->orderBy('term_id')->pluck('termable_type')->all()) + ->toBe(['icon', 'App\\Models\\Post']); +}); + +it('refuses to boot when another provider maps the alias to a different model', function (): void { + $this->extraProviders = [ClaimsIchavaMorphAliasProvider::class]; + + expect(fn () => $this->refreshApplication()) + ->toThrow(IchavaException::class, 'ichava-icon'); +}); diff --git a/tests/Feature/TermSearchMorphTypeTest.php b/tests/Feature/TermSearchMorphTypeTest.php index 41252f1..ed8334d 100644 --- a/tests/Feature/TermSearchMorphTypeTest.php +++ b/tests/Feature/TermSearchMorphTypeTest.php @@ -11,8 +11,8 @@ | Category and variant names are searchable on PostgreSQL |-------------------------------------------------------------------------- | -| Core registers `Relation::morphMap(['icon' => Icon::class])`, so every -| termables row the seeder writes carries `termable_type = 'icon'`. The +| Core registers `Relation::morphMap(['ichava-icon' => Icon::class])`, so every +| termables row the seeder writes carries `termable_type = 'ichava-icon'`. The | PostgreSQL search clause and the search-text functions compared that column | with the fully-qualified class name instead, so no term ever matched and a | search for a category or variant name found nothing. @@ -110,7 +110,7 @@ function skipUnlessPostgres(): void $definition = DB::selectOne('SELECT pg_get_functiondef(?::regprocedure) AS def', [$function])->def; - expect($definition)->toContain("termable_type = 'icon'") + expect($definition)->toContain("termable_type = '" . Icon::MORPH_ALIAS . "'") ->and($definition)->not->toContain('Models'); })->with([ 'refresh_ichava_icon_search_text(bigint)', diff --git a/tests/Support/ClaimsIchavaMorphAliasProvider.php b/tests/Support/ClaimsIchavaMorphAliasProvider.php new file mode 100644 index 0000000..58bd464 --- /dev/null +++ b/tests/Support/ClaimsIchavaMorphAliasProvider.php @@ -0,0 +1,22 @@ + IconTerm::class]); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index b2b8f9f..1b92edc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,6 +26,13 @@ abstract class TestCase extends Orchestra */ private array $fileCacheDirectories = []; + /** + * Providers a test registers after core's, for the next refreshApplication(). + * + * @var list + */ + protected array $extraProviders = []; + protected function setUp(): void { parent::setUp(); @@ -61,6 +68,7 @@ protected function getPackageProviders($app): array { return [ IchavaServiceProvider::class, + ...$this->extraProviders, ]; } From cb08f67f68cb0d61553ad17d90c5ba128510f0ca Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Thu, 5 Nov 2026 05:51:00 -0500 Subject: [PATCH 03/26] Keep direct SeedIconsJob::handle() callers working and pin the new alias The queue injects RuntimeLimits, so a caller handling the job itself gets it from the container. The snippet test now pins ichava-icon. --- src/Jobs/SeedIconsJob.php | 5 ++++- tests/Unit/IconSnippetFormatTest.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Jobs/SeedIconsJob.php b/src/Jobs/SeedIconsJob.php index 00d337d..1a065a6 100644 --- a/src/Jobs/SeedIconsJob.php +++ b/src/Jobs/SeedIconsJob.php @@ -89,8 +89,11 @@ public function __construct( /** * Execute the job. */ - public function handle(IchavaLogger $logger, RuntimeLimits $limits): void + public function handle(IchavaLogger $logger, ?RuntimeLimits $limits = null): void { + // The queue injects $limits; a caller handling the job directly need not. + $limits ??= app(RuntimeLimits::class); + // Check if batch was cancelled if ($this->batch()?->cancelled()) { $logger->info('🛑 Seeding cancelled, skipping', [ diff --git a/tests/Unit/IconSnippetFormatTest.php b/tests/Unit/IconSnippetFormatTest.php index be801b1..93f1a3a 100644 --- a/tests/Unit/IconSnippetFormatTest.php +++ b/tests/Unit/IconSnippetFormatTest.php @@ -97,6 +97,6 @@ public function variant(string $path, array $map): ?string it('writes term attachments with the registered morph alias', function () { // SeedIconsJob::insertTermables() stores (new Icon)->getMorphClass(); // every reader (relations, getMorphClass joins) queries the same value. - expect((new Icon)->getMorphClass())->toBe('icon'); + expect((new Icon)->getMorphClass())->toBe('ichava-icon'); }); }); From c2272908506bfb5f559eae440dbbec3cc27c457b Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Thu, 5 Nov 2026 05:51:20 -0500 Subject: [PATCH 04/26] Seed variant terms from the registry's variant_metadata The terms seeder parsed config.json a second time to find variants. The registry already carries them, so a variant registered from code is now seeded too (C-04). --- src/Support/Seeder/IconTermsSeeder.php | 20 +++--------- .../Feature/TermsSeederReadsRegistryTest.php | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 tests/Feature/TermsSeederReadsRegistryTest.php diff --git a/src/Support/Seeder/IconTermsSeeder.php b/src/Support/Seeder/IconTermsSeeder.php index b5f6e93..eeb8dc1 100644 --- a/src/Support/Seeder/IconTermsSeeder.php +++ b/src/Support/Seeder/IconTermsSeeder.php @@ -145,22 +145,10 @@ protected function seedPackageCategories(string $packageName, array $packageData */ protected function seedPackageVariants(string $packageName, array $packageData): void { - $basePath = $this->basePathOf($packageData); - - if (! $basePath) { - return; - } - - // Check for config.json - $configPath = $basePath . '/config.json'; - if (! File::exists($configPath)) { - return; - } - - $config = json_decode(File::get($configPath), true); - $metadata = is_array($config) ? ($config['metadata'] ?? null) : null; - $data = is_array($metadata) ? ($metadata['data'] ?? null) : null; - $variants = is_array($data) ? ($data['variants'] ?? []) : []; + // The registry records each pack's variants as `variant_metadata`, + // so this reads them there rather than parsing config.json a second + // time: a variant registered from code is seeded too (C-04). + $variants = $packageData['variant_metadata'] ?? []; if (empty($variants) || ! is_array($variants)) { return; diff --git a/tests/Feature/TermsSeederReadsRegistryTest.php b/tests/Feature/TermsSeederReadsRegistryTest.php new file mode 100644 index 0000000..b9af010 --- /dev/null +++ b/tests/Feature/TermsSeederReadsRegistryTest.php @@ -0,0 +1,32 @@ +fromDirectory($dir); + File::delete($dir . '/config.json'); + + $package = 'ichava-fixtures/variant-pack'; + app(IconTermsSeeder::class)->seedSinglePackage($package, $registry->all()[$package]); + + expect(IconTerm::query()->where('package', $package)->where('type', IconTerm::TYPE_VARIANT)->orderBy('slug')->pluck('slug')->all()) + ->toBe(['filled', 'outline']); + + File::deleteDirectory($dir); +}); From 3de50521d6c9dacef4cf15d596d9573e7f652705 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Thu, 5 Nov 2026 05:51:21 -0500 Subject: [PATCH 05/26] Read served SVGs through SvgDriver::load() The model had its own copy of 'read an SVG safely' with no minimum size and no explicit symlink refusal. It now reads through the driver and maps a refusal to the empty string and audit line it used before. 0 of 246,854 pack SVGs are under the minimum or symlinked (C-12). --- src/Models/Icon.php | 19 ++++---- .../SvgModelReadsThroughDriverTest.php | 48 +++++++++++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/SvgModelReadsThroughDriverTest.php diff --git a/src/Models/Icon.php b/src/Models/Icon.php index 65b475a..79c7f86 100644 --- a/src/Models/Icon.php +++ b/src/Models/Icon.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; use Simtabi\Laranail\Ichava\Enums\IconKind; +use Simtabi\Laranail\Ichava\Drivers\SvgDriver; use Illuminate\Database\Eloquent\Collection; use Simtabi\Laranail\Ichava\Support\Helpers; use Simtabi\Laranail\Ichava\Enums\KindSource; @@ -1185,25 +1186,27 @@ function () use ($path, $storedPath): string { /* * The kind's byte budget is enforced at seed time, but the * file is read again here, on every cache miss -- and it may - * have been replaced or grown since. Check it before reading, - * so an oversized file costs a stat rather than a parse (S2). + * have been replaced or grown since. SvgDriver::load() is the + * one safe reader (C-12): it refuses a symlink, an escape from + * the pack's directory, a file under the minimum size and one + * over this kind's budget, before reading (S2). A refusal + * fails closed, as a sanitiser rejection does. */ $kind = $this->kind instanceof IconKind ? $this->kind : IconKind::Icon; - $limit = $kind->maxFileSize(); - $size = @filesize($path); + $base = $this->getPackageBasePath(); - if ($size !== false && $size > $limit) { + try { + $raw = app(SvgDriver::class)->load($path, baseDir: $base !== '' ? $base : null, maxBytes: $kind->maxFileSize()); + } catch (IchavaException $e) { app(AuditLogger::class)->warning('svg.sanitiser_rejected', [ 'path' => $storedPath, 'package' => $this->package, - 'reason' => IchavaException::svgTooLarge($size, $limit)->getMessage(), + 'reason' => $e->getMessage(), ]); return ''; } - $raw = File::get($path); - try { $svg = app(SvgProcessingService::class); diff --git a/tests/Feature/SvgModelReadsThroughDriverTest.php b/tests/Feature/SvgModelReadsThroughDriverTest.php new file mode 100644 index 0000000..1a49e1c --- /dev/null +++ b/tests/Feature/SvgModelReadsThroughDriverTest.php @@ -0,0 +1,48 @@ +dir = sys_get_temp_dir() . '/ichava-svg-driver-' . bin2hex(random_bytes(4)); + File::ensureDirectoryExists($this->dir); + + $this->icon = function (string $abs): Icon { + $icon = Icon::create([ + 'package' => 'ichava/test-pack', 'name' => pathinfo($abs, PATHINFO_FILENAME), 'path' => $abs, + 'file_hash' => md5($abs), 'keywords' => [], 'tags' => [], 'attributes' => [], 'metadata' => [], + ]); + $icon->forceFill(['path' => $abs])->save(); + + return $icon->fresh(); + }; +}); + +afterEach(function (): void { + File::deleteDirectory($this->dir); +}); + +it('refuses a file below the minimum SVG size, as the driver does', function (): void { + File::put($this->dir . '/tiny.svg', ''); + + expect(($this->icon)($this->dir . '/tiny.svg')->svg_content)->toBe(''); +}); + +it('refuses a symlinked SVG, as the driver does', function (): void { + File::put($this->dir . '/real.svg', ''); + symlink($this->dir . '/real.svg', $this->dir . '/link.svg'); + + expect(($this->icon)($this->dir . '/link.svg')->svg_content)->toBe('') + ->and(($this->icon)($this->dir . '/real.svg')->svg_content)->toContain(' Date: Thu, 5 Nov 2026 05:51:21 -0500 Subject: [PATCH 06/26] Give pack config caching one owner and one parser PackConfigCache owns the config.json cache keys, so the watcher forgets a pack's config without reaching ConfigurationService and, through it, IconCacheService (C-05). ConfigurationService parses through Helpers::loadConfigJson() instead of its own copy (C-04). --- src/Services/ConfigurationService.php | 55 ++----------------- src/Services/IconCacheService.php | 9 ++- src/Services/IconWatcherService.php | 4 +- src/Support/PackConfigCache.php | 54 ++++++++++++++++++ tests/Feature/LoggerErrorCallSitesTest.php | 3 +- tests/Feature/WatcherCacheDecouplingTest.php | 36 ++++++++++++ tests/Feature/WatcherMissingDirectoryTest.php | 3 +- 7 files changed, 110 insertions(+), 54 deletions(-) create mode 100644 src/Support/PackConfigCache.php create mode 100644 tests/Feature/WatcherCacheDecouplingTest.php diff --git a/src/Services/ConfigurationService.php b/src/Services/ConfigurationService.php index 94a42e9..f401b9b 100644 --- a/src/Services/ConfigurationService.php +++ b/src/Services/ConfigurationService.php @@ -7,6 +7,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Facades\File; use Simtabi\Laranail\Ichava\Support\Helpers; +use Simtabi\Laranail\Ichava\Support\PackConfigCache; use Simtabi\Laranail\Ichava\Support\PackConfigData; use Simtabi\Laranail\Ichava\Exceptions\IchavaException; @@ -27,7 +28,7 @@ final class ConfigurationService * Constructor with dependency injection */ public function __construct( - private IconCacheService $cache, + private PackConfigCache $configCache, ) {} /** @@ -41,12 +42,8 @@ public function loadPackageConfig(string $directoryPath): array { $configPath = $this->resolveConfigPath($directoryPath); - if (! File::exists($configPath)) { - throw IchavaException::missingConfigFile($configPath); - } - - $json = File::get($configPath); - $config = $this->parseJson($json, $configPath); + // One config.json parser in core: Helpers::loadConfigJson() (C-04). + $config = Helpers::loadConfigJson($directoryPath); $this->validatePackageConfig($config, $configPath); return $config; @@ -59,11 +56,7 @@ public function loadPackageConfig(string $directoryPath): array */ public function rememberPackageConfig(string $directoryPath): array { - $cacheKey = $this->getCacheKey($directoryPath); - - $config = $this->cache->rememberConfig($cacheKey, function () use ($directoryPath) { - return $this->loadPackageConfig($directoryPath); - }); + $config = $this->configCache->remember($directoryPath, fn (): array => $this->loadPackageConfig($directoryPath)); return is_array($config) ? $this->stringKeyed($config, $this->resolveConfigPath($directoryPath)) : $this->loadPackageConfig($directoryPath); } @@ -73,9 +66,7 @@ public function rememberPackageConfig(string $directoryPath): array */ public function forgetPackageConfig(string $directoryPath): bool { - $cacheKey = $this->getCacheKey($directoryPath); - - return $this->cache->forgetConfig($cacheKey); + return $this->configCache->forget($directoryPath); } /** @@ -200,38 +191,4 @@ private function stringKeyed(array $config, string $configPath): array return $object; } - - /** - * Parse JSON with error handling - * - * @return array - */ - private function parseJson(string $json, string $configPath): array - { - $config = json_decode($json, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw IchavaException::invalidConfig( - 'JSON parse error: ' . json_last_error_msg(), - $configPath, - ); - } - - if (! is_array($config)) { - throw IchavaException::invalidConfig( - 'Config must be a JSON object', - $configPath, - ); - } - - return $this->stringKeyed($config, $configPath); - } - - /** - * Generate cache key - */ - private function getCacheKey(string $directoryPath): string - { - return 'config:' . md5($directoryPath); - } } diff --git a/src/Services/IconCacheService.php b/src/Services/IconCacheService.php index d823faf..4e3509b 100644 --- a/src/Services/IconCacheService.php +++ b/src/Services/IconCacheService.php @@ -4,6 +4,7 @@ namespace Simtabi\Laranail\Ichava\Services; +use Simtabi\Laranail\Ichava\Support\PackConfigCache; use Closure; use Exception; use __PHP_Incomplete_Class; @@ -27,6 +28,7 @@ final class IconCacheService public function __construct( private IchavaLogger $logger, + private PackConfigCache $configCache, ) { // Use config() directly instead of injecting ConfigurationService $this->ttl = config()->integer('ichava.ichava-core.cache.ttl', IchavaConstants::DEFAULT_CACHE_TTL); @@ -82,7 +84,7 @@ public function clearAll(): array foreach (app(IconRegistry::class)->all() as $metadata) { $path = $metadata['base_path'] ?? null; - if (is_string($path) && app(ConfigurationService::class)->forgetPackageConfig($path)) { + if (is_string($path) && $this->configCache->forget($path)) { $cleared['configs']++; } } @@ -542,7 +544,10 @@ private static function satisfies(mixed $value, ?string $expects): bool } /** - * Get IconWatcherService instance (lazy loading to avoid circular dependency) + * The watcher, resolved on first use. It cannot be constructor-injected: + * the watcher needs the registry, the registry needs SvgDriver, and + * SvgDriver needs this service. The watcher no longer reaches back here + * through ConfigurationService (it forgets configs via PackConfigCache). */ private function watcher(): IconWatcherService { diff --git a/src/Services/IconWatcherService.php b/src/Services/IconWatcherService.php index 5419b5d..cef1075 100644 --- a/src/Services/IconWatcherService.php +++ b/src/Services/IconWatcherService.php @@ -4,6 +4,7 @@ namespace Simtabi\Laranail\Ichava\Services; +use Simtabi\Laranail\Ichava\Support\PackConfigCache; use Throwable; use SplFileInfo; use Carbon\Carbon; @@ -47,6 +48,7 @@ class IconWatcherService public function __construct( protected IconRegistry $registry, protected IchavaLogger $logger, + protected PackConfigCache $configCache, protected ?IconClassifier $classifier = null, ) {} @@ -489,7 +491,7 @@ protected function syncPackage(string $package, array $metadata): array // A pack's config.json may have changed with its files (a category // re-filed as illustrations, a new theme twin); drop the cached copy // so the next read sees it. - app(ConfigurationService::class)->forgetPackageConfig($basePath); + $this->configCache->forget($basePath); // Scan file system $diskIcons = $this->scanDiskIcons($basePath, $package); diff --git a/src/Support/PackConfigCache.php b/src/Support/PackConfigCache.php new file mode 100644 index 0000000..39178bf --- /dev/null +++ b/src/Support/PackConfigCache.php @@ -0,0 +1,54 @@ +cache->store('file')->remember($this->key($directoryPath), self::TTL_SECONDS, $load(...)); + } catch (Throwable) { + return $load(); + } + } + + /** Drop the cached config for $directoryPath; false when nothing was dropped or the store failed. */ + public function forget(string $directoryPath): bool + { + try { + return $this->cache->store('file')->forget($this->key($directoryPath)); + } catch (Throwable) { + return false; + } + } +} diff --git a/tests/Feature/LoggerErrorCallSitesTest.php b/tests/Feature/LoggerErrorCallSitesTest.php index 68a5c13..6c08158 100644 --- a/tests/Feature/LoggerErrorCallSitesTest.php +++ b/tests/Feature/LoggerErrorCallSitesTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Simtabi\Laranail\Ichava\Support\PackConfigCache; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\File; use Simtabi\Laranail\Ichava\Events\IconCacheEvent; @@ -58,7 +59,7 @@ protected function migrationPath(): ?string it('logs a directory the watcher cannot scan instead of throwing', function (): void { File::shouldReceive('allFiles')->andThrow(IchavaException::invalidConfiguration('unreadable')); - $watcher = new IconWatcherService(app(IconRegistry::class), app(IchavaLogger::class)); + $watcher = new IconWatcherService(app(IconRegistry::class), app(IchavaLogger::class), app(PackConfigCache::class)); $scan = new ReflectionMethod($watcher, 'scanDiskIcons'); expect($scan->invoke($watcher, '/nowhere', 'tests/pack'))->toBe([]); diff --git a/tests/Feature/WatcherCacheDecouplingTest.php b/tests/Feature/WatcherCacheDecouplingTest.php new file mode 100644 index 0000000..98038f7 --- /dev/null +++ b/tests/Feature/WatcherCacheDecouplingTest.php @@ -0,0 +1,36 @@ +resolving(ConfigurationService::class, function () use (&$resolved): void { + $resolved++; + }); + + app(IconWatcherService::class)->rescanPackage('ichava/test-icons'); + + expect($resolved)->toBe(0); +}); + +it('forgets through the same key ConfigurationService remembers under', function (): void { + $dir = dirname(__DIR__, 2) . '/resources/assets/svg/test-icons'; + $cache = app(PackConfigCache::class); + + app(ConfigurationService::class)->rememberPackageConfig($dir); + + expect(cache()->store('file')->has($cache->key($dir)))->toBeTrue() + ->and($cache->forget($dir))->toBeTrue() + ->and(cache()->store('file')->has($cache->key($dir)))->toBeFalse(); +}); diff --git a/tests/Feature/WatcherMissingDirectoryTest.php b/tests/Feature/WatcherMissingDirectoryTest.php index f80d473..ae99482 100644 --- a/tests/Feature/WatcherMissingDirectoryTest.php +++ b/tests/Feature/WatcherMissingDirectoryTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Simtabi\Laranail\Ichava\Support\PackConfigCache; use Illuminate\Support\Facades\Log; use Simtabi\Laranail\Ichava\Services\IchavaLogger; use Simtabi\Laranail\Ichava\Services\IconRegistry; @@ -34,7 +35,7 @@ $missing = sys_get_temp_dir() . '/ichava-missing-' . bin2hex(random_bytes(6)); expect(is_dir($missing))->toBeFalse(); - $watcher = new IconWatcherService(app(IconRegistry::class), app(IchavaLogger::class)); + $watcher = new IconWatcherService(app(IconRegistry::class), app(IchavaLogger::class), app(PackConfigCache::class)); $scan = new ReflectionMethod($watcher, 'scanDiskIcons'); expect($scan->invoke($watcher, $missing, 'tests/pack'))->toBe([]); From 83de36e68e340b39d7847d62bde7064c16c9034a Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Thu, 5 Nov 2026 05:53:18 -0500 Subject: [PATCH 07/26] Snapshot what every estate and fixture pack registers A golden test over 31 pack directories, written by the registration code as it is today, so the single registration path that follows can prove it changes nothing. --- tests/Feature/RegistrationGoldenTest.php | 74 + tests/fixtures/RegistrationGolden.json | 1596 +++++++++++++++++ .../RegistrationGolden/bootstrap/config.json | 71 + .../bootstrap/files/.gitkeep | 0 .../RegistrationGolden/carbon/config.json | 90 + .../RegistrationGolden/carbon/files/.gitkeep | 0 .../RegistrationGolden/emoji/config.json | 84 + .../RegistrationGolden/emoji/files/.gitkeep | 0 .../RegistrationGolden/extras/config.json | 1020 +++++++++++ .../RegistrationGolden/extras/files/.gitkeep | 0 .../RegistrationGolden/flag/config.json | 85 + .../RegistrationGolden/flag/files/.gitkeep | 0 .../RegistrationGolden/fluent/config.json | 90 + .../RegistrationGolden/fluent/files/.gitkeep | 0 .../font-awesome-free/config.json | 105 ++ .../font-awesome-free/files/.gitkeep | 0 .../RegistrationGolden/heroicons/config.json | 124 ++ .../heroicons/files/.gitkeep | 0 .../RegistrationGolden/iconoir/config.json | 106 ++ .../RegistrationGolden/iconoir/files/.gitkeep | 0 .../RegistrationGolden/ionicons/config.json | 99 + .../ionicons/files/.gitkeep | 0 .../RegistrationGolden/lucide/config.json | 77 + .../RegistrationGolden/lucide/files/.gitkeep | 0 .../material-symbols/config.json | 139 ++ .../material-symbols/files/.gitkeep | 0 .../RegistrationGolden/mdi/config.json | 81 + .../RegistrationGolden/mdi/files/.gitkeep | 0 .../RegistrationGolden/octicons/config.json | 121 ++ .../octicons/files/.gitkeep | 0 .../RegistrationGolden/openmoji/config.json | 88 + .../openmoji/files/.gitkeep | 0 .../RegistrationGolden/phosphor/config.json | 136 ++ .../phosphor/files/.gitkeep | 0 .../RegistrationGolden/remix/config.json | 224 +++ .../RegistrationGolden/remix/files/.gitkeep | 0 .../simple-icons/config.json | 1139 ++++++++++++ .../simple-icons/files/.gitkeep | 0 .../RegistrationGolden/tabler/config.json | 97 + .../RegistrationGolden/tabler/files/.gitkeep | 0 40 files changed, 5646 insertions(+) create mode 100644 tests/Feature/RegistrationGoldenTest.php create mode 100644 tests/fixtures/RegistrationGolden.json create mode 100644 tests/fixtures/RegistrationGolden/bootstrap/config.json create mode 100644 tests/fixtures/RegistrationGolden/bootstrap/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/carbon/config.json create mode 100644 tests/fixtures/RegistrationGolden/carbon/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/emoji/config.json create mode 100644 tests/fixtures/RegistrationGolden/emoji/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/extras/config.json create mode 100644 tests/fixtures/RegistrationGolden/extras/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/flag/config.json create mode 100644 tests/fixtures/RegistrationGolden/flag/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/fluent/config.json create mode 100644 tests/fixtures/RegistrationGolden/fluent/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/font-awesome-free/config.json create mode 100644 tests/fixtures/RegistrationGolden/font-awesome-free/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/heroicons/config.json create mode 100644 tests/fixtures/RegistrationGolden/heroicons/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/iconoir/config.json create mode 100644 tests/fixtures/RegistrationGolden/iconoir/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/ionicons/config.json create mode 100644 tests/fixtures/RegistrationGolden/ionicons/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/lucide/config.json create mode 100644 tests/fixtures/RegistrationGolden/lucide/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/material-symbols/config.json create mode 100644 tests/fixtures/RegistrationGolden/material-symbols/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/mdi/config.json create mode 100644 tests/fixtures/RegistrationGolden/mdi/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/octicons/config.json create mode 100644 tests/fixtures/RegistrationGolden/octicons/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/openmoji/config.json create mode 100644 tests/fixtures/RegistrationGolden/openmoji/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/phosphor/config.json create mode 100644 tests/fixtures/RegistrationGolden/phosphor/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/remix/config.json create mode 100644 tests/fixtures/RegistrationGolden/remix/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/simple-icons/config.json create mode 100644 tests/fixtures/RegistrationGolden/simple-icons/files/.gitkeep create mode 100644 tests/fixtures/RegistrationGolden/tabler/config.json create mode 100644 tests/fixtures/RegistrationGolden/tabler/files/.gitkeep diff --git a/tests/Feature/RegistrationGoldenTest.php b/tests/Feature/RegistrationGoldenTest.php new file mode 100644 index 0000000..52fdde6 --- /dev/null +++ b/tests/Feature/RegistrationGoldenTest.php @@ -0,0 +1,74 @@ + is_file($dir . '/config.json'))); + sort($dirs); + + return $dirs; +} + +/** @return array */ +function goldenMetadataFor(string $dir): array +{ + $root = dirname(__DIR__, 2); + $registry = app(IconRegistry::class); + + try { + $before = array_keys($registry->all()); + $registry->fromDirectory($dir); + $added = array_values(array_diff(array_keys($registry->all()), $before)); + $name = $added[0] ?? json_decode((string) file_get_contents($dir . '/config.json'), true)['package']['name']; + $metadata = $registry->all()[$name]; + } catch (Throwable $e) { + return ['exception' => $e::class, 'message' => str_replace($root, '', $e->getMessage())]; + } + + unset($metadata['registered_at']); + $metadata['base_path'] = str_replace($root, '', (string) $metadata['base_path']); + ksort($metadata); + + return ['aliases' => $registry->aliases($name), 'metadata' => $metadata]; +} + +it('registers every estate and fixture pack exactly as before the refactor', function (): void { + $root = dirname(__DIR__, 2); + $snapshotPath = $root . '/tests/fixtures/RegistrationGolden.json'; + $dirs = goldenPackDirectories(); + + expect(count($dirs))->toBeGreaterThanOrEqual(31); + + $actual = []; + foreach ($dirs as $dir) { + app()->forgetInstance(IconRegistry::class); + $actual[str_replace($root, '', $dir)] = goldenMetadataFor($dir); + } + + if (getenv('ICHAVA_WRITE_GOLDEN') === '1') { + file_put_contents($snapshotPath, json_encode($actual, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); + } + + expect($actual)->toBe(json_decode((string) file_get_contents($snapshotPath), true)); +}); diff --git a/tests/fixtures/RegistrationGolden.json b/tests/fixtures/RegistrationGolden.json new file mode 100644 index 0000000..155da9f --- /dev/null +++ b/tests/fixtures/RegistrationGolden.json @@ -0,0 +1,1596 @@ +{ + "/resources/assets/svg/test-icons": { + "aliases": [], + "metadata": { + "base_path": "/resources/assets/svg/test-icons", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "Internal test icon sets for development, testing, and quality assurance purposes", + "homepage": "https://github.com/simtabi/ichava", + "icon_set_name": "ichava/test-icons", + "item_kinds": [], + "keywords": [ + "icons", + "test", + "development", + "qa", + "internal" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Test Icons Collection", + "package_name": "ichava/test-icons", + "prefix": "test", + "provider_class": null, + "repository": "https://github.com/simtabi/ichava", + "theme_variants": [], + "total": 4, + "variant_metadata": [], + "vendor": "ichava", + "version": "1.0.0" + } + }, + "/tests/fixtures/BasePack/resources/assets/svg": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/BasePack/resources/assets/svg", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "A pack built on IconPackServiceProvider", + "homepage": null, + "icon_set_name": "ichava/icon-sets-base-fixture", + "item_kinds": [], + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Base Fixture", + "package_name": "ichava/icon-sets-base-fixture", + "prefix": "bf", + "provider_class": null, + "repository": null, + "theme_variants": [], + "total": 1, + "variant_metadata": [], + "vendor": "ichava", + "version": "1.0.0" + } + }, + "/tests/fixtures/BrokenKindPack": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/BrokenKindPack", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "Every kind mistake doctor reports, in one pack.", + "homepage": null, + "icon_set_name": "ichava-fixtures/broken-kind-pack", + "item_kinds": { + "glyphs/ghost": "illustration" + }, + "keywords": [], + "kind": "icon", + "kind_issues": [ + { + "check": "kind.invalid_value", + "key": "package.kind", + "value": "animation" + }, + { + "check": "kind.invalid_value", + "key": "metadata.data.categories.glyphs.kind", + "value": "bogus" + }, + { + "check": "kind.invalid_glob", + "key": "metadata.data.kinds.rules.0.match", + "value": "a**", + "index": 0, + "pattern": "a**" + }, + { + "check": "kind.invalid_value", + "key": "metadata.data.kinds.rules.1.kind", + "value": "nope" + }, + { + "check": "kind.invalid_value", + "key": "metadata.data.items.glyphs/star.kind", + "value": "sticker" + } + ], + "kind_rules": [ + { + "match": "unused/**", + "kind": "illustration" + }, + { + "match": "scenes/park-dark", + "kind": "illustration" + }, + { + "match": "scenes/*", + "kind": "illustration" + } + ], + "labels": [], + "license": "MIT", + "name": "Broken kind fixture pack", + "package_name": "ichava-fixtures/broken-kind-pack", + "prefix": "brokenfix", + "provider_class": null, + "repository": null, + "theme_variants": { + "scenes": { + "dark": "-dark" + } + }, + "total": 3, + "variant_metadata": [], + "vendor": "ichava-fixtures", + "version": "1.0.0" + } + }, + "/tests/fixtures/HeuristicPack": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/HeuristicPack", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "A flag that must not be suggested and a scene that must.", + "homepage": null, + "icon_set_name": "ichava-fixtures/heuristic-pack", + "item_kinds": [], + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Heuristic fixture pack", + "package_name": "ichava-fixtures/heuristic-pack", + "prefix": "heurfix", + "provider_class": null, + "repository": null, + "theme_variants": [], + "total": 3, + "variant_metadata": [], + "vendor": "ichava-fixtures", + "version": "1.0.0" + } + }, + "/tests/fixtures/IconAliases": { + "aliases": { + "shapes/circle": "shapes/dot", + "shapes/ring": "shapes/dot" + }, + "metadata": { + "base_path": "/tests/fixtures/IconAliases", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "Test fixture: a pack that kept one of two identical icons and aliases the dropped name", + "homepage": null, + "icon_set_name": "ichava-fixtures/aliased", + "item_kinds": [], + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Fixture pack (aliases.json)", + "package_name": "ichava-fixtures/aliased", + "prefix": "fxa", + "provider_class": null, + "repository": null, + "theme_variants": [], + "total": 2, + "variant_metadata": [], + "vendor": "ichava-fixtures", + "version": "1.0.0" + } + }, + "/tests/fixtures/KindPack": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/KindPack", + "blade_component": null, + "category_kinds": { + "scenes": "illustration" + }, + "declared_kind": "icon", + "description": "Icons and illustrations side by side, with a dark theme twin.", + "homepage": null, + "icon_set_name": "ichava-fixtures/kind-pack", + "item_kinds": [], + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Kind fixture pack", + "package_name": "ichava-fixtures/kind-pack", + "prefix": "kindfix", + "provider_class": null, + "repository": null, + "theme_variants": { + "scenes": { + "dark": "-dark" + } + }, + "total": 4, + "variant_metadata": [], + "vendor": "ichava-fixtures", + "version": "1.0.0" + } + }, + "/tests/fixtures/MixedKindPack": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/MixedKindPack", + "blade_component": null, + "category_kinds": { + "illustrations": "illustration" + }, + "declared_kind": "icon", + "description": "The contract's mixed pack: item, rule, category and pack kinds side by side.", + "homepage": null, + "icon_set_name": "ichava-fixtures/mixed-pack", + "item_kinds": { + "glyphs/hero-banner": "illustration" + }, + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [ + { + "match": "spot/tiny-*", + "kind": "icon" + }, + { + "match": "spot/**", + "kind": "illustration" + } + ], + "labels": [], + "license": "MIT", + "name": "Mixed kind fixture pack", + "package_name": "ichava-fixtures/mixed-pack", + "prefix": "mixfix", + "provider_class": null, + "repository": null, + "theme_variants": { + "illustrations": { + "dark": "-dark" + } + }, + "total": 6, + "variant_metadata": [], + "vendor": "ichava-fixtures", + "version": "1.0.0" + } + }, + "/tests/fixtures/PackConfigShapes/nested": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/PackConfigShapes/nested", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "Test fixture: config.json with variants and categories at metadata.data, the shape every pack ships", + "homepage": null, + "icon_set_name": "ichava-fixtures/nested-shape", + "item_kinds": [], + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Fixture pack (metadata.data shape)", + "package_name": "ichava-fixtures/nested-shape", + "prefix": "fxn", + "provider_class": null, + "repository": null, + "theme_variants": [], + "total": 4, + "variant_metadata": { + "default": { + "name": "Default", + "slug": "default", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [] + } + }, + "vendor": "ichava-fixtures", + "version": "1.0.0" + } + }, + "/tests/fixtures/PackConfigShapes/top-level": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/PackConfigShapes/top-level", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "Test fixture: config.json with variants and categories only at the top level, a shape core does not read", + "homepage": null, + "icon_set_name": "ichava-fixtures/top-level-shape", + "item_kinds": [], + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Fixture pack (top-level shape)", + "package_name": "ichava-fixtures/top-level-shape", + "prefix": "fxt", + "provider_class": null, + "repository": null, + "theme_variants": [], + "total": 1, + "variant_metadata": [], + "vendor": "ichava-fixtures", + "version": "1.0.0" + } + }, + "/tests/fixtures/RegistrationGolden/bootstrap": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/bootstrap", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "2,078 Bootstrap Icons from v1.13.2, 16px, flat; filled forms keep their -fill suffix.", + "homepage": "https://icons.getbootstrap.com/", + "icon_set_name": "ichava/icon-sets-bootstrap", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "bootstrap", + "bootstrap-icons", + "laravel", + "ichava" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Bootstrap Icons", + "package_name": "ichava/icon-sets-bootstrap", + "prefix": "bi", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-bootstrap", + "theme_variants": [], + "total": 0, + "variant_metadata": [], + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/carbon": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/carbon", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "IBM's Carbon Design System icons: the full set at 32 px and the 16 px redraws.", + "homepage": "https://carbondesignsystem.com/elements/icons/library/", + "icon_set_name": "ichava/icon-sets-carbon", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "carbon", + "ibm", + "carbon-design-system" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT (code) / Apache-2.0 (icons)", + "name": "Carbon Icons", + "package_name": "ichava/icon-sets-carbon", + "prefix": "carbon", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-carbon", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "size-32": { + "name": "32 px", + "slug": "size-32", + "description": "The full set, drawn on the 32 px grid", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "preview_icon": "add", + "color_scheme": "fill-based" + }, + "size-16": { + "name": "16 px", + "slug": "size-16", + "description": "Glyphs redrawn for 16 px, where upstream ships one", + "default": false, + "icon_suffix": "", + "display_order": 2, + "attributes": [], + "preview_icon": "add", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/emoji": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/emoji", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "Twemoji, Twitter/X's open-source emoji set, as SVG. Unicode 17 / Emoji 17 spec. Categorised by Unicode CLDR groups. OpenMoji is in ichava/icon-sets-openmoji.", + "homepage": null, + "icon_set_name": "ichava/icon-sets-emoji", + "item_kinds": [], + "keywords": [ + "emoji", + "twemoji", + "svg", + "unicode", + "bundle" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT (code) / CC-BY 4.0 (Twemoji assets)", + "name": "Emoji Sets", + "package_name": "ichava/icon-sets-emoji", + "prefix": "emoji", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-emoji", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "default": { + "name": "Default", + "slug": "default", + "description": "Standard Twemoji style", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "color_scheme": "adaptive" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/extras": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/extras", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "20 smaller open-licence icon, logo and emoji sets in one pack, one directory per set, each under its own licence (MIT, Apache-2.0, CC0-1.0 or CC BY).", + "homepage": "https://github.com/ichava/icon-sets-extras#readme", + "icon_set_name": "ichava/icon-sets-extras", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "emoji", + "logos", + "bundle", + "open-source" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT (code); icons per set, see ATTRIBUTION.md", + "name": "Ichava Extras", + "package_name": "ichava/icon-sets-extras", + "prefix": "extras", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-extras", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "default": { + "name": "Default", + "slug": "default", + "description": "Each set's own style", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "color_scheme": "adaptive" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/flag": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/flag", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "271 country flag SVGs sourced from lipis/flag-icons, available in 4x3 and 1x1 aspect ratios. Replaces the flag half of the deprecated simtabi/laflamoji.", + "homepage": "https://github.com/lipis/flag-icons", + "icon_set_name": "ichava/icon-sets-flag", + "item_kinds": [], + "keywords": [ + "flags", + "country-flags", + "svg", + "lipis", + "iso-3166" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Flag Icons", + "package_name": "ichava/icon-sets-flag", + "prefix": "flag", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-flag", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "4x3": { + "name": "4:3", + "slug": "4x3", + "description": "Canonical 4:3 aspect ratio. Best fit for most flags.", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [] + }, + "1x1": { + "name": "1:1", + "slug": "1x1", + "description": "Square 1:1 aspect ratio. Useful for badges, round avatars, picker grids.", + "default": false, + "icon_suffix": "", + "display_order": 2, + "attributes": [] + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/fluent": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/fluent", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "Microsoft's Fluent UI System Icons at 24 px, in regular and filled.", + "homepage": "https://github.com/microsoft/fluentui-system-icons", + "icon_set_name": "ichava/icon-sets-fluent", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "fluent", + "fluentui", + "microsoft" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Fluent UI System Icons", + "package_name": "ichava/icon-sets-fluent", + "prefix": "fluent", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-fluent", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "regular": { + "name": "Regular", + "slug": "regular", + "description": "Outlined style at 24 px", + "default": true, + "icon_suffix": "_24_regular", + "display_order": 1, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + }, + "filled": { + "name": "Filled", + "slug": "filled", + "description": "Solid style at 24 px", + "default": false, + "icon_suffix": "_24_filled", + "display_order": 2, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/font-awesome-free": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/font-awesome-free", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "2,883 Font Awesome Free icons (2,001 solid, 273 regular, 609 brands) by @fontawesome, vendored from Font Awesome 7.3.1. Icons CC BY 4.0, attribution required.", + "homepage": "https://fontawesome.com/", + "icon_set_name": "ichava/icon-sets-font-awesome-free", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "font-awesome", + "fontawesome", + "cc-by-4.0", + "brands" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT (code) / CC-BY-4.0 (Font Awesome Free icons)", + "name": "Font Awesome Free", + "package_name": "ichava/icon-sets-font-awesome-free", + "prefix": "fa", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-font-awesome-free", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "solid": { + "name": "Solid", + "slug": "solid", + "description": "Filled glyphs. The largest Free style.", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "preview_icon": "house", + "color_scheme": "fill-based" + }, + "regular": { + "name": "Regular", + "slug": "regular", + "description": "Outlined glyphs. Free covers only part of the Regular style.", + "default": false, + "icon_suffix": "", + "display_order": 2, + "attributes": [], + "preview_icon": "house", + "color_scheme": "fill-based" + }, + "brands": { + "name": "Brands", + "slug": "brands", + "description": "Logos of companies, products and services. Trademarks of their owners.", + "default": false, + "icon_suffix": "", + "display_order": 3, + "attributes": [], + "preview_icon": "house", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/heroicons": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/heroicons", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "1,288 Heroicons from v2.2.0 in four variants: outline and solid (24px), mini (20px) and micro (16px).", + "homepage": "https://heroicons.com/", + "icon_set_name": "ichava/icon-sets-heroicons", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "heroicons", + "tailwind", + "outline", + "solid", + "laravel", + "ichava" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Heroicons", + "package_name": "ichava/icon-sets-heroicons", + "prefix": "heroicon", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-heroicons", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "outline": { + "name": "Outline", + "slug": "outline", + "description": "24px, 1.5px stroke", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": { + "fill": "none", + "stroke": "currentColor", + "stroke_width": 1.5 + } + }, + "solid": { + "name": "Solid", + "slug": "solid", + "description": "24px, filled", + "default": false, + "icon_suffix": "", + "display_order": 2, + "attributes": { + "fill": "currentColor" + } + }, + "mini": { + "name": "Mini", + "slug": "mini", + "description": "20px, filled, for smaller UI", + "default": false, + "icon_suffix": "", + "display_order": 3, + "attributes": { + "fill": "currentColor" + } + }, + "micro": { + "name": "Micro", + "slug": "micro", + "description": "16px, filled, for dense UI", + "default": false, + "icon_suffix": "", + "display_order": 4, + "attributes": { + "fill": "currentColor" + } + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/iconoir": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/iconoir", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "An open-source library of 24px stroke icons with a smaller solid set, drawn on a 1.5px stroke.", + "homepage": "https://iconoir.com/", + "icon_set_name": "ichava/icon-sets-iconoir", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "iconoir", + "stroke", + "solid", + "open-source" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Iconoir", + "package_name": "ichava/icon-sets-iconoir", + "prefix": "iconoir", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-iconoir", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "regular": { + "name": "Regular", + "slug": "regular", + "description": "Stroked icons on a 1.5px line, transparent fill", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": { + "stroke_width": 1.5, + "fill": "none", + "stroke": "currentColor" + }, + "preview_icon": "heart", + "color_scheme": "stroke-based" + }, + "solid": { + "name": "Solid", + "slug": "solid", + "description": "Solid icons filled with the current colour", + "default": false, + "icon_suffix": "", + "display_order": 2, + "attributes": { + "fill": "currentColor", + "stroke": "none" + }, + "preview_icon": "heart", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/ionicons": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/ionicons", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "Ionic's Ionicons in filled, outline and sharp, with the logo icons under filled.", + "homepage": "https://ionic.io/ionicons", + "icon_set_name": "ichava/icon-sets-ionicons", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "ionicons", + "ionic" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Ionicons", + "package_name": "ichava/icon-sets-ionicons", + "prefix": "ion", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-ionicons", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "filled": { + "name": "Filled", + "slug": "filled", + "description": "Solid style, and the 94 logo icons", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + }, + "outline": { + "name": "Outline", + "slug": "outline", + "description": "Stroked style", + "default": false, + "icon_suffix": "-outline", + "display_order": 2, + "attributes": [], + "preview_icon": "home", + "color_scheme": "stroke-based" + }, + "sharp": { + "name": "Sharp", + "slug": "sharp", + "description": "Solid style with square corners", + "default": false, + "icon_suffix": "-sharp", + "display_order": 3, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/lucide": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/lucide", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "2,313 Lucide icons: one 24px stroke style, vendored from lucide-static 1.68.0. ISC, with MIT for the Feather-derived icons.", + "homepage": "https://lucide.dev/", + "icon_set_name": "ichava/icon-sets-lucide", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "lucide", + "feather", + "stroke", + "laravel", + "ichava" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "ISC", + "name": "Lucide Icons", + "package_name": "ichava/icon-sets-lucide", + "prefix": "lucide", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-lucide", + "theme_variants": [], + "total": 0, + "variant_metadata": [], + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/material-symbols": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/material-symbols", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "Google's Material Symbols at weight 400: 3,940 icons in outlined, rounded and sharp, each unfilled and filled.", + "homepage": "https://fonts.google.com/icons", + "icon_set_name": "ichava/icon-sets-material-symbols", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "material", + "material-symbols", + "google" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT (code) / Apache-2.0 (icons)", + "name": "Material Symbols", + "package_name": "ichava/icon-sets-material-symbols", + "prefix": "material-symbols", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-material-symbols", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "outlined": { + "name": "Outlined", + "slug": "outlined", + "description": "Outlined style, weight 400, unfilled", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + }, + "outlined-fill": { + "name": "Outlined filled", + "slug": "outlined-fill", + "description": "Outlined style, weight 400, filled", + "default": false, + "icon_suffix": "-fill", + "display_order": 2, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + }, + "rounded": { + "name": "Rounded", + "slug": "rounded", + "description": "Rounded style, weight 400, unfilled", + "default": false, + "icon_suffix": "", + "display_order": 3, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + }, + "rounded-fill": { + "name": "Rounded filled", + "slug": "rounded-fill", + "description": "Rounded style, weight 400, filled", + "default": false, + "icon_suffix": "-fill", + "display_order": 4, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + }, + "sharp": { + "name": "Sharp", + "slug": "sharp", + "description": "Sharp style, weight 400, unfilled", + "default": false, + "icon_suffix": "", + "display_order": 5, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + }, + "sharp-fill": { + "name": "Sharp filled", + "slug": "sharp-fill", + "description": "Sharp style, weight 400, filled", + "default": false, + "icon_suffix": "-fill", + "display_order": 6, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/mdi": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/mdi", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "7,447 Material Design Icons by Pictogrammers, one 24px style, vendored from @mdi/svg 7.4.47.", + "homepage": "https://pictogrammers.com/library/mdi/", + "icon_set_name": "ichava/icon-sets-mdi", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "mdi", + "material-design-icons", + "pictogrammers", + "apache-2.0" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT (code) / Apache-2.0 (Material Design Icons assets)", + "name": "Material Design Icons", + "package_name": "ichava/icon-sets-mdi", + "prefix": "mdi", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-mdi", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "default": { + "name": "Default", + "slug": "default", + "description": "The one style: 24px filled glyphs. Outlined designs are separate icons whose names end in -outline.", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "preview_icon": "home", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/octicons": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/octicons", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "GitHub's Primer icon set at two optical sizes, 16px and 24px. The GitHub logos and product marks are excluded: they are trademarks, not part of the MIT grant.", + "homepage": "https://primer.style/octicons/", + "icon_set_name": "ichava/icon-sets-octicons", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "octicons", + "primer", + "github" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Octicons", + "package_name": "ichava/icon-sets-octicons", + "prefix": "octicon", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-octicons", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "16px": { + "name": "16px", + "slug": "16px", + "description": "Drawn for a 16 by 16 grid, the size GitHub uses in dense UI", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": { + "fill": "currentColor" + }, + "preview_icon": "home", + "color_scheme": "fill-based" + }, + "24px": { + "name": "24px", + "slug": "24px", + "description": "Drawn for a 24 by 24 grid, for larger touch targets", + "default": false, + "icon_suffix": "", + "display_order": 2, + "attributes": { + "fill": "currentColor" + }, + "preview_icon": "home", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/openmoji": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/openmoji", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "OpenMoji emoji in two sets (colour and black outline), Unicode emoji 16.0, grouped by Unicode CLDR category. Moved verbatim from ichava/icon-sets-emoji.", + "homepage": "https://openmoji.org/", + "icon_set_name": "ichava/icon-sets-openmoji", + "item_kinds": [], + "keywords": [ + "emoji", + "openmoji", + "svg", + "unicode" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT (code) / CC-BY-SA 4.0 (OpenMoji assets)", + "name": "OpenMoji", + "package_name": "ichava/icon-sets-openmoji", + "prefix": "openmoji", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-openmoji", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "default": { + "name": "Default", + "slug": "default", + "description": "OpenMoji's own style in each set", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "color_scheme": "adaptive" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/phosphor": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/phosphor", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "9,072 Phosphor icons: 1,512 designs in six weights (regular, thin, light, bold, fill, duotone), vendored from @phosphor-icons/core 2.1.1.", + "homepage": "https://phosphoricons.com/", + "icon_set_name": "ichava/icon-sets-phosphor", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "phosphor", + "phosphor-icons", + "duotone" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Phosphor Icons", + "package_name": "ichava/icon-sets-phosphor", + "prefix": "ph", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-phosphor", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "regular": { + "name": "Regular", + "slug": "regular", + "description": "The default line weight.", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "preview_icon": "house", + "color_scheme": "fill-based" + }, + "thin": { + "name": "Thin", + "slug": "thin", + "description": "The lightest weight.", + "default": false, + "icon_suffix": "-thin", + "display_order": 2, + "attributes": [], + "preview_icon": "house", + "color_scheme": "fill-based" + }, + "light": { + "name": "Light", + "slug": "light", + "description": "Between thin and regular.", + "default": false, + "icon_suffix": "-light", + "display_order": 3, + "attributes": [], + "preview_icon": "house", + "color_scheme": "fill-based" + }, + "bold": { + "name": "Bold", + "slug": "bold", + "description": "The heaviest line weight.", + "default": false, + "icon_suffix": "-bold", + "display_order": 4, + "attributes": [], + "preview_icon": "house", + "color_scheme": "fill-based" + }, + "fill": { + "name": "Fill", + "slug": "fill", + "description": "Solid shapes.", + "default": false, + "icon_suffix": "-fill", + "display_order": 5, + "attributes": [], + "preview_icon": "house", + "color_scheme": "fill-based" + }, + "duotone": { + "name": "Duotone", + "slug": "duotone", + "description": "A line layer over a 20% opacity fill layer, both in currentColor.", + "default": false, + "icon_suffix": "-duotone", + "display_order": 6, + "attributes": [], + "preview_icon": "house", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/remix": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/remix", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "3,188 Remix Icon SVGs in 20 categories, line and fill designs, pinned to Remix Icon v4.8.0, the last release under Apache-2.0.", + "homepage": "https://remixicon.com/", + "icon_set_name": "ichava/icon-sets-remix", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "remix", + "remixicon", + "apache-2.0" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT (code) / Apache-2.0 (Remix Icon v4.8.0 assets)", + "name": "Remix Icon", + "package_name": "ichava/icon-sets-remix", + "prefix": "ri", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-remix", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "default": { + "name": "Default", + "slug": "default", + "description": "Remix Icon's own style. Line and fill designs are separate icons, named -line and -fill.", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": [], + "preview_icon": "home-line", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/simple-icons": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/simple-icons", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "3,255 single-colour brand glyphs from Simple Icons 16.36.0. Artwork CC0-1.0; the brands are their owners' trademarks. 212 icons whose upstream data names another licence are left out.", + "homepage": "https://simpleicons.org/", + "icon_set_name": "ichava/icon-sets-simple-icons", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "simple-icons", + "brands", + "logos", + "laravel", + "ichava" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "CC0-1.0", + "name": "Simple Icons", + "package_name": "ichava/icon-sets-simple-icons", + "prefix": "si", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-simple-icons", + "theme_variants": [], + "total": 0, + "variant_metadata": [], + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/RegistrationGolden/tabler": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/RegistrationGolden/tabler", + "blade_component": null, + "category_kinds": [], + "declared_kind": "icon", + "description": "Over 5,200 pixel-perfect icons for web apps. Free and open source icons designed with attention to detail to make your design stand out.", + "homepage": "https://tabler-icons.io/", + "icon_set_name": "ichava/icon-sets-tabler", + "item_kinds": [], + "keywords": [ + "icons", + "svg", + "tabler", + "outline", + "filled", + "open-source", + "pixel-perfect" + ], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Tabler Icons", + "package_name": "ichava/icon-sets-tabler", + "prefix": "ti", + "provider_class": null, + "repository": "https://github.com/ichava/icon-sets-tabler", + "theme_variants": [], + "total": 0, + "variant_metadata": { + "outline": { + "name": "Outline", + "slug": "outline", + "description": "Outlined icon style with 2px stroke width", + "default": true, + "icon_suffix": "-outline", + "display_order": 1, + "attributes": { + "stroke_width": 2, + "fill": "none", + "stroke": "currentColor" + }, + "preview_icon": "home-outline", + "color_scheme": "stroke-based" + }, + "filled": { + "name": "Filled", + "slug": "filled", + "description": "Solid filled icon style", + "default": false, + "icon_suffix": "-filled", + "display_order": 2, + "attributes": { + "fill": "currentColor", + "stroke": "none" + }, + "preview_icon": "home-filled", + "color_scheme": "fill-based" + } + }, + "vendor": "ichava", + "version": "0.1.0" + } + }, + "/tests/fixtures/TranslatedPack/resources/assets/svg": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/TranslatedPack/resources/assets/svg", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "Canonical description, from config.json", + "homepage": null, + "icon_set_name": "ichava/fixture-pack", + "item_kinds": [], + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Fixture Pack", + "package_name": "ichava/fixture-pack", + "prefix": "fx", + "provider_class": null, + "repository": null, + "theme_variants": [], + "total": 1, + "variant_metadata": [], + "vendor": "ichava", + "version": "1.0.0" + } + }, + "/tests/fixtures/VariantPack": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/VariantPack", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "Two variants declared out of display order.", + "homepage": null, + "icon_set_name": "ichava-fixtures/variant-pack", + "item_kinds": [], + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "MIT", + "name": "Variant fixture pack", + "package_name": "ichava-fixtures/variant-pack", + "prefix": "varfix", + "provider_class": null, + "repository": null, + "theme_variants": [], + "total": 2, + "variant_metadata": { + "filled": { + "name": "Filled", + "description": "Solid shapes", + "display_order": 2, + "attributes": { + "fill": "currentColor" + }, + "preview_icon": "filled/home", + "color_scheme": "fill-based" + }, + "outline": { + "name": "Outline", + "description": "Stroked shapes", + "default": true, + "display_order": 1, + "attributes": { + "stroke_width": 2, + "fill": "none" + }, + "preview_icon": "outline/home", + "color_scheme": "stroke-based" + } + }, + "vendor": "ichava-fixtures", + "version": "1.0.0" + } + }, + "/tests/fixtures/ViewfulPack/resources/assets/svg": { + "aliases": [], + "metadata": { + "base_path": "/tests/fixtures/ViewfulPack/resources/assets/svg", + "blade_component": null, + "category_kinds": [], + "declared_kind": null, + "description": "Ships a Blade template.", + "homepage": null, + "icon_set_name": "ichava/viewful-pack", + "item_kinds": [], + "keywords": [], + "kind": "icon", + "kind_issues": [], + "kind_rules": [], + "labels": [], + "license": "Commercial", + "name": "Viewful Pack", + "package_name": "ichava/viewful-pack", + "prefix": "vf", + "provider_class": null, + "repository": null, + "theme_variants": [], + "total": 1, + "variant_metadata": [], + "vendor": "ichava", + "version": "1.0.0" + } + } +} diff --git a/tests/fixtures/RegistrationGolden/bootstrap/config.json b/tests/fixtures/RegistrationGolden/bootstrap/config.json new file mode 100644 index 0000000..c7f6fa9 --- /dev/null +++ b/tests/fixtures/RegistrationGolden/bootstrap/config.json @@ -0,0 +1,71 @@ +{ + "schema_version": "1.0", + "package": { + "name": "ichava/icon-sets-bootstrap", + "title": "Bootstrap Icons", + "description": "2,078 Bootstrap Icons from v1.13.2, 16px, flat; filled forms keep their -fill suffix.", + "keywords": [ + "icons", + "svg", + "bootstrap", + "bootstrap-icons", + "laravel", + "ichava" + ], + "version": "0.1.0", + "upstream_version": "1.13.2", + "type": "single", + "kind": "icon", + "license": "MIT" + }, + "metadata": { + "authors": [ + { + "name": "The Bootstrap Authors", + "role": "Creator -- twbs/icons", + "url": "https://icons.getbootstrap.com/" + } + ], + "homepage": "https://icons.getbootstrap.com/", + "repository": "https://github.com/ichava/icon-sets-bootstrap", + "data": { + "variants": {}, + "categories": {}, + "kinds": { + "rules": [] + }, + "items": {} + } + }, + "config": { + "icon_prefix": "bi" + }, + "upstream": { + "source": { + "type": "npm", + "package": "bootstrap-icons", + "source_path": "icons" + }, + "current_version": "1.13.2", + "version_check_url": "https://registry.npmjs.org/bootstrap-icons/latest", + "license": "MIT", + "cdn": { + "jsdelivr": "https://cdn.jsdelivr.net/npm/bootstrap-icons@{version}/icons/{name}.svg", + "unpkg": "https://unpkg.com/bootstrap-icons@{version}/icons/{name}.svg" + }, + "update_command": { + "type": "npm", + "package": "bootstrap-icons", + "source_path": "icons", + "_note": "Run through ichava/icon-sets-sync (pack recipe icon-sets-bootstrap)." + } + }, + "license": { + "spdx": "MIT", + "file": "resources/assets/svg/licenses/bootstrap/LICENSE", + "upstream_path": "LICENSE", + "sha256": "0fb3e11bd57e896c5a512afd64864d28a37de45d19835016c87ca1ad19ead969", + "checked_at": "2026-11-04", + "checked_version": "1.13.2" + } +} diff --git a/tests/fixtures/RegistrationGolden/bootstrap/files/.gitkeep b/tests/fixtures/RegistrationGolden/bootstrap/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/RegistrationGolden/carbon/config.json b/tests/fixtures/RegistrationGolden/carbon/config.json new file mode 100644 index 0000000..a06541d --- /dev/null +++ b/tests/fixtures/RegistrationGolden/carbon/config.json @@ -0,0 +1,90 @@ +{ + "schema_version": "1.0", + "package": { + "name": "ichava/icon-sets-carbon", + "title": "Carbon Icons", + "description": "IBM's Carbon Design System icons: the full set at 32 px and the 16 px redraws.", + "keywords": [ + "icons", + "svg", + "carbon", + "ibm", + "carbon-design-system" + ], + "version": "0.1.0", + "upstream_version": "11.91.0", + "type": "multi", + "kind": "icon", + "license": "MIT (code) / Apache-2.0 (icons)" + }, + "metadata": { + "authors": [ + { + "name": "IBM Corp.", + "role": "Creator -- Carbon Design System icons", + "url": "https://github.com/carbon-design-system/carbon" + } + ], + "homepage": "https://carbondesignsystem.com/elements/icons/library/", + "repository": "https://github.com/ichava/icon-sets-carbon", + "data": { + "variants": { + "size-32": { + "name": "32 px", + "slug": "size-32", + "description": "The full set, drawn on the 32 px grid", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": {}, + "preview_icon": "add", + "color_scheme": "fill-based" + }, + "size-16": { + "name": "16 px", + "slug": "size-16", + "description": "Glyphs redrawn for 16 px, where upstream ships one", + "default": false, + "icon_suffix": "", + "display_order": 2, + "attributes": {}, + "preview_icon": "add", + "color_scheme": "fill-based" + } + }, + "categories": {}, + "kinds": { + "rules": [] + }, + "items": {} + } + }, + "config": { + "icon_prefix": "carbon" + }, + "license": { + "spdx": "Apache-2.0", + "sha256": "037b3e9eaff51477b270e9082f54b99175e4dd893548654f00995045b68294b8", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/carbon/LICENSE", + "source": "https://github.com/carbon-design-system/carbon/blob/v11.119.0/LICENSE", + "notice": "resources/assets/svg/NOTICE" + }, + "upstream": { + "source": { + "type": "npm", + "package": "@carbon/icons", + "_ref": "v11.119.0", + "_repository": "https://github.com/carbon-design-system/carbon" + }, + "current_version": "11.91.0", + "version_check_url": "https://registry.npmjs.org/@carbon/icons/latest", + "license": "Apache-2.0", + "update_command": { + "type": "recipe", + "recipe": "icon-sets-carbon", + "_note": "icon-sets-sync has no config/icon-sets-carbon.json yet; the proposed recipe is pack-recipes/icon-sets-carbon.json in the ichava design folder, and lands with the one icon-sets-sync pull request that adds the v0.1.0 packs. Until then the weekly sync-upstream run has nothing to run." + }, + "_subset": "Sizes 32 and 16 only. Dropped: size 20 (9 files), size 24 (8 files) and the 18 unsized status glyphs at the root of `svg/`. Names are slugged (`chevron--down` is `chevron-down`); the `watson-health/` and `Q/` namespaces become name prefixes (`watson-health-study-view`, `q-h`). No two names collide after slugging." + } +} diff --git a/tests/fixtures/RegistrationGolden/carbon/files/.gitkeep b/tests/fixtures/RegistrationGolden/carbon/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/RegistrationGolden/emoji/config.json b/tests/fixtures/RegistrationGolden/emoji/config.json new file mode 100644 index 0000000..79d4530 --- /dev/null +++ b/tests/fixtures/RegistrationGolden/emoji/config.json @@ -0,0 +1,84 @@ +{ + "schema_version": "1.0", + "package": { + "name": "ichava/icon-sets-emoji", + "title": "Emoji Sets", + "description": "Twemoji, Twitter/X's open-source emoji set, as SVG. Unicode 17 / Emoji 17 spec. Categorised by Unicode CLDR groups. OpenMoji is in ichava/icon-sets-openmoji.", + "keywords": [ + "emoji", + "twemoji", + "svg", + "unicode", + "bundle" + ], + "version": "0.1.0", + "upstream_version": "15.0.0", + "type": "bulk", + "kind": "icon", + "license": "MIT (code) / CC-BY 4.0 (Twemoji assets)" + }, + "metadata": { + "authors": [ + { + "name": "Twitter, Inc. and other contributors (Twemoji)", + "role": "Creator -- Twemoji", + "url": "https://github.com/jdecked/twemoji" + } + ], + "repository": "https://github.com/ichava/icon-sets-emoji", + "data": { + "variants": { + "default": { + "name": "Default", + "slug": "default", + "description": "Standard Twemoji style", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": {}, + "color_scheme": "adaptive" + } + }, + "categories": { + "twemoji": { + "name": "Twemoji", + "description": "Twitter/X's open-source emoji set (CC-BY 4.0). Recognisable rounded colourful style. Default set.", + "sets": [ + "twemoji" + ] + } + }, + "kinds": { + "rules": [] + }, + "items": {} + } + }, + "config": { + "icon_prefix": "emoji" + }, + "upstream": { + "_comment": "Each emoji set has its own upstream; Twemoji is the only one.", + "source": { + "type": "npm", + "package": "@twemoji/svg", + "_why_npm": "NPM publishes new Twemoji releases the same day as GitHub but with no rate-limit ceiling and a simpler API contract." + }, + "current_version": "15.0.0", + "twemoji_github_tag": "v15.1.0", + "version_check_url": "https://registry.npmjs.org/@twemoji/svg/latest", + "license": "CC-BY 4.0 (Twemoji assets)", + "cdn": { + "twemoji_jsdelivr": "https://cdn.jsdelivr.net/npm/@twemoji/svg@{version}/{codepoint}.svg", + "twemoji_unpkg": "https://unpkg.com/@twemoji/svg@{version}/{codepoint}.svg", + "twemoji_github_raw": "https://raw.githubusercontent.com/jdecked/twemoji/{twemoji_github_tag}/assets/svg/{codepoint}.svg" + }, + "update_command": { + "_note": "Refresh runs in ichava/icon-sets-sync via the `emoji-sets` recipe -- Twemoji (npm) + Unicode CLDR. No pack-side script.", + "type": "recipe", + "recipe": "emoji-sets" + }, + "_vendored_note": "Versions of the upstreams that produced the shipped assets. They move independently and conflating them is what left this pack empty: current_version read 17.0.0, which @twemoji/svg has never published, so `npm pack` failed and no assets were ever written (V4). Twemoji 15.0.0 is npm's latest; Unicode emoji 16.0 is the newest emoji-test.txt that exists and is what the CLDR categorisation used. The jdecked/twemoji repository tags independently of the npm package: npm's 15.0.0 has no v15.0.0 tag, only v15.1.0. `twemoji_github_raw` therefore carries `{twemoji_github_tag}` rather than `{version}` -- a caller substituting only `{version}` leaves a visible placeholder in the URL instead of building a plausible v15.0.0 that 404s.", + "cldr_version": "16.0" + } +} diff --git a/tests/fixtures/RegistrationGolden/emoji/files/.gitkeep b/tests/fixtures/RegistrationGolden/emoji/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/RegistrationGolden/extras/config.json b/tests/fixtures/RegistrationGolden/extras/config.json new file mode 100644 index 0000000..fa692bd --- /dev/null +++ b/tests/fixtures/RegistrationGolden/extras/config.json @@ -0,0 +1,1020 @@ +{ + "schema_version": "1.0", + "package": { + "name": "ichava/icon-sets-extras", + "title": "Ichava Extras", + "description": "20 smaller open-licence icon, logo and emoji sets in one pack, one directory per set, each under its own licence (MIT, Apache-2.0, CC0-1.0 or CC BY).", + "keywords": [ + "icons", + "svg", + "emoji", + "logos", + "bundle", + "open-source" + ], + "version": "0.1.0", + "upstream_version": "per set: upstream.sets..current_version", + "type": "bulk", + "kind": "icon", + "license": "MIT (code); icons per set, see ATTRIBUTION.md" + }, + "metadata": { + "authors": [ + { + "name": "WorkOS (Radix)", + "role": "Creator -- Radix Icons", + "url": "https://www.radix-ui.com/icons" + }, + { + "name": "Cole Bemis", + "role": "Creator -- Feather", + "url": "https://feathericons.com" + }, + { + "name": "Aniket Suvarna (Atisa)", + "role": "Creator -- Boxicons", + "url": "https://boxicons.com" + }, + { + "name": "ByteDance", + "role": "Creator -- IconPark", + "url": "https://iconpark.oceanengine.com" + }, + { + "name": "MingCute Design", + "role": "Creator -- MingCute", + "url": "https://www.mingcute.com" + }, + { + "name": "TDesign (Tencent)", + "role": "Creator -- TDesign Icons", + "url": "https://tdesign.tencent.com" + }, + { + "name": "Siemens AG", + "role": "Creator -- Siemens iX Icons", + "url": "https://ix.siemens.io" + }, + { + "name": "Zendesk", + "role": "Creator -- Zendesk Garden Icons", + "url": "https://garden.zendesk.com" + }, + { + "name": "Kryston Schwarze", + "role": "Creator -- coolicons", + "url": "https://coolicons.cool" + }, + { + "name": "Akveo", + "role": "Creator -- Eva Icons", + "url": "https://akveo.github.io/eva-icons" + }, + { + "name": "Ant UED", + "role": "Creator -- Ant Design Icons", + "url": "https://ant.design/components/icon" + }, + { + "name": "Resolve to Save Lives and the Health Icons contributors", + "role": "Creator -- Health Icons", + "url": "https://healthicons.org" + }, + { + "name": "konpa and the Devicon contributors", + "role": "Creator -- Devicon", + "url": "https://devicon.dev" + }, + { + "name": "Microsoft Corporation", + "role": "Creator -- Fluent Emoji", + "url": "https://github.com/microsoft/fluentui-emoji" + }, + { + "name": "CyCraft", + "role": "Creator -- Pepicons", + "url": "https://pepicons.com" + }, + { + "name": "Lorc, Delapouite and the game-icons.net contributors", + "role": "Creator -- Game Icons", + "url": "https://game-icons.net" + }, + { + "name": "CoreUI (creativeLabs)", + "role": "Creator -- CoreUI Icons Free", + "url": "https://icons.coreui.io" + }, + { + "name": "Wayside Mapping and the Pinhead contributors", + "role": "Creator -- Pinhead", + "url": "https://pinhead.ink" + }, + { + "name": "Gil Barbara", + "role": "Creator -- SVG Logos", + "url": "https://svgporn.com" + }, + { + "name": "Google", + "role": "Creator -- Noto Emoji", + "url": "https://fonts.google.com/noto/specimen/Noto+Color+Emoji" + } + ], + "homepage": "https://github.com/ichava/icon-sets-extras#readme", + "repository": "https://github.com/ichava/icon-sets-extras", + "data": { + "variants": { + "default": { + "name": "Default", + "slug": "default", + "description": "Each set's own style", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": {}, + "color_scheme": "adaptive" + } + }, + "categories": { + "radix-icons": { + "name": "Radix Icons", + "description": "Radix Icons by WorkOS (Radix) (MIT). 15×15, filled.", + "sets": [ + "radix-icons" + ] + }, + "feather": { + "name": "Feather", + "description": "Feather by Cole Bemis (MIT). 24×24, 2px stroke.", + "sets": [ + "feather" + ] + }, + "boxicons": { + "name": "Boxicons", + "description": "Boxicons by Aniket Suvarna (Atisa) (MIT). 24×24; regular (bx-), solid (bxs-), logos (bxl-).", + "sets": [ + "boxicons" + ] + }, + "iconpark": { + "name": "IconPark", + "description": "IconPark by ByteDance (Apache-2.0). 48×48 outline, four themes in the source.", + "sets": [ + "iconpark" + ] + }, + "mingcute": { + "name": "MingCute", + "description": "MingCute by MingCute Design (Apache-2.0). 24×24, regular (line) and filled.", + "sets": [ + "mingcute" + ] + }, + "tdesign": { + "name": "TDesign Icons", + "description": "TDesign Icons by TDesign (Tencent) (MIT). 24×24.", + "sets": [ + "tdesign" + ] + }, + "siemens-ix": { + "name": "Siemens iX Icons", + "description": "Siemens iX Icons by Siemens AG (MIT). Mixed sizes.", + "sets": [ + "siemens-ix" + ] + }, + "garden": { + "name": "Zendesk Garden Icons", + "description": "Zendesk Garden Icons by Zendesk (Apache-2.0). 16×16 and 12×12.", + "sets": [ + "garden" + ] + }, + "coolicons": { + "name": "coolicons", + "description": "coolicons by Kryston Schwarze (MIT). 24×24, line and fill (regular corners).", + "sets": [ + "coolicons" + ] + }, + "eva": { + "name": "Eva Icons", + "description": "Eva Icons by Akveo (MIT). 24×24, fill and outline.", + "sets": [ + "eva" + ] + }, + "ant-design": { + "name": "Ant Design Icons", + "description": "Ant Design Icons by Ant UED (MIT). 1024×1024, outlined, filled and two-tone.", + "sets": [ + "ant-design" + ] + }, + "health-icons": { + "name": "Health Icons", + "description": "Health Icons by Resolve to Save Lives and the Health Icons contributors (CC0-1.0). 48×48, outline and filled.", + "sets": [ + "health-icons" + ] + }, + "devicon": { + "name": "Devicon", + "description": "Devicon by konpa and the Devicon contributors (MIT). 128×128 logos; original, plain, line and wordmark.", + "sets": [ + "devicon" + ] + }, + "fluent-emoji": { + "name": "Fluent Emoji", + "description": "Fluent Emoji by Microsoft Corporation (MIT). Flat and high-contrast, with skin tones.", + "sets": [ + "fluent-emoji" + ] + }, + "pepicons": { + "name": "Pepicons", + "description": "Pepicons by CyCraft (CC BY 4.0). 20×20; pop, print and pencil.", + "sets": [ + "pepicons" + ] + }, + "game-icons": { + "name": "Game Icons", + "description": "Game Icons by Lorc, Delapouite and the game-icons.net contributors (CC BY 3.0 (CC0 for two contributors)). 512×512, white on a black square.", + "sets": [ + "game-icons" + ] + }, + "coreui": { + "name": "CoreUI Icons Free", + "description": "CoreUI Icons Free by CoreUI (creativeLabs) (CC BY 4.0). 512×512 line icons.", + "sets": [ + "coreui" + ] + }, + "pinhead": { + "name": "Pinhead", + "description": "Pinhead by Wayside Mapping and the Pinhead contributors (CC0-1.0). 15×15 map icons.", + "sets": [ + "pinhead" + ] + }, + "svg-logos": { + "name": "SVG Logos", + "description": "SVG Logos by Gil Barbara (CC0-1.0). Full-colour brand logos.", + "sets": [ + "svg-logos" + ] + }, + "noto-emoji": { + "name": "Noto Emoji", + "description": "Noto Emoji by Google (Apache-2.0). Colour emoji, 128×128.", + "sets": [ + "noto-emoji" + ] + } + }, + "kinds": { + "rules": [] + }, + "items": {} + } + }, + "config": { + "icon_prefix": "extras" + }, + "upstream": { + "source": { + "type": "manifest", + "_note": "One upstream per set, under upstream.sets. Each has its own recipe in the design folder (pack-recipes/icon-sets-extras..json)." + }, + "license": "per set", + "sets": { + "radix-icons": { + "name": "Radix Icons", + "owner": "WorkOS (Radix)", + "homepage": "https://www.radix-ui.com/icons", + "repository": "https://github.com/radix-ui/icons", + "source": { + "type": "github-archive", + "owner": "radix-ui", + "repo": "icons", + "archive_url": "https://github.com/radix-ui/icons/archive/refs/tags/website@{version}.tar.gz", + "source_path": "packages/radix-icons/icons", + "_version_note": "archive tag is website@0.0.17; the icons package inside it is 1.3.2" + }, + "current_version": "0.0.17", + "ref": "tag website@0.0.17 (bde33b13aa), packages/radix-icons 1.3.2", + "version_check_url": "https://api.github.com/repos/radix-ui/icons/tags", + "license": { + "spdx": "MIT", + "sha256": "0e80a2d229d2fd4fc7e8636142ec5d0ff0bc031f14c15b682e2ac01dfd5b5138", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/radix-icons/LICENSE", + "read_from": "LICENSE at tag website@0.0.17" + }, + "attribution_required": false, + "trademarks": false, + "icons": 318, + "layout": "packages/radix-icons/icons/*.svg, unchanged names" + }, + "feather": { + "name": "Feather", + "owner": "Cole Bemis", + "homepage": "https://feathericons.com", + "repository": "https://github.com/feathericons/feather", + "source": { + "type": "npm", + "package": "feather-icons", + "source_path": "dist/icons" + }, + "current_version": "4.29.2", + "ref": "npm feather-icons 4.29.2", + "version_check_url": "https://registry.npmjs.org/feather-icons/latest", + "license": { + "spdx": "MIT", + "sha256": "308028e93fcf84972523cdf6e616f73168546b4953895f516d01287f16fe7bee", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/feather/LICENSE", + "read_from": "LICENSE in the npm package" + }, + "attribution_required": false, + "trademarks": false, + "icons": 287, + "layout": "dist/icons/*.svg, unchanged names", + "note": "Lucide is its maintained successor." + }, + "boxicons": { + "name": "Boxicons", + "owner": "Aniket Suvarna (Atisa)", + "homepage": "https://boxicons.com", + "repository": "https://github.com/box-icons/boxicons", + "source": { + "type": "npm", + "package": "boxicons", + "source_path": "svg" + }, + "current_version": "2.1.4", + "ref": "npm boxicons 2.1.4", + "version_check_url": "https://registry.npmjs.org/boxicons/latest", + "license": { + "spdx": "MIT", + "sha256": "1676a8bdd036382b7bdb8a9ec26a55af6bf15035dd4d405169da2ee45b066e22", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/boxicons/LICENSE", + "read_from": "LICENSE in the npm package" + }, + "attribution_required": false, + "trademarks": true, + "icons": 1634, + "layout": "svg/regular, svg/solid and svg/logos merged; the bx-/bxs-/bxl- prefixes already keep names apart", + "note": "The npm manifest's `license` field reads `(CC-BY-4.0 OR OFL-1.1 OR MIT)`; the LICENSE file the package ships is MIT, and MIT is the option taken. The `bxl-` logos depict trademarks." + }, + "iconpark": { + "name": "IconPark", + "owner": "ByteDance", + "homepage": "https://iconpark.oceanengine.com", + "repository": "https://github.com/bytedance/IconPark", + "source": { + "type": "github-archive", + "owner": "bytedance", + "repo": "IconPark", + "archive_url": "https://github.com/bytedance/IconPark/archive/refs/tags/v{version}.tar.gz", + "source_path": "source" + }, + "current_version": "1.4.2", + "ref": "tag v1.4.2 (repository archived)", + "version_check_url": "https://api.github.com/repos/bytedance/IconPark/tags", + "license": { + "spdx": "Apache-2.0", + "sha256": "81268f00802b1a9cffae4df43f78c3a0809845e03fef3da68f26ad2e4c49681e", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/iconpark/LICENSE", + "read_from": "LICENSE at tag v1.4.2" + }, + "attribution_required": false, + "trademarks": false, + "icons": 2658, + "layout": "source//*.svg flattened; names are unique across categories", + "note": "Upstream is archived; 1.4.2 is final." + }, + "mingcute": { + "name": "MingCute", + "owner": "MingCute Design", + "homepage": "https://www.mingcute.com", + "repository": "https://github.com/mingcute-design/mingcute-icons", + "source": { + "type": "github-archive", + "owner": "mingcute-design", + "repo": "mingcute-icons", + "archive_url": "https://github.com/mingcute-design/mingcute-icons/archive/refs/tags/v{version}.tar.gz", + "source_path": "assets/stroke/core" + }, + "current_version": "4.0.0", + "ref": "tag v4.0.0", + "version_check_url": "https://api.github.com/repos/mingcute-design/mingcute-icons/tags", + "license": { + "spdx": "Apache-2.0", + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/mingcute/LICENSE", + "read_from": "LICENSE at tag v4.0.0" + }, + "attribution_required": false, + "trademarks": false, + "icons": 4526, + "layout": "assets/stroke/core/{regular,filled}//.svg flattened to -regular / -filled, underscores slugged to hyphens", + "note": "The npm package @mingcute/icons ships JavaScript only, so the source is the tagged archive." + }, + "tdesign": { + "name": "TDesign Icons", + "owner": "TDesign (Tencent)", + "homepage": "https://tdesign.tencent.com", + "repository": "https://github.com/Tencent/tdesign-icons", + "source": { + "type": "npm", + "package": "tdesign-icons-svg", + "source_path": "src" + }, + "current_version": "0.4.8", + "ref": "npm tdesign-icons-svg 0.4.8", + "version_check_url": "https://registry.npmjs.org/tdesign-icons-svg/latest", + "license": { + "spdx": "MIT", + "sha256": "b3dbcb89dcf4a11abf1b70d043795a3da0c458af16fefd2ff315d9ff5875312f", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/tdesign/LICENSE", + "read_from": "LICENSE in the npm package" + }, + "attribution_required": false, + "trademarks": false, + "icons": 2358, + "layout": "src/*.svg, unchanged names" + }, + "siemens-ix": { + "name": "Siemens iX Icons", + "owner": "Siemens AG", + "homepage": "https://ix.siemens.io", + "repository": "https://github.com/siemens/ix-icons", + "source": { + "type": "npm", + "package": "@siemens/ix-icons", + "source_path": "svg" + }, + "current_version": "3.7.1", + "ref": "npm @siemens/ix-icons 3.7.1", + "version_check_url": "https://registry.npmjs.org/@siemens/ix-icons/latest", + "license": { + "spdx": "MIT", + "sha256": "42cb46c894aa9267307aa4985f40f44ba669e54d9b6b487960ecd24e1989213b", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/siemens-ix/LICENSE.md", + "read_from": "LICENSE.md in the npm package" + }, + "attribution_required": false, + "trademarks": false, + "icons": 1490, + "layout": "svg/*.svg, unchanged names" + }, + "garden": { + "name": "Zendesk Garden Icons", + "owner": "Zendesk", + "homepage": "https://garden.zendesk.com", + "repository": "https://github.com/zendeskgarden/svg-icons", + "source": { + "type": "npm", + "package": "@zendeskgarden/svg-icons", + "source_path": "src" + }, + "current_version": "8.4.0", + "ref": "npm @zendeskgarden/svg-icons 8.4.0", + "version_check_url": "https://registry.npmjs.org/@zendeskgarden/svg-icons/latest", + "license": { + "spdx": "Apache-2.0", + "sha256": "46ae5f0dc2e06a18cde8b06bdb45abea0b6e28d169e6eff2069536780304cf6e", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/garden/LICENSE.md", + "read_from": "LICENSE.md in the npm package" + }, + "attribution_required": false, + "trademarks": false, + "icons": 932, + "layout": "src/16 and src/12 flattened to -16 / -12", + "note": "9.0.0 exists only as a pre-release (next); the stable 8.4.0 is pinned." + }, + "coolicons": { + "name": "coolicons", + "owner": "Kryston Schwarze", + "homepage": "https://coolicons.cool", + "repository": "https://github.com/krystonschwarze/coolicons", + "source": { + "type": "github-archive", + "owner": "krystonschwarze", + "repo": "coolicons", + "archive_url": "https://github.com/krystonschwarze/coolicons/archive/refs/tags/v{version}.tar.gz", + "source_path": "icons/svg" + }, + "current_version": "5.2.0", + "ref": "tag v5.2.0", + "version_check_url": "https://api.github.com/repos/krystonschwarze/coolicons/tags", + "license": { + "spdx": "MIT", + "sha256": "35d8a4a8e1793b261198949d213583904b11e3826be8618ae9b9dbc6b70f50cc", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/coolicons/LICENSE", + "read_from": "LICENSE at tag v5.2.0" + }, + "attribution_required": false, + "trademarks": false, + "icons": 2648, + "layout": "icons/svg/{line,fill}/regular/*.svg flattened to -line / -fill", + "note": "v5 ships 1,324 icons in 15 style folders (line, fill, bulk, duotone, duocolor x regular, round, sharp): 19,860 files. Only line and fill at regular corners are vendored (2,648), to keep the bundle small; the other 13 folders are the same drawings restyled. v4 was CC-BY-4.0; v5 is MIT." + }, + "eva": { + "name": "Eva Icons", + "owner": "Akveo", + "homepage": "https://akveo.github.io/eva-icons", + "repository": "https://github.com/akveo/eva-icons", + "source": { + "type": "npm", + "package": "eva-icons", + "source_path": "." + }, + "current_version": "1.1.3", + "ref": "npm eva-icons 1.1.3 (tag v1.1.3)", + "version_check_url": "https://registry.npmjs.org/eva-icons/latest", + "license": { + "spdx": "MIT", + "sha256": "4788c563f8871931bce41ede59379a5277ac1a473e289f9100924149ce8bd311", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/eva/LICENSE", + "read_from": "LICENSE.txt at tag v1.1.3 (the npm package ships none)" + }, + "attribution_required": false, + "trademarks": false, + "icons": 490, + "layout": "fill/svg and outline/svg merged; upstream already names outline icons -outline", + "note": "Unmaintained upstream (last push 2023)." + }, + "ant-design": { + "name": "Ant Design Icons", + "owner": "Ant UED", + "homepage": "https://ant.design/components/icon", + "repository": "https://github.com/ant-design/ant-design-icons", + "source": { + "type": "npm", + "package": "@ant-design/icons-svg", + "source_path": "inline-svg" + }, + "current_version": "4.6.0", + "ref": "npm @ant-design/icons-svg 4.6.0 (repo commit 7f2516ac91)", + "version_check_url": "https://registry.npmjs.org/@ant-design/icons-svg/latest", + "license": { + "spdx": "MIT", + "sha256": "5d367fb0a07340571542eb4ee8eb1add62d37b71bad6786a57c2e9a86bf76c70", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/ant-design/LICENSE", + "read_from": "LICENSE at commit 7f2516ac91 (the commit that set packages/icons-svg to 4.6.0; the npm package ships none)" + }, + "attribution_required": false, + "trademarks": false, + "icons": 848, + "layout": "inline-svg/{outlined,filled,twotone} flattened to -outlined / -filled / -twotone" + }, + "health-icons": { + "name": "Health Icons", + "owner": "Resolve to Save Lives and the Health Icons contributors", + "homepage": "https://healthicons.org", + "repository": "https://github.com/resolvetosavelives/healthicons", + "source": { + "type": "npm", + "package": "healthicons", + "source_path": "public/icons/svg" + }, + "current_version": "3.1.0", + "ref": "npm healthicons 3.1.0", + "version_check_url": "https://registry.npmjs.org/healthicons/latest", + "license": { + "spdx": "CC0-1.0", + "sha256": "d36196c6c2b0a983415778adddf81bfe63c51f408e707a36fa2b029d65e92d06", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/health-icons/LICENSE", + "read_from": "LICENSE in the npm package (icons CC0-1.0; website code MIT)" + }, + "attribution_required": false, + "trademarks": false, + "icons": 1552, + "layout": "public/icons/svg/{outline,filled}//.svg flattened to -outline / -filled", + "note": "The 24px redraws (filled-24px, outline-24px; 288 each) are not vendored." + }, + "devicon": { + "name": "Devicon", + "owner": "konpa and the Devicon contributors", + "homepage": "https://devicon.dev", + "repository": "https://github.com/devicons/devicon", + "source": { + "type": "npm", + "package": "devicon", + "source_path": "icons" + }, + "current_version": "2.18.0", + "ref": "npm devicon 2.18.0", + "version_check_url": "https://registry.npmjs.org/devicon/latest", + "license": { + "spdx": "MIT", + "sha256": "121194741d4a915b9f5890fdd6dd95121f9b1f816517c792358d72d7c838d664", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/devicon/LICENSE", + "read_from": "LICENSE in the npm package" + }, + "attribution_required": false, + "trademarks": true, + "icons": 2120, + "layout": "icons//.svg flattened; upstream file names carry the tech and the style" + }, + "fluent-emoji": { + "name": "Fluent Emoji", + "owner": "Microsoft Corporation", + "homepage": "https://github.com/microsoft/fluentui-emoji", + "repository": "https://github.com/microsoft/fluentui-emoji", + "source": { + "type": "github-archive", + "owner": "microsoft", + "repo": "fluentui-emoji", + "archive_url": "https://github.com/microsoft/fluentui-emoji/archive/{version}.tar.gz", + "source_path": "assets" + }, + "current_version": "1ffb34c752ecf5d402f04cfb4b392c77f57c54bc", + "ref": "commit 1ffb34c752 (main, 2026-08-24; upstream has no tags)", + "version_check_url": "https://api.github.com/repos/microsoft/fluentui-emoji/commits/main", + "license": { + "spdx": "MIT", + "sha256": "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/fluent-emoji/LICENSE", + "read_from": "LICENSE at commit 1ffb34c752" + }, + "attribution_required": false, + "trademarks": false, + "icons": 4740, + "layout": "assets//[/]{Flat,High Contrast}/.svg flattened; upstream file names carry the style and tone", + "note": "The Color style (3,145 files, 132 MB of gradients) and the 3D PNGs are not vendored; Flat (3,145) and High Contrast (1,595) are." + }, + "pepicons": { + "name": "Pepicons", + "owner": "CyCraft", + "homepage": "https://pepicons.com", + "repository": "https://github.com/CyCraft/pepicons", + "source": { + "type": "npm", + "package": "pepicons", + "source_path": "svg" + }, + "current_version": "3.1.6", + "ref": "npm pepicons 3.1.6 (tag v3.1.6)", + "version_check_url": "https://registry.npmjs.org/pepicons/latest", + "license": { + "spdx": "CC-BY-4.0", + "sha256": "dbb19cb7bda19afef40f19094a668dfe61375cfd57c49d33c660bdb76676b5c5", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/pepicons/LICENSE", + "read_from": "LICENSE at tag v3.1.6 (the npm package ships none)" + }, + "attribution_required": true, + "trademarks": false, + "icons": 3825, + "layout": "svg/{pop,print,pencil} flattened to -pop / -print / -pencil" + }, + "game-icons": { + "name": "Game Icons", + "owner": "Lorc, Delapouite and the game-icons.net contributors", + "homepage": "https://game-icons.net", + "repository": "https://github.com/game-icons/icons", + "source": { + "type": "github-archive", + "owner": "game-icons", + "repo": "icons", + "archive_url": "https://github.com/game-icons/icons/archive/{version}.tar.gz", + "source_path": "." + }, + "current_version": "82d948812bfe3f269ef8f731dcdb07b08160edc4", + "ref": "commit 82d948812b (master; upstream has no tags)", + "version_check_url": "https://api.github.com/repos/game-icons/icons/commits/master", + "license": { + "spdx": "CC-BY-3.0", + "sha256": "fac95252798668fab5ea2cb08338f2ebc6bf894a72b19a15af1ca62f0a6d2e79", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/game-icons/license.txt", + "read_from": "license.txt at commit 82d948812b" + }, + "attribution_required": true, + "trademarks": false, + "icons": 4178, + "layout": "/.svg, one directory per contributor so every icon stays attributable", + "note": "badges/ (59) and various-artists/ (2) are not vendored: license.txt does not name them, so no licence is stated for them." + }, + "coreui": { + "name": "CoreUI Icons Free", + "owner": "CoreUI (creativeLabs)", + "homepage": "https://icons.coreui.io", + "repository": "https://github.com/coreui/coreui-icons", + "source": { + "type": "npm", + "package": "@coreui/icons", + "source_path": "svg/free" + }, + "current_version": "3.1.0", + "ref": "npm @coreui/icons 3.1.0", + "version_check_url": "https://registry.npmjs.org/@coreui/icons/latest", + "license": { + "spdx": "CC-BY-4.0", + "sha256": "171b668bd4fa31e5b75141d30290e5ab9eefcc881027ef24d0bd9e91aea8689b", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/coreui/LICENSE", + "read_from": "LICENSE in the npm package" + }, + "attribution_required": true, + "trademarks": false, + "icons": 562, + "layout": "svg/free/*.svg, unchanged names", + "note": "Only the free set is vendored. The brand (828) and flag (198) SVGs are CC0 in the same LICENSE, but the brands are trademarks and the flags duplicate ichava/icon-sets-flag." + }, + "pinhead": { + "name": "Pinhead", + "owner": "Wayside Mapping and the Pinhead contributors", + "homepage": "https://pinhead.ink", + "repository": "https://github.com/waysidemapping/pinhead", + "source": { + "type": "npm", + "package": "@waysidemapping/pinhead", + "source_path": "dist/icons" + }, + "current_version": "16.26.0", + "ref": "npm @waysidemapping/pinhead 16.26.0", + "version_check_url": "https://registry.npmjs.org/@waysidemapping/pinhead/latest", + "license": { + "spdx": "CC0-1.0", + "sha256": "a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/pinhead/LICENSE", + "read_from": "LICENSE in the npm package" + }, + "attribution_required": false, + "trademarks": false, + "icons": 2747, + "layout": "dist/icons/*.svg, unchanged names", + "note": "Pinhead incorporates icons from Temaki, Maki, OpenStreetMap Carto, Osmic, Health Icons and others; its README lists every source as CC0 or public domain." + }, + "svg-logos": { + "name": "SVG Logos", + "owner": "Gil Barbara", + "homepage": "https://svgporn.com", + "repository": "https://github.com/gilbarbara/logos", + "source": { + "type": "github-archive", + "owner": "gilbarbara", + "repo": "logos", + "archive_url": "https://github.com/gilbarbara/logos/archive/{version}.tar.gz", + "source_path": "logos" + }, + "current_version": "22f03ffc4a652309d1fea4ade70f46e9e1d6602c", + "ref": "commit 22f03ffc4a (main; upstream has no releases)", + "version_check_url": "https://api.github.com/repos/gilbarbara/logos/commits/main", + "license": { + "spdx": "CC0-1.0", + "sha256": "36ffd9dc085d529a7e60e1276d73ae5a030b020313e6c5408593a6ae2af39673", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/svg-logos/LICENSE.txt", + "read_from": "LICENSE.txt at commit 22f03ffc4a" + }, + "attribution_required": false, + "trademarks": true, + "icons": 2068, + "layout": "logos/*.svg, unchanged names" + }, + "noto-emoji": { + "name": "Noto Emoji", + "owner": "Google", + "homepage": "https://fonts.google.com/noto/specimen/Noto+Color+Emoji", + "repository": "https://github.com/googlefonts/noto-emoji", + "source": { + "type": "github-archive", + "owner": "googlefonts", + "repo": "noto-emoji", + "archive_url": "https://github.com/googlefonts/noto-emoji/archive/refs/tags/v{version}.tar.gz", + "source_path": "2D/svg" + }, + "current_version": "2026-09-24-unicode18_0", + "ref": "tag v2026-09-24-unicode18_0 (e20cbc2bbe)", + "version_check_url": "https://api.github.com/repos/googlefonts/noto-emoji/releases/latest", + "license": { + "spdx": "Apache-2.0", + "sha256": "611ceab36dae96644ca84e8ace6873821790192bf6f73b0d0624a21b24b4b332", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/noto-emoji/LICENSE", + "read_from": "2D/svg/LICENSE at tag v2026-09-24-unicode18_0" + }, + "attribution_required": false, + "trademarks": false, + "icons": 3580, + "layout": "2D/svg/emoji_u.svg, names slugged (emoji-u1f600)", + "note": "The repository's root LICENSE is OFL-1.1 and covers the fonts; the SVG directory carries its own LICENSE, the Apache 2.0 notice, which is what governs these files. 170 SVGs are not vendored: they declare Adobe Illustrator entities in an internal DTD, which the sanitiser cannot parse, so they cannot be checked.", + "excluded": { + "reason": "Internal DTD declaring Adobe Illustrator entities; the sanitiser cannot parse them, so they are not shipped.", + "names": [ + "emoji-u1f306", + "emoji-u1f307", + "emoji-u1f341", + "emoji-u1f361", + "emoji-u1f367", + "emoji-u1f368", + "emoji-u1f369", + "emoji-u1f374", + "emoji-u1f37d", + "emoji-u1f380", + "emoji-u1f39b", + "emoji-u1f39f", + "emoji-u1f3a6", + "emoji-u1f3a9", + "emoji-u1f3aa", + "emoji-u1f3ac", + "emoji-u1f3cf", + "emoji-u1f3d1", + "emoji-u1f3d3", + "emoji-u1f3d8", + "emoji-u1f3d9", + "emoji-u1f3e8", + "emoji-u1f3e9", + "emoji-u1f3eb", + "emoji-u1f3f5", + "emoji-u1f3f8", + "emoji-u1f40b", + "emoji-u1f428", + "emoji-u1f42d", + "emoji-u1f433", + "emoji-u1f436", + "emoji-u1f43b-200d-2744", + "emoji-u1f43c", + "emoji-u1f454", + "emoji-u1f45b", + "emoji-u1f468-1f3fb-200d-1f393", + "emoji-u1f468-1f3fc-200d-1f393", + "emoji-u1f468-1f3fd-200d-1f393", + "emoji-u1f468-1f3fe-200d-1f393", + "emoji-u1f468-1f3ff-200d-1f393", + "emoji-u1f468-200d-1f393", + "emoji-u1f469-1f3fb-200d-1f393", + "emoji-u1f469-1f3fc-200d-1f393", + "emoji-u1f469-1f3fd-200d-1f393", + "emoji-u1f469-1f3fe-200d-1f393", + "emoji-u1f469-1f3ff-200d-1f393", + "emoji-u1f469-200d-1f393", + "emoji-u1f489", + "emoji-u1f4b1", + "emoji-u1f4b2", + "emoji-u1f4bb", + "emoji-u1f4d6", + "emoji-u1f4da", + "emoji-u1f4de", + "emoji-u1f4e2", + "emoji-u1f4e3", + "emoji-u1f4f3", + "emoji-u1f4f6", + "emoji-u1f4fa", + "emoji-u1f500", + "emoji-u1f501", + "emoji-u1f502", + "emoji-u1f504", + "emoji-u1f508", + "emoji-u1f509", + "emoji-u1f50a", + "emoji-u1f516", + "emoji-u1f518", + "emoji-u1f526", + "emoji-u1f53c", + "emoji-u1f53d", + "emoji-u1f54b", + "emoji-u1f573", + "emoji-u1f578", + "emoji-u1f5b2", + "emoji-u1f5fa", + "emoji-u1f638", + "emoji-u1f639", + "emoji-u1f63b", + "emoji-u1f680", + "emoji-u1f68e", + "emoji-u1f6a2", + "emoji-u1f6a3", + "emoji-u1f6a3-1f3fb", + "emoji-u1f6a3-1f3fb-200d-2640", + "emoji-u1f6a3-1f3fb-200d-2642", + "emoji-u1f6a3-1f3fc", + "emoji-u1f6a3-1f3fc-200d-2640", + "emoji-u1f6a3-1f3fc-200d-2642", + "emoji-u1f6a3-1f3fd", + "emoji-u1f6a3-1f3fd-200d-2640", + "emoji-u1f6a3-1f3fd-200d-2642", + "emoji-u1f6a3-1f3fe", + "emoji-u1f6a3-1f3fe-200d-2640", + "emoji-u1f6a3-1f3fe-200d-2642", + "emoji-u1f6a3-1f3ff", + "emoji-u1f6a3-1f3ff-200d-2640", + "emoji-u1f6a3-1f3ff-200d-2642", + "emoji-u1f6a3-200d-2640", + "emoji-u1f6a3-200d-2642", + "emoji-u1f6a5", + "emoji-u1f6a6", + "emoji-u1f6aa", + "emoji-u1f6b0", + "emoji-u1f6ce", + "emoji-u1f6d9", + "emoji-u1f6f6", + "emoji-u1f930", + "emoji-u1f930-1f3fb", + "emoji-u1f930-1f3fc", + "emoji-u1f930-1f3fd", + "emoji-u1f930-1f3fe", + "emoji-u1f930-1f3ff", + "emoji-u1f94d", + "emoji-u1f952", + "emoji-u1f962", + "emoji-u1f9c8", + "emoji-u1f9d1-1f3fb-200d-1f393", + "emoji-u1f9d1-1f3fc-200d-1f393", + "emoji-u1f9d1-1f3fd-200d-1f393", + "emoji-u1f9d1-1f3fe-200d-1f393", + "emoji-u1f9d1-1f3ff-200d-1f393", + "emoji-u1f9d1-200d-1f393", + "emoji-u1f9fd", + "emoji-u1fa8b", + "emoji-u1fa8d", + "emoji-u1fa90", + "emoji-u1fa9e", + "emoji-u1faa1", + "emoji-u1faa4", + "emoji-u1faa7", + "emoji-u1fac3-1f3fb", + "emoji-u1fac3-1f3fc", + "emoji-u1fac3-1f3fd", + "emoji-u1fac3-1f3fe", + "emoji-u1fac3-1f3ff", + "emoji-u1faeb", + "emoji-u1faf9", + "emoji-u1faf9-1f3fb", + "emoji-u1faf9-1f3fc", + "emoji-u1faf9-1f3fd", + "emoji-u1faf9-1f3fe", + "emoji-u1faf9-1f3ff", + "emoji-u1fafa", + "emoji-u1fafa-1f3fb", + "emoji-u1fafa-1f3fc", + "emoji-u1fafa-1f3fd", + "emoji-u1fafa-1f3fe", + "emoji-u1fafa-1f3ff", + "emoji-u23cf", + "emoji-u23e9", + "emoji-u23ea", + "emoji-u23eb", + "emoji-u23ec", + "emoji-u23ed", + "emoji-u23ee", + "emoji-u23ef", + "emoji-u23f8", + "emoji-u23f9", + "emoji-u23fa", + "emoji-u25aa", + "emoji-u25ab", + "emoji-u25b6", + "emoji-u25c0", + "emoji-u25fd", + "emoji-u25fe", + "emoji-u26d4", + "emoji-u26f0", + "emoji-u26f7", + "emoji-u2734" + ] + } + } + }, + "dropped": { + "vscode-icons": { + "ref": "tag v13.0.0", + "reason": "README at v13.0.0: \"The icons are licensed under the Creative Commons - ShareAlike (CC BY-SA) license\"; only the extension code is MIT. CC-BY-SA is not bundle-eligible." + } + }, + "update_command": { + "type": "recipe", + "recipe": "icon-sets-extras.", + "_note": "Each set has its own icon-sets-sync recipe, icon-sets-extras. (icon-sets-sync#88), which refreshes files// and licenses// and stamps upstream.sets..current_version." + } + }, + "license": { + "spdx": "MIT", + "scope": "package code only", + "icons": "per set: upstream.sets..license" + } +} diff --git a/tests/fixtures/RegistrationGolden/extras/files/.gitkeep b/tests/fixtures/RegistrationGolden/extras/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/RegistrationGolden/flag/config.json b/tests/fixtures/RegistrationGolden/flag/config.json new file mode 100644 index 0000000..02b2fd5 --- /dev/null +++ b/tests/fixtures/RegistrationGolden/flag/config.json @@ -0,0 +1,85 @@ +{ + "schema_version": "1.0", + "package": { + "name": "ichava/icon-sets-flag", + "title": "Flag Icons", + "description": "271 country flag SVGs sourced from lipis/flag-icons, available in 4x3 and 1x1 aspect ratios. Replaces the flag half of the deprecated simtabi/laflamoji.", + "keywords": [ + "flags", + "country-flags", + "svg", + "lipis", + "iso-3166" + ], + "version": "0.1.0", + "upstream_version": "7.6.0", + "type": "single", + "kind": "icon", + "license": "MIT" + }, + "metadata": { + "authors": [ + { + "name": "Panayiotis Lipiridis (lipis)", + "role": "Creator -- lipis/flag-icons", + "url": "https://github.com/lipis/flag-icons" + } + ], + "homepage": "https://github.com/lipis/flag-icons", + "repository": "https://github.com/ichava/icon-sets-flag", + "data": { + "variants": { + "4x3": { + "name": "4:3", + "slug": "4x3", + "description": "Canonical 4:3 aspect ratio. Best fit for most flags.", + "default": true, + "icon_suffix": "", + "display_order": 1, + "attributes": {} + }, + "1x1": { + "name": "1:1", + "slug": "1x1", + "description": "Square 1:1 aspect ratio. Useful for badges, round avatars, picker grids.", + "default": false, + "icon_suffix": "", + "display_order": 2, + "attributes": {} + } + }, + "categories": { + "country": { + "name": "Country flags", + "description": "ISO 3166-1 alpha-2 country codes (us, gb, de, jp, ...)", + "sets": [ + "country" + ] + } + } + } + }, + "config": { + "icon_prefix": "flag" + }, + "upstream": { + "source": { + "type": "npm", + "package": "flag-icons" + }, + "current_version": "7.6.0", + "version_check_url": "https://registry.npmjs.org/flag-icons/latest", + "license": "MIT", + "cdn": { + "jsdelivr": "https://cdn.jsdelivr.net/npm/flag-icons@{version}/flags/{ratio}/{code}.svg", + "unpkg": "https://unpkg.com/flag-icons@{version}/flags/{ratio}/{code}.svg", + "github_raw": "https://raw.githubusercontent.com/lipis/flag-icons/v{version}/flags/{ratio}/{code}.svg" + }, + "update_command": { + "type": "github-archive", + "_note": "Re-runs the bundled fetch + categorise. Run when assets need refreshing past the snapshot vendored here, which is `current_version`.", + "archive_url": "https://github.com/lipis/flag-icons/archive/refs/tags/v{version}.zip", + "source_path": "flag-icons-{version}/flags" + } + } +} diff --git a/tests/fixtures/RegistrationGolden/flag/files/.gitkeep b/tests/fixtures/RegistrationGolden/flag/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/RegistrationGolden/fluent/config.json b/tests/fixtures/RegistrationGolden/fluent/config.json new file mode 100644 index 0000000..67851d3 --- /dev/null +++ b/tests/fixtures/RegistrationGolden/fluent/config.json @@ -0,0 +1,90 @@ +{ + "schema_version": "1.0", + "package": { + "name": "ichava/icon-sets-fluent", + "title": "Fluent UI System Icons", + "description": "Microsoft's Fluent UI System Icons at 24 px, in regular and filled.", + "keywords": [ + "icons", + "svg", + "fluent", + "fluentui", + "microsoft" + ], + "version": "0.1.0", + "upstream_version": "1.1.345", + "type": "multi", + "kind": "icon", + "license": "MIT" + }, + "metadata": { + "authors": [ + { + "name": "Microsoft Corporation", + "role": "Creator -- Fluent UI System Icons", + "url": "https://github.com/microsoft/fluentui-system-icons" + } + ], + "homepage": "https://github.com/microsoft/fluentui-system-icons", + "repository": "https://github.com/ichava/icon-sets-fluent", + "data": { + "variants": { + "regular": { + "name": "Regular", + "slug": "regular", + "description": "Outlined style at 24 px", + "default": true, + "icon_suffix": "_24_regular", + "display_order": 1, + "attributes": {}, + "preview_icon": "home", + "color_scheme": "fill-based" + }, + "filled": { + "name": "Filled", + "slug": "filled", + "description": "Solid style at 24 px", + "default": false, + "icon_suffix": "_24_filled", + "display_order": 2, + "attributes": {}, + "preview_icon": "home", + "color_scheme": "fill-based" + } + }, + "categories": {}, + "kinds": { + "rules": [] + }, + "items": {} + } + }, + "config": { + "icon_prefix": "fluent" + }, + "license": { + "spdx": "MIT", + "sha256": "69bc45dc42b9acb96a69823adbc6ae538374e3c0bde169b855b32c48eaaef52f", + "checked_at": "2026-11-04", + "file": "resources/assets/svg/licenses/fluentui-system-icons/LICENSE", + "source": "https://github.com/microsoft/fluentui-system-icons/blob/73ec879377b1f412f33aca4a128aa967e6c238f0/LICENSE", + "notice": "resources/assets/svg/licenses/fluentui-system-icons/NOTICE" + }, + "upstream": { + "source": { + "type": "npm", + "package": "@fluentui/svg-icons", + "_ref": "73ec879377b1f412f33aca4a128aa967e6c238f0", + "_repository": "https://github.com/microsoft/fluentui-system-icons" + }, + "current_version": "1.1.345", + "version_check_url": "https://registry.npmjs.org/@fluentui/svg-icons/latest", + "license": "MIT", + "update_command": { + "type": "recipe", + "recipe": "icon-sets-fluent", + "_note": "icon-sets-sync has no config/icon-sets-fluent.json yet; the proposed recipe is pack-recipes/icon-sets-fluent.json in the ichava design folder, and lands with the one icon-sets-sync pull request that adds the v0.1.0 packs. Until then the weekly sync-upstream run has nothing to run." + }, + "_subset": "Size 24 only, regular and filled. The other sizes (10, 12, 16, 20, 28, 32, 48) and the `light` and `color` styles are not vendored. Upstream names are `_24_