Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
25eddea
Scope runtime limits to Ichava's own operations
imanimanyara Nov 5, 2026
5e31fbf
Scope Icon's morph alias to ichava-icon and guard it at boot
imanimanyara Nov 5, 2026
cb08f67
Keep direct SeedIconsJob::handle() callers working and pin the new alias
imanimanyara Nov 5, 2026
c227290
Seed variant terms from the registry's variant_metadata
imanimanyara Nov 5, 2026
3de5052
Read served SVGs through SvgDriver::load()
imanimanyara Nov 5, 2026
2d00fb3
Give pack config caching one owner and one parser
imanimanyara Nov 5, 2026
83de36e
Snapshot what every estate and fixture pack registers
imanimanyara Nov 5, 2026
72f31e9
Register every icon set through one PackDefinition path
imanimanyara Nov 5, 2026
b58f3e1
Route package() registrations through PackBuilder
imanimanyara Nov 5, 2026
2f4c58f
Read a pack's constants through its registered definition
imanimanyara Nov 5, 2026
a0bd2fe
Apply config.defaults when a set registers, and build lazily
imanimanyara Nov 5, 2026
66ea2d4
Add registerMany() and run bulk registration through it
imanimanyara Nov 5, 2026
2a0e201
Wire every documented custom-icons key into registration
imanimanyara Nov 5, 2026
c74a71d
Make <x-ichava::icon> a configured short prefix beside ichava-core
imanimanyara Nov 5, 2026
54afb98
Give pack providers an optional pack(PackSpec) hook and canonical tags
imanimanyara Nov 5, 2026
a0e745e
Make concrete classes final by default, with a guarded exception list
imanimanyara Nov 5, 2026
c09904d
Put the update checker and database operations behind contracts
imanimanyara Nov 5, 2026
194e8d0
Name every action's entry point handle()
imanimanyara Nov 5, 2026
b6952cc
Move CountSvgFiles to Support\SvgFileCounter
imanimanyara Nov 5, 2026
8dc6cf1
Inject the registry's and session manager's collaborators
imanimanyara Nov 5, 2026
10ed094
Extract the served-SVG pipeline into IconSvgReader
imanimanyara Nov 5, 2026
76d016c
Move the pack base providers into the Providers namespace
imanimanyara Nov 5, 2026
eef91e6
Ship the pack test kit: PackTestCase, PackConformance, SvgInspector
imanimanyara Nov 5, 2026
f7b96da
Document the pack paths, bulk registration and the short tag
imanimanyara Nov 5, 2026
09f2933
Apply the code style to the changed files
imanimanyara Nov 5, 2026
185597d
Satisfy PHPStan at max on the new code
imanimanyara Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan

### Added

