feat(resources): redesign asset descriptors and type resolution - #420
Merged
Conversation
added 13 commits
July 27, 2026 02:41
…criptor surface DefineAssetDescriptor/AssetBinding's constructor-reference field is now `ctor` (was `type`), and the string kind-tag field is now `type` (was `kind`), across Asset/AssetImpl, AnyAssetConfig, AssetTypeRegistry.bindAsset, defineAsset, coreAssetBindings, and every consumer (Loader, Assets, AssetResidency, materializeAssetBindings). Foundation task for the asset-loading API redesign.
… guides, examples, api docs
Task 1's descriptor rename left packages/exojs-{tiled,aseprite,ldtk} on the old
DefineAssetDescriptor/AssetBinding shape, breaking pnpm verify:quick: package
typecheck errors, guide/example snippets still calling Asset.kind(...), and a
TypeDoc entry-point failure that aborted API-doc generation. Applies the same
two renames (string kind-tag -> type, constructor reference -> ctor) across
the three packages' src/ and test/, their READMEs, every affected guide MDX
and top-level example (.ts + regenerated .js), and regenerates site/src/content/api.
Also fixes a real runtime bug uncovered by running the package test suites:
several test mocks branched on a type-erased `asset.kind` check that the
rename silently broke (asset._config now carries `type`, not `kind`).
…tTypeRegistry AssetTypeRegistry.registerType()/resolveExtensionType() let a Loader override which AssetDefinitions type a file extension resolves to, app-local and override-first ahead of the global extensionKindRegistry default. bindAsset() now accepts an optional `type` and writes its extensions into the same override table so defineAsset-based bindings and explicit registerType() calls conflict-check consistently. Loader.registerType() exposes it publicly.
…terType coverage AssetBinding.type's JSDoc claimed materializeAssetBindings doesn't consume it, which the Task 2 wiring in materialize.ts already contradicted — it now forwards type into Loader.bindAsset to populate the per-app extension override table. Also notes the same on AssetTypeRegistry.bindAsset's docstring and adds a Loader.registerType() pass-through unit test.
… registration path Loader.register()/AssetFactory were a parallel, unused registration path alongside bindAsset()/defineAsset() (the paved path since the asset-system v2 rework) — every core binding already went through bindAsset via coreAssetBindings.ts, so the factory-fallback dispatch in AssetDecoder was dead for first-party types. Removes Loader.register(), AssetDecoder._fetch() and its hasFactory/resolveFactory fallback branch in _dispatchFetch/_injectSource, and AssetTypeRegistry's register()/hasFactory()/resolveFactory()/ destroyFactories(). FactoryRegistry.ts/AssetFactory.ts/AbstractAssetFactory.ts and the concrete factories/*.ts implementations stay — they're still used internally by coreAssetBindings.ts's binaryFactoryHandler/textFactoryHandler. Closes the one real capability gap register() had — a per-type IDB cache namespace — by adding an optional storageName to defineAsset/bindAsset, threaded through AssetBinding -> Loader.bindAsset -> AssetTypeRegistry's HandlerEntry -> AssetDecoder's context.fetchText/fetchArrayBuffer/fetchJson namespace selection. test/resources/loader.test.ts's register()-based TextAsset fixture is replaced with a bindAsset handler routed through context.fetchText; the corrupt-cache-entry delete+retry behavior it incidentally covered (now unreachable through the context.fetchText identity-factory path) gets its own direct CacheFirstStrategy unit tests in the new test/resources/cache-first-strategy.test.ts.
…no-handler paths Task review found _loadSingleAsset's two new behaviors from the AssetResidency _fetch() fix (storageName threaded into _buildHandlerContext, and rejecting with a clear error instead of the removed factory fallback when no handler is bound) had no test coverage, unlike the identical logic in AssetDecoder. _dispatchFetch. Adds the two missing tests, mirroring asset-decoder.test.ts's existing pattern. Also: extracted the duplicated "No asset handler registered..." error string (built independently in AssetDecoder and AssetResidency) into a shared AssetTypeRegistry._missingHandlerError(type) helper, and fixed a stale Loader.hasLoadable() doc comment that still said "handler or factory" after the factory path was removed.
…input shape
Bare paths now normalize to a canonical { type, source } descriptor through
AssetTypeRegistry._resolveTypeForPath, which walks the basename's dot-suffixes
longest-first and consults the app-local registerType override before the global
defineAsset default. get()/load() drop from seven overloads each to five: the
two-argument constructor lookup form and the ExtensionTypeMap/LoadByPath-based
path overloads are gone, leaving one bare-path signature typed through
KindByPath/LeafForPath.
- Loader.get(Type, alias) and its SceneLoader mirror are removed; the residency
read accessor _getStored that only backed it goes with them.
- load('x.png') now infers Texture instead of unknown (the legacy generic
overload used to win and erase the return type).
- Non-leaf resource types (bmFont, font, svg, image, music, video) no longer
accept a bare path at compile time, matching get() and Assets.from(); name
them with Asset.type(...). The runtime still resolves them for dynamic strings.
- An extension bound via bindAsset without a type reserves the suffix but no
longer participates in bare-path resolution.
…ndMap
Task 4 review follow-up (two Important findings).
1. exojs-tiled and exojs-ldtk augmented ExtensionTypeMap, which no longer feeds
any call signature after the get()/load() consolidation — the surviving
bare-path overload reads KindByPath -> ExtensionKindMap. Both packages'
documented shorthands (load('world.tmj') / load('world.ldtk')) therefore
resolved to a never parameter for consumers. Runtime was unaffected: bindAsset
already writes each binding's extensions into the app override table.
ExtensionKindMap and KindByPath were not exported from the root index, so the
augmentation the new JSDoc tells consumers to write was not actually
reachable; both are exported now.
Pinned by test/type-tests/package-bare-path-inference.type-test.ts, compiled
by typecheck:type-tests. Package test/ dirs are excluded from their own
tsconfig, so an assertion placed there would never have been gated; verified
by reverting the tiled augmentation and confirming the gate fails with
"Argument of type '\"world.tmj\"' is not assignable to parameter of type 'never'".
2. The rewritten non-leaf bare-path branch (font family inference from the
filename, bmFont dispatch) had no coverage through Loader.load(path), since
those suffixes are never at the type level. Added four tests using the same
'as never' cast the rest of the file uses; verified the font assertions fail
when the family-inference condition is mutated away.
…kind
Task 4 review follow-up round 2.
Round 1 moved tmj/ldtk into ExtensionKindMap, which correctly fixed
load('world.tmj') but also made those suffixes reachable through get()'s
bare-path overload. LeafForPath decides whether to wrap the leaf in AssetRef by
testing membership in ValueAssetKind, which was a hardcoded 8-member union of
core kinds — so get('world.tmj') typed as bare TileMap while defineAsset's
runtime default (isValue ?? seamless === undefined) makes it an AssetRef<TileMap>.
A consumer calling loader.get('world.tmj').someMethod() compiled and crashed.
ValueAssetKind is now CoreValueAssetKind (the unchanged 8 built-ins) unioned
with the kinds a declaration-merged AssetDefinitions entry marks isValue: true.
The marker is co-located with the resource/config it describes and named after
the defineAsset option it mirrors, so no new exported interface or second
augmentation block is needed — ValueAssetKind is not exported from the root, so
nothing about the public surface changed.
Loader._valueTokenByKind is re-keyed to CoreValueAssetKind. It maps kinds to
built-in dispatch tokens that only exist for core types, and keeping it on the
now-extensible union would have made every build that sees a package
augmentation demand a token that cannot exist.
tileMap, tiledMap, ldtkMap and asepriteSheet all ship without a seamless
adapter, so all four are marked. Verified fail-before/pass-after by dropping the
tileMap marker: the get() and catalog-leaf assertions fail while the load()
assertion still passes, which is the correct split. Two runtime tests pin the
defineAsset default the marker mirrors, so the two halves cannot drift silently.
Also adds the missing @codexo/exojs-aseprite path mapping to
tsconfig.examples.json, without which the aseprite assertions could not resolve.
…document onError as the production diagnostic contract
…th a seeded-random fuzz test The three static tests duplicated coverage already in loader-claims.test.ts, asset-ref.test.ts, loader-seamless.test.ts, and loader.test.ts, and never exercised the idle readiness state or true claim/fetch interleaving. Replaced with one SG-006-style seeded PRNG walk (200 steps) over two disjoint key channels — get()/_adopt claim residency and load() store-before-fetch dedup — checking claim-count/eviction and store/fetch invariants after every step.
Bundle ReportChanges will increase total bundle size by 15.66kB (0.06%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: exo-esm-modules-esmAssets Changed:
view changes for bundle: exo-iife-min-Exo-iifeAssets Changed:
view changes for bundle: exojs-tiled-esmAssets Changed:
view changes for bundle: exojs-ldtk-esmAssets Changed:
view changes for bundle: exo-esm-esmAssets Changed:
view changes for bundle: exo-full-iife-Exo-iifeAssets Changed:
view changes for bundle: exojs-aseprite-esmAssets Changed:
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Exoridus
marked this pull request as ready for review
July 27, 2026 00:58
Exoridus
enabled auto-merge (squash)
July 27, 2026 00:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
type,ctorundAsset.type(...)Loader.register()-/AssetFactory-Pfadget()undload()auf die kanonische Descriptor-NormalisierungResolution precedence
registerType()-OverridedefineAsset-DefaultImportant behavior
Asset.type(...)wirkt nur auf den jeweiligen Aufrufget()meldet asynchrone Fehler über Zustand undonErrorload()rejected und meldet denselben Fehler überonErrorTest plan
pnpm typecheckpnpm typecheck:type-testspnpm docs:api:checkpnpm verify:quickpnpm testReview note
Der Branch wurde nach Abschluss eines Whole-Branch-Reviews auf aktuelles
main(bef47f39) rebased. PR #419 war dort als Squash-Commit integriert; die Patch-ID des Squash-Commits entspricht exakt der ursprünglichen zweigliedrigen SystemRegistry-Serie. Die 13 Asset-System-Commits wurden konfliktfrei und ohne Reordering auf diesen Stand gesetzt.Lokal bestehen keine bekannten offenen Critical-, High- oder Medium-Findings.