@dynamic-icon/core 0.0.0-stage → 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/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
@@ -1,3 +1,17 @@
1
- # Temporary Holding Version
1
+ # @dynamic-icon/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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`.
@@ -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,140 @@
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
+ /** 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);
7
+ function positive(name, value) {
8
+ if (!Number.isFinite(value) || value <= 0) {
9
+ throw new RangeError(`[dynamic-icon] ${name} must be a positive finite number, got ${value}`);
10
+ }
11
+ return value;
12
+ }
13
+ function toResolver(type, source) {
14
+ if (typeof source === "function")
15
+ return source;
16
+ if (source !== null && typeof source === "object") {
17
+ // Held by reference, never copied: no extra memory, and module namespaces work as-is. `hasOwn` keeps
18
+ // identifiers such as "constructor" or "toString" from reaching Object.prototype. Treat the object as
19
+ // read-only: later mutations are visible to lookups.
20
+ return (ref) => (Object.hasOwn(source, ref) ? source[ref] : undefined);
21
+ }
22
+ throw new TypeError(`[dynamic-icon] source "${type}" must be a record or a function`);
23
+ }
24
+ function build(config) {
25
+ if (config.sanitize !== undefined && typeof config.sanitize !== "function") {
26
+ throw new TypeError("[dynamic-icon] sanitize must be a function");
27
+ }
28
+ if (config.onLog !== undefined && typeof config.onLog !== "function") {
29
+ throw new TypeError("[dynamic-icon] onLog must be a function");
30
+ }
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
+ };
56
+ const resolvers = new Map();
57
+ for (const [type, source] of Object.entries(config.sources ?? {})) {
58
+ if (RESERVED.has(type)) {
59
+ throw new Error(`[dynamic-icon] "${type}" is a built-in type and cannot be used as a source name`);
60
+ }
61
+ resolvers.set(type, toResolver(type, source));
62
+ }
63
+ const fontTypes = new Set(config.fontTypes ?? DEFAULT_FONT_TYPES);
64
+ for (const type of fontTypes) {
65
+ if (RESERVED.has(type) || resolvers.has(type)) {
66
+ throw new Error(`[dynamic-icon] "${type}" cannot be both a font type and a source/built-in type`);
67
+ }
68
+ }
69
+ const { sanitize } = config;
70
+ return {
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
+ },
96
+ fontTypes,
97
+ sanitize: sanitize
98
+ ? (html) => {
99
+ try {
100
+ return sanitize(html);
101
+ }
102
+ catch (error) {
103
+ log("error", "[dynamic-icon] sanitize threw; icon rendered empty", error);
104
+ return "";
105
+ }
106
+ }
107
+ : undefined,
108
+ fallback: config.fallback,
109
+ size: positive("defaults.size", config.defaults?.size ?? 24),
110
+ strokeWidth: positive("defaults.strokeWidth", config.defaults?.strokeWidth ?? 2)
111
+ };
112
+ }
113
+ // One registry per realm. The holder is created once and captured by this module, so every read is a plain
114
+ // property access. `Symbol.for` makes duplicate copies of this package (version skew, nested installs) share
115
+ // the same holder instead of silently keeping separate registries. The `@1` is the holder's format version:
116
+ // bump it on a breaking change so incompatible copies never share state. The config is immutable and
117
+ // identical for every SSR request, so a process-wide singleton needs no per-request isolation.
118
+ const KEY = Symbol.for("@dynamic-icon/core@1");
119
+ const realm = globalThis;
120
+ const holder = (realm[KEY] ??= { config: build({}) });
121
+ /**
122
+ * Sets the icon configuration. Call once at app start (module scope of your root/hooks file, not inside a
123
+ * component), identically on server and client. Replaces any previous configuration; throws on invalid
124
+ * input and keeps the previous configuration in that case.
125
+ */
126
+ export function configureIcons(config) {
127
+ holder.config = build(config);
128
+ }
129
+ /**
130
+ * The current configuration (defaults apply until `configureIcons` is called).
131
+ * `C` is a type assertion: the registry stores whatever `configureIcons` was given, so framework wrappers
132
+ * must be the only writers for their `C`.
133
+ */
134
+ export function getIconConfig() {
135
+ return holder.config;
136
+ }
137
+ /** Resolves a component for `webType`/`webRef`, or `undefined`. Usable outside components. */
138
+ export function resolveIcon(webType, webRef) {
139
+ return holder.config.resolve(webType, webRef);
140
+ }
@@ -0,0 +1,2 @@
1
+ export { configureIcons, getIconConfig, resolveIcon } from "./config.js";
2
+ export type { IconConfig, IconLogEntry, IconSource, ResolvedIconConfig } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { configureIcons, getIconConfig, resolveIcon } from "./config.js";
@@ -0,0 +1,44 @@
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 IconLogEntry {
8
+ type: "error" | "warn";
9
+ message: string;
10
+ /** The original thrown value, when there is one. */
11
+ error?: unknown;
12
+ }
13
+ export interface IconConfig<C = unknown> {
14
+ /** Component sources by `webType`. Reserved names: `image`, `emoji`, `markup`. */
15
+ sources?: Readonly<Record<string, IconSource<C>>>;
16
+ /** `webType`s rendered as `<i class={webRef}>` (icon fonts). Default: material, font-awesome, remix. */
17
+ fontTypes?: readonly string[];
18
+ /** Required for `markup`: raw HTML is only rendered after passing through this. */
19
+ sanitize?: (html: string) => string;
20
+ /** Rendered when nothing else resolves (and no per-instance fallback is given). */
21
+ fallback?: C;
22
+ /** Defaults for `size` (px, default 24) and `strokeWidth` (default 2). */
23
+ defaults?: {
24
+ size?: number;
25
+ strokeWidth?: number;
26
+ };
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;
33
+ }
34
+ /** Validated configuration built by `configureIcons`. Record sources are referenced, not copied. */
35
+ export interface ResolvedIconConfig<C = unknown> {
36
+ /** Component for `webType`/`webRef`, or `undefined`. Never throws. */
37
+ readonly resolve: (webType: string | undefined, webRef: string | undefined) => C | undefined;
38
+ readonly fontTypes: ReadonlySet<string>;
39
+ /** Never throws: logs and returns `''` if the user's sanitizer throws. */
40
+ readonly sanitize: ((html: string) => string) | undefined;
41
+ readonly fallback: C | undefined;
42
+ readonly size: number;
43
+ readonly strokeWidth: number;
44
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/llms.txt ADDED
@@ -0,0 +1,57 @@
1
+ # @dynamic-icon/core
2
+
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.
49
+
50
+ ## Docs
51
+
52
+ - [Svelte binding and full documentation](https://unpkg.com/@dynamic-icon/svelte/llms-full.txt)
53
+ - [README](https://unpkg.com/@dynamic-icon/core/README.md)
54
+
55
+ ## Optional
56
+
57
+ - 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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.2",
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
  }