- **One pack model, with a simple and an advanced path** (Proposal 2). Every set now registers as a `Data\PackDefinition` through `IconRegistry::register()`, built by the fluent `Support\Pack\PackBuilder` from a `config.json` (`PackBuilder::fromDirectory()`), from code (`PackBuilder::make('vendor/package', $filesPath)`), or both. Each builder method writes a key an existing reader consumes (`prefix`, `about`, `variant`, `defaultVariant`, `categories`, `kind`, `categoryKind`, `kindRule`, `itemKind`, `themeTwin`, `alias`/`aliases`, `defaultClass`, `defaultAttributes`, `fallback`, `providedBy`, `component`, `metadata`, `withOptions`, `when`). Fluent scalars replace the file's, maps merge per key, and fluent kind rules come first. `fromDirectory()`, `package()` and `custom-icons` all delegate to it, and a golden snapshot over 31 pack directories (19 estate packs and every fixture) proves registration is unchanged. `IconRegistry::definition()`/`definitionAt()` return what a set registered with, and a pack's constants class reads it through `Support\Pack\PackConfigReader`, so it sees the builder's overrides. `docs/tools/pack-registration.md`. `tests/Feature/PackBuilderTest.php`, `RegistrationGoldenTest.php`, `JsonConfigConstantsReadsDefinitionTest.php`.
- **`IconPackServiceProvider::pack(PackSpec $pack)`**, an optional hook for the advanced path. The default does nothing, so a pack that keeps everything in `config.json` is unchanged. `Support\Pack\PackSpec` shapes the default set (`set()`), adds a set (`addSet()`) or every config-bearing sub-directory of a path (`sets()`), adds Blade components (`component()`, `mainComponent()`) and names the config file (`configFile()`). `tests/Feature/PackSpecTest.php`, fixture `tests/fixtures/SpecPack`.
- **Bulk registration on the same path** (Proposal 3). `IconRegistry::registerMany(iterable $definitions, ?BulkOptions $options): BulkResult` registers definitions or builders as one run: one registrar id, `started` (the factory had no dispatcher), each set's `processing`/`registered`/`failed`, then one `completed` with the counts. A broken set is reported and skipped unless `BulkOptions::$failFast`. `PackBuilder::discover($basePath, $only)` lists the config-bearing sub-directories. `Data\BulkOptions`, `Data\BulkResult`. Core's bundled test set registers through it on every boot. `tests/Feature/BulkRegistrationTest.php`.
- **`components.short_prefix` and `components.enabled`** (Proposal 1). `View\ComponentPrefixes` registers the canonical `ichava-core` prefix always and the short one (default `ichava`, `ICHAVA_COMPONENT_SHORT_PREFIX`; null turns it off) beside it, validated against Blade's tag parser at boot. `components.enabled` false registers no core tag. Doctor's `component.short_prefix_shadowed` warns when a later provider has replaced the short alias. Packs follow the same two tiers: `<x-{vendor}-{package}::icon>` always, `<x-{package}-icon>` while the short prefix is on (C-08). `tests/Feature/ComponentNamespaceDeferralTest.php`.
- **A pack test kit** (P-03, P-04): `Testing\PackTestCase`, the base every pack suite extends; `Testing\PackConformance`, one method per pack rule (core-only dependency, pack type, PHPStan level, sibling constraints, sync caller, workflow permissions, stray text, SVG policy), each asserting how many files it read; and `Support\SvgInspector` (`policyViolations()`, `strayTextNodes()`). `tests/Unit/PackConformanceTest.php`, `SvgInspectorTest.php`.
- **`Contracts\ChecksPackUpdates` and `Contracts\DatabaseOperations`**, bound to `IconPackUpdateChecker` and `DatabaseOperationsService`; core's consumers depend on the contracts (C-06). `tests/Feature/ServiceContractsTest.php`.
- **`Services\IconSvgReader`**, the served-SVG pipeline with its collaborators injected; `Icon`'s accessor forwards to it (C-11). **`Support\RuntimeLimits`**, **`Support\PackConfigCache`**, **`Support\DiscoveryCacheGeneration`**.
- **`Support\IconPackServiceProvider`, the base a pack provider extends.** A pack declares `constantsClass()` and `componentClass()` and nothing else; the base names the package from the constants class, sets the path from the provider file, registers `config/{slug}.php` when the pack ships one, registers `<x-{slug}-icon>`, and registers `resources/assets/svg` with the registry, in the order every pack used. A pack built on it writes no `laranail/package-tools` type, so it can require `ichava/core` alone. `Support\ServiceProvider` is unchanged and still works for a provider that writes its own `configurePackage()`. `tests/Feature/IconPackServiceProviderTest.php`, with the `tests/fixtures/BasePack` fixture.
- **`IchavaConstants::PACK_COMPOSER_TYPE`** (`ichava-icon-set`), the composer `type` a pack declares so discovery can find it.
- **Kind resolution covers every D6 row** (K1, contract §1–2). Besides a category's `kind` and `package.kind`, a pack can now declare kind per item (`metadata.data.items.<item path>.kind`) and by ordered glob rule (`metadata.data.kinds.rules[]`, first match wins), and a host can override any of it in `ichava.ichava-core.kinds.overrides` (`'<vendor/pack | *>::<glob or exact item path>' => kind`, exact keys before globs). The order is host, item, rule, category, pack, then `icon`. New `Support\KindGlob` (`*`, `?`, whole-segment `**`; the contract's vector table is its test), `Support\KindResolver`, `Enums\KindSource` and the `final readonly` DTO `Data\KindResolution`; `PackConfigData::items()`/`kindRules()` read the new keys from the documented shape and the top level. `IconRegistry::kindOf()`, the renderer's size budget and `IconClassifier::kind()` all resolve through `KindResolver`; `IconRegistry::kindFor()` keeps its category/pack behaviour for its callers. A v0.1 `config.json` with no kind keys loads exactly as before. `tests/Unit/KindGlobTest.php`, `tests/Feature/KindResolutionTest.php`.
Expand Down Expand Up @@ -39,6 +46,18 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan

### Changed

