@dynamic-icon/core 0.1.1 → 0.1.2
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/README.md +16 -0
- package/dist/config.js +60 -18
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +13 -3
- package/llms.txt +47 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1 +1,17 @@
|
|
|
1
1
|
# @dynamic-icon/core
|
|
2
|
+
|
|
3
|
+
> Experimental package — do not use in production.
|
|
4
|
+
|
|
5
|
+
Framework-independent icon registry behind [`@dynamic-icon/svelte`](../svelte): maps `webType` + `webRef` strings from a
|
|
6
|
+
database to components. Zero dependencies, no DOM, no framework. Most apps use `configureIcons` from `@dynamic-icon/svelte`
|
|
7
|
+
and never import this package directly.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { configureIcons, resolveIcon } from '@dynamic-icon/core';
|
|
11
|
+
|
|
12
|
+
configureIcons<MyComponent>({ sources: { lucide: { 'car-front': CarFront } } });
|
|
13
|
+
resolveIcon<MyComponent>('lucide', 'car-front'); // CarFront | undefined
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Full reference** (options, logging, registry, SSR rules): [`llms.txt`](./llms.txt), shipped in the npm package, e.g.
|
|
17
|
+
`https://unpkg.com/@dynamic-icon/core/llms.txt`.
|
package/dist/config.js
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
/** `webType`s handled by the renderer itself and therefore not usable as source names. */
|
|
2
2
|
const RESERVED = new Set(["image", "emoji", "markup"]);
|
|
3
3
|
const DEFAULT_FONT_TYPES = ["material", "font-awesome", "remix"];
|
|
4
|
+
/** Distinct messages reported per configuration; protects against unbounded growth from bad data. */
|
|
5
|
+
const MAX_LOGGED = 500;
|
|
6
|
+
const defaultLog = ({ type, message, error }) => error === undefined ? console[type](message) : console[type](message, error);
|
|
4
7
|
function positive(name, value) {
|
|
5
8
|
if (!Number.isFinite(value) || value <= 0) {
|
|
6
9
|
throw new RangeError(`[dynamic-icon] ${name} must be a positive finite number, got ${value}`);
|
|
7
10
|
}
|
|
8
11
|
return value;
|
|
9
12
|
}
|
|
10
|
-
function toResolver(type, source
|
|
11
|
-
if (typeof source === "function")
|
|
12
|
-
return
|
|
13
|
-
try {
|
|
14
|
-
return source(ref);
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
report(error);
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
}
|
|
13
|
+
function toResolver(type, source) {
|
|
14
|
+
if (typeof source === "function")
|
|
15
|
+
return source;
|
|
22
16
|
if (source !== null && typeof source === "object") {
|
|
23
17
|
// Held by reference, never copied: no extra memory, and module namespaces work as-is. `hasOwn` keeps
|
|
24
18
|
// identifiers such as "constructor" or "toString" from reaching Object.prototype. Treat the object as
|
|
@@ -31,16 +25,40 @@ function build(config) {
|
|
|
31
25
|
if (config.sanitize !== undefined && typeof config.sanitize !== "function") {
|
|
32
26
|
throw new TypeError("[dynamic-icon] sanitize must be a function");
|
|
33
27
|
}
|
|
34
|
-
if (config.
|
|
35
|
-
throw new TypeError("[dynamic-icon]
|
|
28
|
+
if (config.onLog !== undefined && typeof config.onLog !== "function") {
|
|
29
|
+
throw new TypeError("[dynamic-icon] onLog must be a function");
|
|
36
30
|
}
|
|
37
|
-
|
|
31
|
+
// Each distinct message is delivered once per configuration (so 1,000 icons with one typo log once, and a
|
|
32
|
+
// server does not log per request). A throwing `onLog` never reaches rendering.
|
|
33
|
+
const onLog = config.onLog ?? defaultLog;
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const emit = (entry) => {
|
|
36
|
+
try {
|
|
37
|
+
onLog(entry);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* logging must never break rendering */
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const log = (type, message, error) => {
|
|
44
|
+
if (seen.has(message))
|
|
45
|
+
return;
|
|
46
|
+
if (seen.size >= MAX_LOGGED) {
|
|
47
|
+
if (seen.size === MAX_LOGGED) {
|
|
48
|
+
seen.add(""); // marks the notice as sent
|
|
49
|
+
emit({ type: "warn", message: "[dynamic-icon] too many distinct messages; further ones are suppressed" });
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
seen.add(message);
|
|
54
|
+
emit(error === undefined ? { type, message } : { type, message, error });
|
|
55
|
+
};
|
|
38
56
|
const resolvers = new Map();
|
|
39
57
|
for (const [type, source] of Object.entries(config.sources ?? {})) {
|
|
40
58
|
if (RESERVED.has(type)) {
|
|
41
59
|
throw new Error(`[dynamic-icon] "${type}" is a built-in type and cannot be used as a source name`);
|
|
42
60
|
}
|
|
43
|
-
resolvers.set(type, toResolver(type, source
|
|
61
|
+
resolvers.set(type, toResolver(type, source));
|
|
44
62
|
}
|
|
45
63
|
const fontTypes = new Set(config.fontTypes ?? DEFAULT_FONT_TYPES);
|
|
46
64
|
for (const type of fontTypes) {
|
|
@@ -50,7 +68,31 @@ function build(config) {
|
|
|
50
68
|
}
|
|
51
69
|
const { sanitize } = config;
|
|
52
70
|
return {
|
|
53
|
-
resolve: (webType, webRef) =>
|
|
71
|
+
resolve: (webType, webRef) => {
|
|
72
|
+
if (!webType || !webRef)
|
|
73
|
+
return undefined; // an empty record is not an error
|
|
74
|
+
const resolver = resolvers.get(webType);
|
|
75
|
+
if (resolver) {
|
|
76
|
+
try {
|
|
77
|
+
const component = resolver(webRef);
|
|
78
|
+
if (component === undefined)
|
|
79
|
+
log("warn", `[dynamic-icon] icon "${webRef}" not found in source "${webType}"`);
|
|
80
|
+
return component;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
log("error", `[dynamic-icon] source "${webType}" threw while resolving "${webRef}"`, error);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Not a source: only the built-in renderers are legitimate here, anything else is a data/config bug.
|
|
88
|
+
if (!RESERVED.has(webType) && !fontTypes.has(webType)) {
|
|
89
|
+
log("warn", `[dynamic-icon] unknown webType "${webType}" (not a source, built-in or font type); icon "${webRef}" not rendered`);
|
|
90
|
+
}
|
|
91
|
+
else if (webType === "markup" && !sanitize) {
|
|
92
|
+
log("error", '[dynamic-icon] "markup" icon not rendered: configureIcons({ sanitize }) is not set');
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
},
|
|
54
96
|
fontTypes,
|
|
55
97
|
sanitize: sanitize
|
|
56
98
|
? (html) => {
|
|
@@ -58,7 +100,7 @@ function build(config) {
|
|
|
58
100
|
return sanitize(html);
|
|
59
101
|
}
|
|
60
102
|
catch (error) {
|
|
61
|
-
|
|
103
|
+
log("error", "[dynamic-icon] sanitize threw; icon rendered empty", error);
|
|
62
104
|
return "";
|
|
63
105
|
}
|
|
64
106
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { configureIcons, getIconConfig, resolveIcon } from "./config.js";
|
|
2
|
-
export type { IconConfig, IconSource, ResolvedIconConfig } from "./types.js";
|
|
2
|
+
export type { IconConfig, IconLogEntry, IconSource, ResolvedIconConfig } from "./types.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* - A function: full control (lazy tables, key normalisation, memoisation, ...).
|
|
5
5
|
*/
|
|
6
6
|
export type IconSource<C = unknown> = Readonly<Record<string, C>> | ((ref: string) => C | undefined);
|
|
7
|
+
export interface IconLogEntry {
|
|
8
|
+
type: "error" | "warn";
|
|
9
|
+
message: string;
|
|
10
|
+
/** The original thrown value, when there is one. */
|
|
11
|
+
error?: unknown;
|
|
12
|
+
}
|
|
7
13
|
export interface IconConfig<C = unknown> {
|
|
8
14
|
/** Component sources by `webType`. Reserved names: `image`, `emoji`, `markup`. */
|
|
9
15
|
sources?: Readonly<Record<string, IconSource<C>>>;
|
|
@@ -18,15 +24,19 @@ export interface IconConfig<C = unknown> {
|
|
|
18
24
|
size?: number;
|
|
19
25
|
strokeWidth?: number;
|
|
20
26
|
};
|
|
21
|
-
/**
|
|
22
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Receives diagnostics: icons missing from a source, unknown `webType`s, `markup` without `sanitize`, and
|
|
29
|
+
* errors thrown by function sources or `sanitize` (`error` carries the original). Each distinct message is
|
|
30
|
+
* delivered once. Default: `console.warn` / `console.error`. Override to silence or route (e.g. dev only).
|
|
31
|
+
*/
|
|
32
|
+
onLog?: (entry: IconLogEntry) => void;
|
|
23
33
|
}
|
|
24
34
|
/** Validated configuration built by `configureIcons`. Record sources are referenced, not copied. */
|
|
25
35
|
export interface ResolvedIconConfig<C = unknown> {
|
|
26
36
|
/** Component for `webType`/`webRef`, or `undefined`. Never throws. */
|
|
27
37
|
readonly resolve: (webType: string | undefined, webRef: string | undefined) => C | undefined;
|
|
28
38
|
readonly fontTypes: ReadonlySet<string>;
|
|
29
|
-
/** Never throws: returns `''` if the user's sanitizer throws. */
|
|
39
|
+
/** Never throws: logs and returns `''` if the user's sanitizer throws. */
|
|
30
40
|
readonly sanitize: ((html: string) => string) | undefined;
|
|
31
41
|
readonly fallback: C | undefined;
|
|
32
42
|
readonly size: number;
|
package/llms.txt
CHANGED
|
@@ -1,54 +1,55 @@
|
|
|
1
1
|
# @dynamic-icon/core
|
|
2
2
|
|
|
3
|
-
Framework-independent icon registry.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- State
|
|
47
|
-
-
|
|
3
|
+
> Framework-independent, zero-dependency icon registry. Maps `webType` + `webRef` strings from a database to components, and stores fallback, defaults, a markup sanitizer and a log 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.
|
|
4
|
+
|
|
5
|
+
## API
|
|
6
|
+
|
|
7
|
+
- `configureIcons<C>(config: IconConfig<C>): void`: validates eagerly and replaces the whole configuration. On invalid input it throws and the previous configuration stays active.
|
|
8
|
+
- `resolveIcon<C>(webType, webRef): C | undefined`: the lookup the component uses. Never throws.
|
|
9
|
+
- `getIconConfig<C>(): ResolvedIconConfig<C>`: the current configuration (defaults apply until `configureIcons` is called).
|
|
10
|
+
|
|
11
|
+
## IconConfig
|
|
12
|
+
|
|
13
|
+
| Field | Type | Default | Meaning |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| `sources` | `Record<webType, Record<ref, C> \| (ref) => C \| undefined>` | `{}` | Component sources by `webType`. `image`, `emoji`, `markup` are reserved and throw. |
|
|
16
|
+
| `fontTypes` | `string[]` | `['material','font-awesome','remix']` | `webType`s rendered as `<i class={webRef}>` by the framework binding. Must not collide with sources or reserved names. |
|
|
17
|
+
| `sanitize` | `(html: string) => string` | none | Required for `markup`. Failures are logged and yield `''`. |
|
|
18
|
+
| `fallback` | `C` | none | Rendered by the framework binding when nothing resolves. |
|
|
19
|
+
| `defaults` | `{ size?: number; strokeWidth?: number }` | `{ size: 24, strokeWidth: 2 }` | Must be positive finite numbers or it throws. |
|
|
20
|
+
| `onLog` | `(entry: { type: 'error' \| 'warn'; message: string; error?: unknown }) => void` | `console.warn` / `console.error` | Receives diagnostics (below). Override to silence or route, e.g. dev only. |
|
|
21
|
+
|
|
22
|
+
## Sources
|
|
23
|
+
|
|
24
|
+
- A record is held **by reference, never copied** (no extra memory; module namespaces work as-is). Lookups use `Object.hasOwn`, so `constructor`, `__proto__`, `toString` never resolve. Treat the object as read-only: later mutations are visible to lookups, and on a server would be shared across requests.
|
|
25
|
+
- A function source gets full control (key normalisation, lazy tables, memoisation). It is the way to defer loading; there is no separate getter mode.
|
|
26
|
+
|
|
27
|
+
## Logging (`onLog`)
|
|
28
|
+
|
|
29
|
+
Reported by `resolve` (so for every icon the framework binding renders):
|
|
30
|
+
|
|
31
|
+
| When | type | message |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `webType` is a configured source but the ref is missing | `warn` | `icon "x" not found in source "lucide"` |
|
|
34
|
+
| `webType` is not a source, built-in (`image`, `emoji`, `markup`) or font type | `warn` | `unknown webType "x" ...` |
|
|
35
|
+
| `webType` is `markup` and no `sanitize` is configured | `error` | `"markup" icon not rendered: configureIcons({ sanitize }) is not set` |
|
|
36
|
+
| A function source throws | `error` (+ `error`) | `source "x" threw while resolving "y"` |
|
|
37
|
+
| `sanitize` throws | `error` (+ `error`) | `sanitize threw; icon rendered empty` |
|
|
38
|
+
|
|
39
|
+
Not logged: empty records (no `webType` or no `webRef`), built-in types, font types.
|
|
40
|
+
Each distinct message is delivered **once per configuration** (1,000 icons with one typo log once; a server does not log per request). After 500 distinct messages one suppression notice is sent and the rest are dropped. A throwing `onLog` is swallowed: logging never breaks rendering.
|
|
41
|
+
|
|
42
|
+
Dev-only logging in a Vite app: `onLog: import.meta.env.DEV ? undefined : () => {}` (`undefined` keeps the console default).
|
|
43
|
+
|
|
44
|
+
## Registry and SSR
|
|
45
|
+
|
|
46
|
+
- State is one holder object on `globalThis[Symbol.for('@dynamic-icon/core@1')]`, captured once per module so reads are plain property access. Duplicate installs share it; `@1` is the holder format version (bumped on breaking changes so incompatible copies never share state).
|
|
47
|
+
- The configuration is app-wide data that does not vary per request, so a process-wide singleton is safe on the server. Call `configureIcons` once at module scope, identically on server and client; never per request or per component. Per-request or per-tenant icon sets are not supported.
|
|
48
|
+
- Built with `lib: ES2022` only: no DOM, no Node typings, no dependencies.
|
|
48
49
|
|
|
49
50
|
## Docs
|
|
50
51
|
|
|
51
|
-
- [
|
|
52
|
+
- [Svelte binding and full documentation](https://unpkg.com/@dynamic-icon/svelte/llms-full.txt)
|
|
52
53
|
- [README](https://unpkg.com/@dynamic-icon/core/README.md)
|
|
53
54
|
|
|
54
55
|
## Optional
|