@microscope-js/core 0.1.0

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 microscope-js 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
13
+ all 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
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @microscope-js/core
2
+
3
+ Framework-agnostic core of microscope-js. Defines the `Renderer` interface and the registry that picks the right renderer for a source.
4
+
5
+ ```ts
6
+ import { createRegistry, mount } from '@microscope-js/core';
7
+ import { pdfRenderer } from '@microscope-js/renderer-pdf';
8
+
9
+ const registry = createRegistry([pdfRenderer]);
10
+ const handle = await mount({
11
+ source: file,
12
+ container: document.getElementById('viewer')!,
13
+ registry,
14
+ });
15
+ // later...
16
+ handle.destroy();
17
+ ```
18
+
19
+ ## Defining a renderer
20
+
21
+ ```ts
22
+ import type { Renderer } from '@microscope-js/core';
23
+
24
+ export const myRenderer: Renderer = {
25
+ id: 'myformat',
26
+ name: 'My Format',
27
+ mimes: ['application/x-myformat'],
28
+ extensions: ['myf'],
29
+ async render({ source, container, signal }) {
30
+ // ... mount into container, listen to signal for cancellation
31
+ return {
32
+ destroy() { /* clean up */ },
33
+ };
34
+ },
35
+ };
36
+ ```
37
+
38
+ Renderers are pure values — no class instantiation, no global state. The registry handles matching by MIME + extension, with custom `canRender` overrides for byte-sniffing renderers.
package/dist/index.cjs ADDED
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ var utils = require('@microscope-js/utils');
4
+
5
+ // src/registry.ts
6
+ function createRegistry(renderers) {
7
+ const entries = renderers.map(
8
+ (r, i) => "renderer" in r ? r : { renderer: r, priority: i }
9
+ );
10
+ const byId = /* @__PURE__ */ new Map();
11
+ for (const { renderer } of entries) {
12
+ byId.set(renderer.id, renderer);
13
+ }
14
+ return {
15
+ entries,
16
+ get(id) {
17
+ return byId.get(id) ?? null;
18
+ },
19
+ async match(source) {
20
+ const ext = utils.extOf(source.name);
21
+ const mime = source.mime;
22
+ const candidates = [];
23
+ for (const entry of entries) {
24
+ const r = entry.renderer;
25
+ if (r.canRender) {
26
+ const ok = await r.canRender(source);
27
+ if (ok) candidates.push(entry);
28
+ continue;
29
+ }
30
+ if (claimsByMeta(r, mime, ext)) candidates.push(entry);
31
+ }
32
+ if (candidates.length === 0) return null;
33
+ candidates.sort((a, b) => b.priority - a.priority);
34
+ return candidates[0]?.renderer ?? null;
35
+ }
36
+ };
37
+ }
38
+ function claimsByMeta(r, mime, ext) {
39
+ if (mime) {
40
+ for (const m of r.mimes) {
41
+ if (utils.mimeMatches(mime, m)) return true;
42
+ }
43
+ }
44
+ if (ext && r.extensions.includes(ext)) return true;
45
+ return false;
46
+ }
47
+ function composeRegistries(...regs) {
48
+ const merged = [];
49
+ for (const r of regs) merged.push(...r.entries);
50
+ return createRegistry(merged);
51
+ }
52
+ async function mount(opts) {
53
+ const { source, container, registry, options, signal, t, rendererId } = opts;
54
+ if (!container) {
55
+ throw new utils.MicroscopeError("mount() requires a container element", "INVALID_SOURCE");
56
+ }
57
+ if (signal?.aborted) {
58
+ throw new utils.MicroscopeError("aborted before start", "ABORTED");
59
+ }
60
+ const normalized = await utils.normalizeSource(source);
61
+ if (!normalized.mime) {
62
+ normalized.mime = await utils.sniffMime(normalized.blob);
63
+ }
64
+ const renderer = rendererId ? registry.get(rendererId) : await registry.match(normalized);
65
+ if (!renderer) {
66
+ throw new utils.MicroscopeError(
67
+ `No renderer registered for source (mime=${normalized.mime ?? "unknown"}, name=${normalized.name ?? "unknown"})`,
68
+ "UNSUPPORTED"
69
+ );
70
+ }
71
+ utils.clearContainer(container);
72
+ return renderer.render({ source: normalized, container, options, signal, t });
73
+ }
74
+
75
+ Object.defineProperty(exports, "MicroscopeError", {
76
+ enumerable: true,
77
+ get: function () { return utils.MicroscopeError; }
78
+ });
79
+ Object.defineProperty(exports, "extOf", {
80
+ enumerable: true,
81
+ get: function () { return utils.extOf; }
82
+ });
83
+ Object.defineProperty(exports, "normalizeSource", {
84
+ enumerable: true,
85
+ get: function () { return utils.normalizeSource; }
86
+ });
87
+ Object.defineProperty(exports, "sniffMime", {
88
+ enumerable: true,
89
+ get: function () { return utils.sniffMime; }
90
+ });
91
+ exports.composeRegistries = composeRegistries;
92
+ exports.createRegistry = createRegistry;
93
+ exports.mount = mount;
94
+ //# sourceMappingURL=index.cjs.map
95
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry.ts","../src/mount.ts"],"names":["extOf","mimeMatches","MicroscopeError","normalizeSource","sniffMime","clearContainer"],"mappings":"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","file":"index.cjs","sourcesContent":["import { extOf, mimeMatches } from '@microscope-js/utils';\nimport type { Registry, RegistryEntry, Renderer } from './types.js';\n\n/**\n * Build a registry from a list of renderers. Later renderers in the list win\n * unless an explicit priority is given via `{ renderer, priority }`.\n *\n * The registry is intentionally tiny — picking a renderer is just:\n * 1. Run `canRender` overrides first (custom byte-sniffing wins).\n * 2. Otherwise match by MIME, then by extension, then by sniffed MIME.\n * 3. Highest priority wins.\n */\nexport function createRegistry(renderers: ReadonlyArray<Renderer | RegistryEntry>): Registry {\n const entries: RegistryEntry[] = renderers.map((r, i) =>\n 'renderer' in r ? r : { renderer: r, priority: i },\n );\n\n const byId = new Map<string, Renderer>();\n for (const { renderer } of entries) {\n byId.set(renderer.id, renderer);\n }\n\n return {\n entries,\n get(id) {\n return byId.get(id) ?? null;\n },\n async match(source) {\n const ext = extOf(source.name);\n const mime = source.mime;\n\n // First pass — let any renderer with a custom `canRender` claim the source.\n // This is how byte-sniffing renderers (ZIP-based formats) override MIME.\n const candidates: RegistryEntry[] = [];\n for (const entry of entries) {\n const r = entry.renderer;\n if (r.canRender) {\n const ok = await r.canRender(source);\n if (ok) candidates.push(entry);\n continue;\n }\n if (claimsByMeta(r, mime, ext)) candidates.push(entry);\n }\n\n if (candidates.length === 0) return null;\n candidates.sort((a, b) => b.priority - a.priority);\n return candidates[0]?.renderer ?? null;\n },\n };\n}\n\nfunction claimsByMeta(r: Renderer, mime: string | null, ext: string | null): boolean {\n if (mime) {\n for (const m of r.mimes) {\n if (mimeMatches(mime, m)) return true;\n }\n }\n if (ext && r.extensions.includes(ext)) return true;\n return false;\n}\n\n/**\n * Compose two registries — useful when an app wants its own custom renderers\n * layered on top of the default ones without losing tree-shakability.\n */\nexport function composeRegistries(...regs: ReadonlyArray<Registry>): Registry {\n const merged: RegistryEntry[] = [];\n for (const r of regs) merged.push(...r.entries);\n return createRegistry(merged);\n}\n","import { MicroscopeError, clearContainer, normalizeSource, sniffMime } from '@microscope-js/utils';\nimport type { MountOptions, RenderHandle } from './types.js';\n\n/**\n * The single entry point most callers will use. Normalizes the source,\n * sniffs MIME if unknown, picks a renderer from the registry, and renders.\n *\n * Returns a handle whose `destroy()` MUST be called by the caller.\n */\nexport async function mount(opts: MountOptions): Promise<RenderHandle> {\n const { source, container, registry, options, signal, t, rendererId } = opts;\n\n if (!container) {\n throw new MicroscopeError('mount() requires a container element', 'INVALID_SOURCE');\n }\n if (signal?.aborted) {\n throw new MicroscopeError('aborted before start', 'ABORTED');\n }\n\n const normalized = await normalizeSource(source);\n if (!normalized.mime) {\n normalized.mime = await sniffMime(normalized.blob);\n }\n\n const renderer = rendererId ? registry.get(rendererId) : await registry.match(normalized);\n\n if (!renderer) {\n throw new MicroscopeError(\n `No renderer registered for source (mime=${normalized.mime ?? 'unknown'}, name=${normalized.name ?? 'unknown'})`,\n 'UNSUPPORTED',\n );\n }\n\n clearContainer(container);\n return renderer.render({ source: normalized, container, options, signal, t });\n}\n"]}
@@ -0,0 +1,96 @@
1
+ import { Source, NormalizedSource } from '@microscope-js/utils';
2
+ export { MicroscopeError, NormalizedSource, Source, extOf, normalizeSource, sniffMime } from '@microscope-js/utils';
3
+
4
+ /** Runtime context handed to a Renderer when it's asked to display a source. */
5
+ interface RenderContext {
6
+ /** The normalized source. Renderers receive a Blob, not the original input. */
7
+ source: NormalizedSource;
8
+ /** DOM node the renderer must mount into. It will be cleared before render. */
9
+ container: HTMLElement;
10
+ /** Per-render options forwarded to the renderer. Shape is renderer-specific. */
11
+ options?: Record<string, unknown>;
12
+ /** Cancels in-flight rendering. The renderer should clean up partial state. */
13
+ signal?: AbortSignal;
14
+ /** Translation callback for any user-facing strings (toolbar labels, errors). */
15
+ t?: (key: string, fallback: string) => string;
16
+ }
17
+ /** Handle returned to callers — destroy() is the only required capability. */
18
+ interface RenderHandle {
19
+ /** Tear down: revoke object URLs, abort workers, detach listeners. */
20
+ destroy(): void;
21
+ /** Renderer-specific capabilities (zoom, navigate, fullscreen, etc.). */
22
+ readonly capabilities?: Readonly<Record<string, unknown>>;
23
+ }
24
+ /** What a Renderer reports about itself before being asked to handle anything. */
25
+ interface RendererMeta {
26
+ /** Unique id — e.g. `pdf`, `image`, `docx`. */
27
+ id: string;
28
+ /** Human-readable name for UI. */
29
+ name: string;
30
+ /** MIME types this renderer claims. Supports `*` wildcards (e.g. `image/*`). */
31
+ mimes: ReadonlyArray<string>;
32
+ /** Lowercase file extensions this renderer claims (no leading dot). */
33
+ extensions: ReadonlyArray<string>;
34
+ }
35
+ /**
36
+ * The single interface every format implementation must satisfy.
37
+ * Stateless — instances are reusable across renders.
38
+ */
39
+ interface Renderer extends RendererMeta {
40
+ /**
41
+ * Cheap predicate run by the registry. Defaults check MIME + extension; renderers
42
+ * that need byte-sniffing override this to inspect `source.bytes()`.
43
+ */
44
+ canRender?(source: NormalizedSource): boolean | Promise<boolean>;
45
+ /** Perform the actual render. Must throw {@link MicroscopeError} on failure. */
46
+ render(ctx: RenderContext): Promise<RenderHandle>;
47
+ }
48
+ /** Entry the registry stores internally. */
49
+ interface RegistryEntry {
50
+ renderer: Renderer;
51
+ priority: number;
52
+ }
53
+ /** Input to {@link mount} — what callers actually pass. */
54
+ interface MountOptions {
55
+ source: Source;
56
+ container: HTMLElement;
57
+ registry: Registry;
58
+ options?: Record<string, unknown>;
59
+ signal?: AbortSignal;
60
+ t?: RenderContext['t'];
61
+ /** Force a specific renderer by id (skips matching). */
62
+ rendererId?: string;
63
+ }
64
+ interface Registry {
65
+ readonly entries: ReadonlyArray<RegistryEntry>;
66
+ /** Pick the highest-priority renderer that claims this source, or null. */
67
+ match(source: NormalizedSource): Promise<Renderer | null>;
68
+ /** Look up a renderer by id (used when callers force-select one). */
69
+ get(id: string): Renderer | null;
70
+ }
71
+
72
+ /**
73
+ * Build a registry from a list of renderers. Later renderers in the list win
74
+ * unless an explicit priority is given via `{ renderer, priority }`.
75
+ *
76
+ * The registry is intentionally tiny — picking a renderer is just:
77
+ * 1. Run `canRender` overrides first (custom byte-sniffing wins).
78
+ * 2. Otherwise match by MIME, then by extension, then by sniffed MIME.
79
+ * 3. Highest priority wins.
80
+ */
81
+ declare function createRegistry(renderers: ReadonlyArray<Renderer | RegistryEntry>): Registry;
82
+ /**
83
+ * Compose two registries — useful when an app wants its own custom renderers
84
+ * layered on top of the default ones without losing tree-shakability.
85
+ */
86
+ declare function composeRegistries(...regs: ReadonlyArray<Registry>): Registry;
87
+
88
+ /**
89
+ * The single entry point most callers will use. Normalizes the source,
90
+ * sniffs MIME if unknown, picks a renderer from the registry, and renders.
91
+ *
92
+ * Returns a handle whose `destroy()` MUST be called by the caller.
93
+ */
94
+ declare function mount(opts: MountOptions): Promise<RenderHandle>;
95
+
96
+ export { type MountOptions, type Registry, type RegistryEntry, type RenderContext, type RenderHandle, type Renderer, type RendererMeta, composeRegistries, createRegistry, mount };
@@ -0,0 +1,96 @@
1
+ import { Source, NormalizedSource } from '@microscope-js/utils';
2
+ export { MicroscopeError, NormalizedSource, Source, extOf, normalizeSource, sniffMime } from '@microscope-js/utils';
3
+
4
+ /** Runtime context handed to a Renderer when it's asked to display a source. */
5
+ interface RenderContext {
6
+ /** The normalized source. Renderers receive a Blob, not the original input. */
7
+ source: NormalizedSource;
8
+ /** DOM node the renderer must mount into. It will be cleared before render. */
9
+ container: HTMLElement;
10
+ /** Per-render options forwarded to the renderer. Shape is renderer-specific. */
11
+ options?: Record<string, unknown>;
12
+ /** Cancels in-flight rendering. The renderer should clean up partial state. */
13
+ signal?: AbortSignal;
14
+ /** Translation callback for any user-facing strings (toolbar labels, errors). */
15
+ t?: (key: string, fallback: string) => string;
16
+ }
17
+ /** Handle returned to callers — destroy() is the only required capability. */
18
+ interface RenderHandle {
19
+ /** Tear down: revoke object URLs, abort workers, detach listeners. */
20
+ destroy(): void;
21
+ /** Renderer-specific capabilities (zoom, navigate, fullscreen, etc.). */
22
+ readonly capabilities?: Readonly<Record<string, unknown>>;
23
+ }
24
+ /** What a Renderer reports about itself before being asked to handle anything. */
25
+ interface RendererMeta {
26
+ /** Unique id — e.g. `pdf`, `image`, `docx`. */
27
+ id: string;
28
+ /** Human-readable name for UI. */
29
+ name: string;
30
+ /** MIME types this renderer claims. Supports `*` wildcards (e.g. `image/*`). */
31
+ mimes: ReadonlyArray<string>;
32
+ /** Lowercase file extensions this renderer claims (no leading dot). */
33
+ extensions: ReadonlyArray<string>;
34
+ }
35
+ /**
36
+ * The single interface every format implementation must satisfy.
37
+ * Stateless — instances are reusable across renders.
38
+ */
39
+ interface Renderer extends RendererMeta {
40
+ /**
41
+ * Cheap predicate run by the registry. Defaults check MIME + extension; renderers
42
+ * that need byte-sniffing override this to inspect `source.bytes()`.
43
+ */
44
+ canRender?(source: NormalizedSource): boolean | Promise<boolean>;
45
+ /** Perform the actual render. Must throw {@link MicroscopeError} on failure. */
46
+ render(ctx: RenderContext): Promise<RenderHandle>;
47
+ }
48
+ /** Entry the registry stores internally. */
49
+ interface RegistryEntry {
50
+ renderer: Renderer;
51
+ priority: number;
52
+ }
53
+ /** Input to {@link mount} — what callers actually pass. */
54
+ interface MountOptions {
55
+ source: Source;
56
+ container: HTMLElement;
57
+ registry: Registry;
58
+ options?: Record<string, unknown>;
59
+ signal?: AbortSignal;
60
+ t?: RenderContext['t'];
61
+ /** Force a specific renderer by id (skips matching). */
62
+ rendererId?: string;
63
+ }
64
+ interface Registry {
65
+ readonly entries: ReadonlyArray<RegistryEntry>;
66
+ /** Pick the highest-priority renderer that claims this source, or null. */
67
+ match(source: NormalizedSource): Promise<Renderer | null>;
68
+ /** Look up a renderer by id (used when callers force-select one). */
69
+ get(id: string): Renderer | null;
70
+ }
71
+
72
+ /**
73
+ * Build a registry from a list of renderers. Later renderers in the list win
74
+ * unless an explicit priority is given via `{ renderer, priority }`.
75
+ *
76
+ * The registry is intentionally tiny — picking a renderer is just:
77
+ * 1. Run `canRender` overrides first (custom byte-sniffing wins).
78
+ * 2. Otherwise match by MIME, then by extension, then by sniffed MIME.
79
+ * 3. Highest priority wins.
80
+ */
81
+ declare function createRegistry(renderers: ReadonlyArray<Renderer | RegistryEntry>): Registry;
82
+ /**
83
+ * Compose two registries — useful when an app wants its own custom renderers
84
+ * layered on top of the default ones without losing tree-shakability.
85
+ */
86
+ declare function composeRegistries(...regs: ReadonlyArray<Registry>): Registry;
87
+
88
+ /**
89
+ * The single entry point most callers will use. Normalizes the source,
90
+ * sniffs MIME if unknown, picks a renderer from the registry, and renders.
91
+ *
92
+ * Returns a handle whose `destroy()` MUST be called by the caller.
93
+ */
94
+ declare function mount(opts: MountOptions): Promise<RenderHandle>;
95
+
96
+ export { type MountOptions, type Registry, type RegistryEntry, type RenderContext, type RenderHandle, type Renderer, type RendererMeta, composeRegistries, createRegistry, mount };
package/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ import { extOf, mimeMatches, MicroscopeError, normalizeSource, sniffMime, clearContainer } from '@microscope-js/utils';
2
+ export { MicroscopeError, extOf, normalizeSource, sniffMime } from '@microscope-js/utils';
3
+
4
+ // src/registry.ts
5
+ function createRegistry(renderers) {
6
+ const entries = renderers.map(
7
+ (r, i) => "renderer" in r ? r : { renderer: r, priority: i }
8
+ );
9
+ const byId = /* @__PURE__ */ new Map();
10
+ for (const { renderer } of entries) {
11
+ byId.set(renderer.id, renderer);
12
+ }
13
+ return {
14
+ entries,
15
+ get(id) {
16
+ return byId.get(id) ?? null;
17
+ },
18
+ async match(source) {
19
+ const ext = extOf(source.name);
20
+ const mime = source.mime;
21
+ const candidates = [];
22
+ for (const entry of entries) {
23
+ const r = entry.renderer;
24
+ if (r.canRender) {
25
+ const ok = await r.canRender(source);
26
+ if (ok) candidates.push(entry);
27
+ continue;
28
+ }
29
+ if (claimsByMeta(r, mime, ext)) candidates.push(entry);
30
+ }
31
+ if (candidates.length === 0) return null;
32
+ candidates.sort((a, b) => b.priority - a.priority);
33
+ return candidates[0]?.renderer ?? null;
34
+ }
35
+ };
36
+ }
37
+ function claimsByMeta(r, mime, ext) {
38
+ if (mime) {
39
+ for (const m of r.mimes) {
40
+ if (mimeMatches(mime, m)) return true;
41
+ }
42
+ }
43
+ if (ext && r.extensions.includes(ext)) return true;
44
+ return false;
45
+ }
46
+ function composeRegistries(...regs) {
47
+ const merged = [];
48
+ for (const r of regs) merged.push(...r.entries);
49
+ return createRegistry(merged);
50
+ }
51
+ async function mount(opts) {
52
+ const { source, container, registry, options, signal, t, rendererId } = opts;
53
+ if (!container) {
54
+ throw new MicroscopeError("mount() requires a container element", "INVALID_SOURCE");
55
+ }
56
+ if (signal?.aborted) {
57
+ throw new MicroscopeError("aborted before start", "ABORTED");
58
+ }
59
+ const normalized = await normalizeSource(source);
60
+ if (!normalized.mime) {
61
+ normalized.mime = await sniffMime(normalized.blob);
62
+ }
63
+ const renderer = rendererId ? registry.get(rendererId) : await registry.match(normalized);
64
+ if (!renderer) {
65
+ throw new MicroscopeError(
66
+ `No renderer registered for source (mime=${normalized.mime ?? "unknown"}, name=${normalized.name ?? "unknown"})`,
67
+ "UNSUPPORTED"
68
+ );
69
+ }
70
+ clearContainer(container);
71
+ return renderer.render({ source: normalized, container, options, signal, t });
72
+ }
73
+
74
+ export { composeRegistries, createRegistry, mount };
75
+ //# sourceMappingURL=index.js.map
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry.ts","../src/mount.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { extOf, mimeMatches } from '@microscope-js/utils';\nimport type { Registry, RegistryEntry, Renderer } from './types.js';\n\n/**\n * Build a registry from a list of renderers. Later renderers in the list win\n * unless an explicit priority is given via `{ renderer, priority }`.\n *\n * The registry is intentionally tiny — picking a renderer is just:\n * 1. Run `canRender` overrides first (custom byte-sniffing wins).\n * 2. Otherwise match by MIME, then by extension, then by sniffed MIME.\n * 3. Highest priority wins.\n */\nexport function createRegistry(renderers: ReadonlyArray<Renderer | RegistryEntry>): Registry {\n const entries: RegistryEntry[] = renderers.map((r, i) =>\n 'renderer' in r ? r : { renderer: r, priority: i },\n );\n\n const byId = new Map<string, Renderer>();\n for (const { renderer } of entries) {\n byId.set(renderer.id, renderer);\n }\n\n return {\n entries,\n get(id) {\n return byId.get(id) ?? null;\n },\n async match(source) {\n const ext = extOf(source.name);\n const mime = source.mime;\n\n // First pass — let any renderer with a custom `canRender` claim the source.\n // This is how byte-sniffing renderers (ZIP-based formats) override MIME.\n const candidates: RegistryEntry[] = [];\n for (const entry of entries) {\n const r = entry.renderer;\n if (r.canRender) {\n const ok = await r.canRender(source);\n if (ok) candidates.push(entry);\n continue;\n }\n if (claimsByMeta(r, mime, ext)) candidates.push(entry);\n }\n\n if (candidates.length === 0) return null;\n candidates.sort((a, b) => b.priority - a.priority);\n return candidates[0]?.renderer ?? null;\n },\n };\n}\n\nfunction claimsByMeta(r: Renderer, mime: string | null, ext: string | null): boolean {\n if (mime) {\n for (const m of r.mimes) {\n if (mimeMatches(mime, m)) return true;\n }\n }\n if (ext && r.extensions.includes(ext)) return true;\n return false;\n}\n\n/**\n * Compose two registries — useful when an app wants its own custom renderers\n * layered on top of the default ones without losing tree-shakability.\n */\nexport function composeRegistries(...regs: ReadonlyArray<Registry>): Registry {\n const merged: RegistryEntry[] = [];\n for (const r of regs) merged.push(...r.entries);\n return createRegistry(merged);\n}\n","import { MicroscopeError, clearContainer, normalizeSource, sniffMime } from '@microscope-js/utils';\nimport type { MountOptions, RenderHandle } from './types.js';\n\n/**\n * The single entry point most callers will use. Normalizes the source,\n * sniffs MIME if unknown, picks a renderer from the registry, and renders.\n *\n * Returns a handle whose `destroy()` MUST be called by the caller.\n */\nexport async function mount(opts: MountOptions): Promise<RenderHandle> {\n const { source, container, registry, options, signal, t, rendererId } = opts;\n\n if (!container) {\n throw new MicroscopeError('mount() requires a container element', 'INVALID_SOURCE');\n }\n if (signal?.aborted) {\n throw new MicroscopeError('aborted before start', 'ABORTED');\n }\n\n const normalized = await normalizeSource(source);\n if (!normalized.mime) {\n normalized.mime = await sniffMime(normalized.blob);\n }\n\n const renderer = rendererId ? registry.get(rendererId) : await registry.match(normalized);\n\n if (!renderer) {\n throw new MicroscopeError(\n `No renderer registered for source (mime=${normalized.mime ?? 'unknown'}, name=${normalized.name ?? 'unknown'})`,\n 'UNSUPPORTED',\n );\n }\n\n clearContainer(container);\n return renderer.render({ source: normalized, container, options, signal, t });\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@microscope-js/core",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic core for microscope-js: Renderer interface + registry + mount() entry point",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "dependencies": {
23
+ "@microscope-js/utils": "0.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "tsup": "^8.3.5",
27
+ "typescript": "^5.7.2",
28
+ "vitest": "^2.1.8"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "dev": "tsup --watch",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "vitest run"
38
+ }
39
+ }