- **A pack's `config.defaults` applies wherever the pack renders** (P-06). The registry applies `config.defaults.class`, `.attributes`, `.stroke_width` (as `stroke-width`) and `.fallback` to the set it builds, so `<x-ichava::icon>`, the pack's own tag and `IconRegistry::render()` draw the same attributes. Before, a pack's defaults lived in its `IconComponent` subclass and reached its own tag only. `tests/Feature/PackConfigDefaultsTest.php`.
- **The pack base providers live in `Providers\`** (C-07): `Support\IconPackServiceProvider` is `Providers\IconPackServiceProvider`, and `Support\ServiceProvider` is `Providers\PackServiceProvider`. No alias is left at the old names; a pack changes one `use` line. `tests/Unit/ProvidersNamespaceTest.php`.
- **`custom-icons.sets` keys take effect** (C-09): `enabled`, `prefix`, `default_class`, `default_attributes`, `fallback`, `variants`, `default_variant`, `supports_categories`, `blade_namespace`, `blade_component`, plus `package` for a directory without a `config.json` and `bulk`/`only` for several sets. Before, only `path` was read. `tests/Feature/CustomIconSetsTest.php`.
- **`registerBulkIconSets()`/`IchavaRegistrar` apply what they are given** (C-10): each set's values are applied as builder overrides, an empty map registers every set, the five write-only settings gain readers, `statistics()` returns the run's `BulkResult`, and `enableLogging()` logs before or after `configure()`.
- **`package()` registrations carry the full metadata** (C-03), the same as `fromDirectory()`.
- **Actions expose one `handle()`** (C-13): `BuildIconUsageSyntax`, `ClearDiscoveryCaches`, `DiscoverInstalledPackages` and `SearchIconsInDatabase` no longer use `__invoke()`. `Actions\CountSvgFiles` is `Support\SvgFileCounter`. `ClearDiscoveryCaches::generation()` is `DiscoveryCacheGeneration::current()`. `tests/Unit/ActionShapeTest.php`.
- **Concrete classes are `final` by default** (C-14): 19 classes nothing extends or mocks; `tests/Unit/FinalByDefaultTest.php` lists the 7 that stay open and why.
- **Collaborators are injected** (C-05): `IconRegistry` takes `IconCacheService`, `Filesystem` and `SvgFileCounter`; `IchavaSessionManager` takes `IchavaLogger`; the watcher no longer reaches `ConfigurationService`. `tests/Unit/ContainerReachInTest.php`, `tests/Feature/WatcherCacheDecouplingTest.php`.
- **One reader of `config.json` per concern** (C-04): the terms seeder seeds variants from the registry's `variant_metadata`, and `ConfigurationService` parses through `Helpers::loadConfigJson()`.
- **Served SVGs are read through `SvgDriver::load()`** (C-12), so the model path refuses a symlink and a file under `MIN_SVG_FILE_SIZE` as the driver does. 0 of 246,854 pack SVGs are under the minimum or symlinked. `tests/Feature/SvgModelReadsThroughDriverTest.php`.
- **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 `<svg>` or `<g>` and keeps the indentation; text in `<title>`, `<desc>`, `<text>` and `<tspan>` 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.
Expand All @@ -50,6 +69,8 @@ All notable changes to `ichava/core` follow [Keep a Changelog](https://keepachan

### Removed

- **Forward-only renames, with no alias left behind** (owner, 2026-11-05): `Support\IconPackServiceProvider` and `Support\ServiceProvider` (now `Providers\IconPackServiceProvider`, `Providers\PackServiceProvider`), `Actions\CountSvgFiles` (now `Support\SvgFileCounter`), `ClearDiscoveryCaches::generation()` (now `DiscoveryCacheGeneration::current()`), and `__invoke()` on four actions (now `handle()`). Every caller in the estate moves in the same sweep; each is recorded in the deprecations audit.
- **The bare `icon` morph alias** (now `ichava-icon`; the migration moves stored rows).
The estate is forward-only (owner, 2026-11-05: "never support legacy only forward"). Nothing has been released, so these compatibility paths go rather than stay beside their replacements.

- **The top-level `variants` / `categories` / `items` / `kinds` fallback in `config.json`.** `PackConfigData` reads `metadata.data` only, the shape every pack and the scaffolder's stub ship; doctor's kind checks always name `metadata.data.…` keys. No `config.json` in the estate used the top level (43 read; the one hit was the test fixture that pinned the fallback, which now pins that the top level is ignored).
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,28 @@ Render an icon with the Blade component, which works with any installed pack:
<x-ichava-core::icon name="ichava/icon-sets-tabler::outline/home" class="w-6 h-6" />
```

`<x-ichava::icon>` is the same component under the ecosystem prefix and keeps working.

Or with the fluent helper, which renders when cast to a string:
`<x-ichava::icon>` is the same component under the configurable short prefix (`components.short_prefix`), on by default. Or use the fluent helper, which renders when cast to a string:

