@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 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, 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
- }
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.onError !== undefined && typeof config.onError !== "function") {
35
- throw new TypeError("[dynamic-icon] onError must be a function");
28
+ if (config.onLog !== undefined && typeof config.onLog !== "function") {
29
+ throw new TypeError("[dynamic-icon] onLog must be a function");
36
30
  }
37
- const report = config.onError ?? ((error) => console.error("[dynamic-icon]", error));
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, report));
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) => (webType && webRef ? resolvers.get(webType)?.(webRef) : undefined),
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
- report(error);
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
- /** Receives errors thrown by function sources or `sanitize`. Default: `console.error`. The icon renders as unresolved. */
22
- onError?: (error: unknown) => void;
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. 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`.
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
- - [Full documentation (Svelte binding + core)](https://unpkg.com/@dynamic-icon/svelte/llms-full.txt)
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynamic-icon/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Framework-independent icon registry: maps database identifiers to components. Zero dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",