@dynamic-icon/core 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -3
- package/dist/config.d.ts +15 -0
- package/dist/config.js +98 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +1 -0
- package/llms.txt +56 -0
- package/package.json +37 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dynamic-icon contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IconConfig, ResolvedIconConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sets the icon configuration. Call once at app start (module scope of your root/hooks file, not inside a
|
|
4
|
+
* component), identically on server and client. Replaces any previous configuration; throws on invalid
|
|
5
|
+
* input and keeps the previous configuration in that case.
|
|
6
|
+
*/
|
|
7
|
+
export declare function configureIcons<C = unknown>(config: IconConfig<C>): void;
|
|
8
|
+
/**
|
|
9
|
+
* The current configuration (defaults apply until `configureIcons` is called).
|
|
10
|
+
* `C` is a type assertion: the registry stores whatever `configureIcons` was given, so framework wrappers
|
|
11
|
+
* must be the only writers for their `C`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getIconConfig<C = unknown>(): ResolvedIconConfig<C>;
|
|
14
|
+
/** Resolves a component for `webType`/`webRef`, or `undefined`. Usable outside components. */
|
|
15
|
+
export declare function resolveIcon<C = unknown>(webType: string | undefined, webRef: string | undefined): C | undefined;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** `webType`s handled by the renderer itself and therefore not usable as source names. */
|
|
2
|
+
const RESERVED = new Set(["image", "emoji", "markup"]);
|
|
3
|
+
const DEFAULT_FONT_TYPES = ["material", "font-awesome", "remix"];
|
|
4
|
+
function positive(name, value) {
|
|
5
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
6
|
+
throw new RangeError(`[dynamic-icon] ${name} must be a positive finite number, got ${value}`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function toResolver(type, source, report) {
|
|
11
|
+
if (typeof source === "function") {
|
|
12
|
+
return (ref) => {
|
|
13
|
+
try {
|
|
14
|
+
return source(ref);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
report(error);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (source !== null && typeof source === "object") {
|
|
23
|
+
// Held by reference, never copied: no extra memory, and module namespaces work as-is. `hasOwn` keeps
|
|
24
|
+
// identifiers such as "constructor" or "toString" from reaching Object.prototype. Treat the object as
|
|
25
|
+
// read-only: later mutations are visible to lookups.
|
|
26
|
+
return (ref) => (Object.hasOwn(source, ref) ? source[ref] : undefined);
|
|
27
|
+
}
|
|
28
|
+
throw new TypeError(`[dynamic-icon] source "${type}" must be a record or a function`);
|
|
29
|
+
}
|
|
30
|
+
function build(config) {
|
|
31
|
+
if (config.sanitize !== undefined && typeof config.sanitize !== "function") {
|
|
32
|
+
throw new TypeError("[dynamic-icon] sanitize must be a function");
|
|
33
|
+
}
|
|
34
|
+
if (config.onError !== undefined && typeof config.onError !== "function") {
|
|
35
|
+
throw new TypeError("[dynamic-icon] onError must be a function");
|
|
36
|
+
}
|
|
37
|
+
const report = config.onError ?? ((error) => console.error("[dynamic-icon]", error));
|
|
38
|
+
const resolvers = new Map();
|
|
39
|
+
for (const [type, source] of Object.entries(config.sources ?? {})) {
|
|
40
|
+
if (RESERVED.has(type)) {
|
|
41
|
+
throw new Error(`[dynamic-icon] "${type}" is a built-in type and cannot be used as a source name`);
|
|
42
|
+
}
|
|
43
|
+
resolvers.set(type, toResolver(type, source, report));
|
|
44
|
+
}
|
|
45
|
+
const fontTypes = new Set(config.fontTypes ?? DEFAULT_FONT_TYPES);
|
|
46
|
+
for (const type of fontTypes) {
|
|
47
|
+
if (RESERVED.has(type) || resolvers.has(type)) {
|
|
48
|
+
throw new Error(`[dynamic-icon] "${type}" cannot be both a font type and a source/built-in type`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const { sanitize } = config;
|
|
52
|
+
return {
|
|
53
|
+
resolve: (webType, webRef) => (webType && webRef ? resolvers.get(webType)?.(webRef) : undefined),
|
|
54
|
+
fontTypes,
|
|
55
|
+
sanitize: sanitize
|
|
56
|
+
? (html) => {
|
|
57
|
+
try {
|
|
58
|
+
return sanitize(html);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
report(error);
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
: undefined,
|
|
66
|
+
fallback: config.fallback,
|
|
67
|
+
size: positive("defaults.size", config.defaults?.size ?? 24),
|
|
68
|
+
strokeWidth: positive("defaults.strokeWidth", config.defaults?.strokeWidth ?? 2)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// One registry per realm. The holder is created once and captured by this module, so every read is a plain
|
|
72
|
+
// property access. `Symbol.for` makes duplicate copies of this package (version skew, nested installs) share
|
|
73
|
+
// the same holder instead of silently keeping separate registries. The `@1` is the holder's format version:
|
|
74
|
+
// bump it on a breaking change so incompatible copies never share state. The config is immutable and
|
|
75
|
+
// identical for every SSR request, so a process-wide singleton needs no per-request isolation.
|
|
76
|
+
const KEY = Symbol.for("@dynamic-icon/core@1");
|
|
77
|
+
const realm = globalThis;
|
|
78
|
+
const holder = (realm[KEY] ??= { config: build({}) });
|
|
79
|
+
/**
|
|
80
|
+
* Sets the icon configuration. Call once at app start (module scope of your root/hooks file, not inside a
|
|
81
|
+
* component), identically on server and client. Replaces any previous configuration; throws on invalid
|
|
82
|
+
* input and keeps the previous configuration in that case.
|
|
83
|
+
*/
|
|
84
|
+
export function configureIcons(config) {
|
|
85
|
+
holder.config = build(config);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The current configuration (defaults apply until `configureIcons` is called).
|
|
89
|
+
* `C` is a type assertion: the registry stores whatever `configureIcons` was given, so framework wrappers
|
|
90
|
+
* must be the only writers for their `C`.
|
|
91
|
+
*/
|
|
92
|
+
export function getIconConfig() {
|
|
93
|
+
return holder.config;
|
|
94
|
+
}
|
|
95
|
+
/** Resolves a component for `webType`/`webRef`, or `undefined`. Usable outside components. */
|
|
96
|
+
export function resolveIcon(webType, webRef) {
|
|
97
|
+
return holder.config.resolve(webType, webRef);
|
|
98
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { configureIcons, getIconConfig, resolveIcon } from "./config.js";
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where components for one `webType` come from. `C` is the framework's component type.
|
|
3
|
+
* - A record keyed by the identifier stored in the database: `{ 'car-front': CarFront }`.
|
|
4
|
+
* - A function: full control (lazy tables, key normalisation, memoisation, ...).
|
|
5
|
+
*/
|
|
6
|
+
export type IconSource<C = unknown> = Readonly<Record<string, C>> | ((ref: string) => C | undefined);
|
|
7
|
+
export interface IconConfig<C = unknown> {
|
|
8
|
+
/** Component sources by `webType`. Reserved names: `image`, `emoji`, `markup`. */
|
|
9
|
+
sources?: Readonly<Record<string, IconSource<C>>>;
|
|
10
|
+
/** `webType`s rendered as `<i class={webRef}>` (icon fonts). Default: material, font-awesome, remix. */
|
|
11
|
+
fontTypes?: readonly string[];
|
|
12
|
+
/** Required for `markup`: raw HTML is only rendered after passing through this. */
|
|
13
|
+
sanitize?: (html: string) => string;
|
|
14
|
+
/** Rendered when nothing else resolves (and no per-instance fallback is given). */
|
|
15
|
+
fallback?: C;
|
|
16
|
+
/** Defaults for `size` (px, default 24) and `strokeWidth` (default 2). */
|
|
17
|
+
defaults?: {
|
|
18
|
+
size?: number;
|
|
19
|
+
strokeWidth?: number;
|
|
20
|
+
};
|
|
21
|
+
/** Receives errors thrown by function sources or `sanitize`. Default: `console.error`. The icon renders as unresolved. */
|
|
22
|
+
onError?: (error: unknown) => void;
|
|
23
|
+
}
|
|
24
|
+
/** Validated configuration built by `configureIcons`. Record sources are referenced, not copied. */
|
|
25
|
+
export interface ResolvedIconConfig<C = unknown> {
|
|
26
|
+
/** Component for `webType`/`webRef`, or `undefined`. Never throws. */
|
|
27
|
+
readonly resolve: (webType: string | undefined, webRef: string | undefined) => C | undefined;
|
|
28
|
+
readonly fontTypes: ReadonlySet<string>;
|
|
29
|
+
/** Never throws: returns `''` if the user's sanitizer throws. */
|
|
30
|
+
readonly sanitize: ((html: string) => string) | undefined;
|
|
31
|
+
readonly fallback: C | undefined;
|
|
32
|
+
readonly size: number;
|
|
33
|
+
readonly strokeWidth: number;
|
|
34
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/llms.txt
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @dynamic-icon/core
|
|
2
|
+
|
|
3
|
+
Framework-independent icon registry. It maps `webType` + `webRef` pairs stored in a database (`lucide` + `car-front`)
|
|
4
|
+
to components, and holds the fallback, defaults and markup sanitizer. **Zero dependencies, no DOM, no framework**
|
|
5
|
+
(compiled with `lib: ES2022` only). The component type is a generic parameter, so any UI framework can reuse it;
|
|
6
|
+
[`@dynamic-icon/svelte`](../svelte) is the Svelte 5 binding and re-exports everything you need.
|
|
7
|
+
|
|
8
|
+
Most apps never import this package directly; they use `configureIcons` from `@dynamic-icon/svelte`.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { configureIcons, resolveIcon } from '@dynamic-icon/core';
|
|
12
|
+
|
|
13
|
+
configureIcons<MyComponent>({
|
|
14
|
+
sources: {
|
|
15
|
+
lucide: { 'car-front': CarFront }, // record keyed by the DB identifier (held by reference)
|
|
16
|
+
svg: (ref) => customIcons[toPascal(ref)] // or a function: full control, memoise it yourself
|
|
17
|
+
},
|
|
18
|
+
fallback: CircleHelp,
|
|
19
|
+
defaults: { size: 24, strokeWidth: 2 },
|
|
20
|
+
sanitize: (html) => DOMPurify.sanitize(html), // required for `markup`
|
|
21
|
+
onError: (e) => report(e) // function sources / sanitize that throw; default console.error
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
resolveIcon<MyComponent>('lucide', 'car-front'); // CarFront | undefined
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Behaviour
|
|
28
|
+
|
|
29
|
+
- `configureIcons` validates eagerly (reserved names `image`/`emoji`/`markup`, non-positive defaults, wrong types), **throws and
|
|
30
|
+
keeps the previous configuration** on error, and otherwise replaces the whole configuration.
|
|
31
|
+
- Record sources are **held by reference, never copied** (no extra memory; module namespaces work as-is). Lookups use
|
|
32
|
+
`Object.hasOwn`, so `constructor` / `__proto__` never resolve to `Object.prototype` members. Treat source objects as read-only:
|
|
33
|
+
later mutations are visible to lookups (and, on a server, would be shared across requests).
|
|
34
|
+
- Function sources and `sanitize` are wrapped: a throw is reported to `onError` and the icon renders as unresolved.
|
|
35
|
+
- The registry lives on `globalThis[Symbol.for('@dynamic-icon/core@1')]` and is captured once per module, so reads are a plain
|
|
36
|
+
property access. Duplicate installs of this package share one registry. The `@1` is the holder's format version.
|
|
37
|
+
- SSR: the configuration is app-wide data that never changes per request, identical for every request, so a process-wide singleton is safe.
|
|
38
|
+
Call `configureIcons` once at module scope (e.g. `hooks.server.ts` / root `+layout.ts`), never per request or per component.
|
|
39
|
+
|
|
40
|
+
> Framework-independent, zero-dependency icon registry. Maps `webType` + `webRef` strings from a database to components, and stores fallback, defaults, a markup sanitizer and an error hook. The component type is a generic parameter `C`. For Svelte 5 use `@dynamic-icon/svelte`, which re-exports a typed `configureIcons` / `resolveIcon` over this package.
|
|
41
|
+
|
|
42
|
+
Key facts:
|
|
43
|
+
- API: `configureIcons<C>(config)`, `resolveIcon<C>(webType, webRef): C | undefined`, `getIconConfig<C>()`.
|
|
44
|
+
- `configureIcons` validates eagerly, throws (keeping the previous config) on invalid input, and replaces the whole configuration.
|
|
45
|
+
- Record sources are held by reference (never copied; looked up with `Object.hasOwn`, so prototype keys cannot resolve; treat them as read-only); function sources and `sanitize` never throw to the caller (errors go to `onError`, default `console.error`).
|
|
46
|
+
- State lives on `globalThis[Symbol.for('@dynamic-icon/core@1')]`, captured once per module; duplicate installs share one registry.
|
|
47
|
+
- Reserved `webType` names (not allowed as source names): `image`, `emoji`, `markup`.
|
|
48
|
+
|
|
49
|
+
## Docs
|
|
50
|
+
|
|
51
|
+
- [Full documentation (Svelte binding + core)](https://unpkg.com/@dynamic-icon/svelte/llms-full.txt)
|
|
52
|
+
- [README](https://unpkg.com/@dynamic-icon/core/README.md)
|
|
53
|
+
|
|
54
|
+
## Optional
|
|
55
|
+
|
|
56
|
+
- Same files via jsDelivr: https://cdn.jsdelivr.net/npm/@dynamic-icon/core/llms.txt
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamic-icon/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Framework-independent icon registry: maps database identifiers to components. Zero dependencies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./llms.txt": "./llms.txt",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"llms.txt"
|
|
22
|
+
],
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"publint": "^0.2.12",
|
|
25
|
+
"typescript": "^5.6.0",
|
|
26
|
+
"vitest": "^2.1.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"lint:package": "publint"
|
|
39
|
+
}
|
|
6
40
|
}
|