```blade
{{ ichava('ichava/icon-sets-tabler::filled/home')->color('#4338ca')->class('w-5 h-5') }}
```

Ask what an SVG is, and which rule decided it:
Register a set of your own, from a pack provider's optional `pack()` hook or straight from code:

```php
use Simtabi\Laranail\Ichava\Facades\IchavaFacade as Ichava;

Ichava::kindOf('ichava/icon-sets-tabler::outline/home'); // IconKind::Icon
Ichava::kindResolution('ichava/icon-sets-tabler::outline/home'); // kind, source (host, item, rule, category, pack, default) and rule
use Simtabi\Laranail\Ichava\Services\IconRegistry;
use Simtabi\Laranail\Ichava\Support\Pack\PackBuilder;

app(IconRegistry::class)->register(
PackBuilder::make('acme/social', storage_path('icons/social/files'))
->prefix('social')
->variant('outline', ['fill' => 'none', 'stroke' => 'currentColor'])
->build(),
);
```

A pack that keeps everything in its `config.json` needs none of this: its provider names its constants and component classes, and [Pack registration](docs/tools/pack-registration.md) covers both paths and bulk registration.

See [Getting started](docs/getting-started.md) for the full walkthrough, and the [documentation index](#documentation) for everything else.

## <a name="documentation"></a>Documentation
Expand All @@ -84,7 +89,8 @@ The full documentation lives in this repository, under `docs/`, and is indexed b
### Reference

- [Icon path format](docs/tools/icon-path-format.md) — the one normaliser both spellings go through
- [Blade components](docs/tools/blade-components.md) — `<x-ichava::icon>` and its attributes
- [Blade components](docs/tools/blade-components.md) — `<x-ichava-core::icon>`, the short tag, and the attributes
- [Pack registration](docs/tools/pack-registration.md) — the simple and advanced pack paths, `PackBuilder`, and bulk registration
- [Global helper](docs/tools/global-helper.md) — the fluent `ichava()` API
- [Artisan commands](docs/tools/artisan-commands.md) — every command core registers
- [Query and model API](docs/tools/query-api.md) — scopes, filters, events, status and the extension interfaces
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"laravel/pint": "^1.18",
"orchestra/testbench": "^11.0",
"pestphp/pest": "^5.0",
"phpstan/phpstan": "^2.1"
"phpstan/phpstan": "^2.1",
"symfony/yaml": "^7.4 || ^8.0"
},
"conflict": {
"symfony/http-foundation": "<7.4.23 || >=8.0,<8.1.11",
Expand All @@ -65,7 +66,8 @@
"Simtabi\\Laranail\\Ichava\\Tests\\": "tests/",
"Simtabi\\Laranail\\Ichava\\Tests\\Fixtures\\BasePack\\": "tests/fixtures/BasePack/src/",
"Simtabi\\Laranail\\Ichava\\Tests\\Fixtures\\TranslatedPack\\": "tests/fixtures/TranslatedPack/src/",
"Simtabi\\Laranail\\Ichava\\Tests\\Fixtures\\ViewfulPack\\": "tests/fixtures/ViewfulPack/src/"
"Simtabi\\Laranail\\Ichava\\Tests\\Fixtures\\ViewfulPack\\": "tests/fixtures/ViewfulPack/src/",
"Simtabi\\Laranail\\Ichava\\Tests\\Fixtures\\SpecPack\\": "tests/fixtures/SpecPack/src/"
}
},
"extra": {
Expand Down
15 changes: 12 additions & 3 deletions config/ichava-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -289,7 +289,16 @@
|--------------------------------------------------------------------------
*/
'components' => [
'enabled' => env('ICHAVA_COMPONENTS_ENABLED', true),
// Core's whole Blade surface. false registers neither tag below.
'enabled' => env('ICHAVA_COMPONENTS_ENABLED', true),

// `<x-ichava-core::icon>` is always registered. This is the short
// prefix registered beside it: `<x-ichava::icon>` by default, any
// lowercase prefix Blade can parse (letters, digits, hyphens), or
// null/'' to turn it off. Packs follow it: `<x-icon-sets-tabler-icon>`
// beside the canonical `<x-ichava-icon-sets-tabler::icon>`.
'short_prefix' => env('ICHAVA_COMPONENT_SHORT_PREFIX', 'ichava'),

'test_icons' => env('ICHAVA_TEST_COMPONENT_ENABLED', env('APP_ENV') === 'local'),
],

Expand Down
Loading
Loading