@crup/port 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CRUP 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 ADDED
@@ -0,0 +1,212 @@
1
+ # @crup/port
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40crup%2Fport?color=1f8b4c)](https://www.npmjs.com/package/@crup/port)
4
+ [![npm downloads](https://img.shields.io/npm/dm/%40crup%2Fport?color=0b7285)](https://www.npmjs.com/package/@crup/port)
5
+ [![License](https://img.shields.io/github/license/crup/port?color=495057)](https://github.com/crup/port/blob/main/LICENSE)
6
+ [![CI](https://github.com/crup/port/actions/workflows/ci.yml/badge.svg)](https://github.com/crup/port/actions/workflows/ci.yml)
7
+ [![Docs](https://github.com/crup/port/actions/workflows/docs.yml/badge.svg)](https://github.com/crup/port/actions/workflows/docs.yml)
8
+
9
+ Protocol-first iframe runtime for explicit host/child communication.
10
+
11
+ `@crup/port` exists for the part of embedded app work that usually rots first: lifecycle, handshake timing, and message discipline. It gives the host page a small runtime for mounting an iframe, opening it inline or in a modal, enforcing origin checks, and exchanging request/response messages without ad hoc `postMessage` glue.
12
+
13
+ Tags: `iframe` `postMessage` `embed` `protocol` `TypeScript` `runtime`
14
+
15
+ Package: https://www.npmjs.com/package/@crup/port
16
+
17
+ Live demo: https://crup.github.io/port/
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @crup/port
23
+ ```
24
+
25
+ ```bash
26
+ pnpm add @crup/port
27
+ ```
28
+
29
+ ```bash
30
+ yarn add @crup/port
31
+ ```
32
+
33
+ Import the host runtime from `@crup/port` and the child runtime from `@crup/port/child`.
34
+
35
+ ## Quick Links
36
+
37
+ - npm package: https://www.npmjs.com/package/@crup/port
38
+ - live demo: https://crup.github.io/port/
39
+ - source: https://github.com/crup/port
40
+ - issues: https://github.com/crup/port/issues
41
+
42
+ ## Why This Package Exists
43
+
44
+ - Raw `postMessage` is low-level and easy to drift across products.
45
+ - Iframe lifecycle bugs usually hide in timing and cleanup paths.
46
+ - Cross-window integrations need explicit origin pinning and state transitions.
47
+ - Small embed runtimes should stay tiny, predictable, and framework-agnostic.
48
+
49
+ ## Quick Start
50
+
51
+ ### Host
52
+
53
+ ```ts
54
+ import { createPort } from '@crup/port';
55
+
56
+ const port = createPort({
57
+ url: 'https://example.com/embed',
58
+ allowedOrigin: 'https://example.com',
59
+ target: '#embed-root',
60
+ mode: 'inline',
61
+ minHeight: 360,
62
+ maxHeight: 720
63
+ });
64
+
65
+ await port.mount();
66
+
67
+ port.on('widget:loaded', (payload) => {
68
+ console.log('child event', payload);
69
+ });
70
+
71
+ const result = await port.call<{ ok: boolean }>('system:ping', {
72
+ requestedAt: Date.now()
73
+ });
74
+
75
+ console.log(result.ok);
76
+ ```
77
+
78
+ ### Child
79
+
80
+ ```ts
81
+ import { createChildPort } from '@crup/port/child';
82
+
83
+ const child = createChildPort({
84
+ allowedOrigin: 'https://host.example.com'
85
+ });
86
+
87
+ child.on('request:system:ping', (message) => {
88
+ const request = message as { messageId: string; payload?: unknown };
89
+
90
+ child.respond(request.messageId, {
91
+ ok: true,
92
+ receivedAt: Date.now()
93
+ });
94
+ });
95
+
96
+ child.emit('widget:loaded', { version: '1' });
97
+ child.resize(document.body.scrollHeight);
98
+ ```
99
+
100
+ ## What You Get
101
+
102
+ - Explicit lifecycle: `idle -> mounting -> mounted -> handshaking -> ready -> open -> closed -> destroyed`
103
+ - Strict origin pinning on both host and child
104
+ - Inline and modal host modes
105
+ - Event emission plus request/response RPC
106
+ - Child-driven height updates
107
+ - Small ESM-first bundle built with `tsup`
108
+
109
+ ## API Surface
110
+
111
+ ### `createPort(config)`
112
+
113
+ Host runtime with:
114
+
115
+ - `mount()`
116
+ - `open()`
117
+ - `close()`
118
+ - `destroy()`
119
+ - `send(type, payload?)`
120
+ - `call<T>(type, payload?)`
121
+ - `on(type, handler)`
122
+ - `off(type, handler)`
123
+ - `update(partialConfig)`
124
+ - `getState()`
125
+
126
+ ### `createChildPort(config?)`
127
+
128
+ Child runtime with:
129
+
130
+ - `ready()`
131
+ - `emit(type, payload?)`
132
+ - `on(type, handler)`
133
+ - `respond(messageId, payload)`
134
+ - `resize(height)`
135
+ - `destroy()`
136
+
137
+ ### Message Shape
138
+
139
+ ```ts
140
+ type PortMessage = {
141
+ protocol: 'crup.port';
142
+ version: '1';
143
+ instanceId: string;
144
+ messageId: string;
145
+ replyTo?: string;
146
+ kind: 'event' | 'request' | 'response' | 'error' | 'system';
147
+ type: string;
148
+ payload?: unknown;
149
+ };
150
+ ```
151
+
152
+ ## Demo And Examples
153
+
154
+ - Live GitHub Pages demo: https://crup.github.io/port/
155
+ - Inline example: [`examples/host-inline.ts`](examples/host-inline.ts)
156
+ - Modal example: [`examples/host-modal.ts`](examples/host-modal.ts)
157
+ - Child example: [`examples/child-basic.ts`](examples/child-basic.ts)
158
+ - Example overview: [`examples/README.md`](examples/README.md)
159
+
160
+ ## Documentation
161
+
162
+ - Getting started: [`docs/getting-started.md`](docs/getting-started.md)
163
+ - Protocol notes: [`docs/protocol.md`](docs/protocol.md)
164
+ - Security guidance: [`docs/security.md`](docs/security.md)
165
+ - Release process: [`docs/releasing.md`](docs/releasing.md)
166
+ - Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
167
+
168
+ ## Local Development
169
+
170
+ ```bash
171
+ pnpm install
172
+ pnpm lint
173
+ pnpm typecheck
174
+ pnpm test
175
+ pnpm build
176
+ pnpm demo:dev
177
+ ```
178
+
179
+ Useful scripts:
180
+
181
+ - `pnpm docs:build` builds the GitHub Pages site into `demo-dist/`
182
+ - `pnpm size` reports raw and gzip bundle sizes for `dist/`
183
+ - `pnpm changeset` adds a release note entry when you want to track pending package notes
184
+ - `pnpm readme:check` validates the README install and package links
185
+
186
+ ## Release Model
187
+
188
+ - `ci.yml` validates lint, types, tests, package build, demo build, README checks, size output, and package packing.
189
+ - `docs.yml` deploys the Vite demo to GitHub Pages at `https://crup.github.io/port/`.
190
+ - `release.yml` is a guarded manual stable release workflow modeled on `crup/react-timer-hook`.
191
+ - `prerelease.yml` publishes a manual alpha prerelease from the `next` branch.
192
+
193
+ ## Security
194
+
195
+ This package helps with origin checks, but it cannot secure a weak embed strategy on its own. Always pin `allowedOrigin`, set restrictive iframe attributes, and validate application-level payloads. The practical guidance lives in [`docs/security.md`](docs/security.md).
196
+
197
+ ## OSS Baseline
198
+
199
+ This repo ships with:
200
+
201
+ - MIT license
202
+ - Code of conduct
203
+ - Contributing guide
204
+ - Security policy
205
+ - Issue and PR templates
206
+ - Husky hooks
207
+ - Changesets
208
+ - GitHub Actions for CI, Pages, size reporting, and releases
209
+
210
+ ## License
211
+
212
+ MIT, see [`LICENSE`](LICENSE).
@@ -0,0 +1,16 @@
1
+ type EventHandler<T = unknown> = (payload: T) => void | Promise<void>;
2
+ interface ChildPortConfig {
3
+ allowedOrigin?: string;
4
+ }
5
+
6
+ interface ChildPort {
7
+ ready(): void;
8
+ emit(type: string, payload?: unknown): void;
9
+ on(type: string, handler: EventHandler): void;
10
+ respond(messageId: string, payload: unknown): void;
11
+ resize(height: number): void;
12
+ destroy(): void;
13
+ }
14
+ declare function createChildPort(config?: ChildPortConfig): ChildPort;
15
+
16
+ export { type ChildPort, createChildPort };
package/dist/child.mjs ADDED
@@ -0,0 +1,114 @@
1
+ // src/emitter.ts
2
+ var Emitter = class {
3
+ constructor() {
4
+ this.listeners = /* @__PURE__ */ new Map();
5
+ }
6
+ on(type, handler) {
7
+ const set = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
8
+ set.add(handler);
9
+ this.listeners.set(type, set);
10
+ }
11
+ off(type, handler) {
12
+ this.listeners.get(type)?.delete(handler);
13
+ }
14
+ async emit(type, payload) {
15
+ const list = this.listeners.get(type);
16
+ if (!list) {
17
+ return;
18
+ }
19
+ await Promise.all([...list].map(async (handler) => handler(payload)));
20
+ }
21
+ };
22
+
23
+ // src/utils.ts
24
+ var PROTOCOL = "crup.port";
25
+ var VERSION = "1";
26
+ function randomId(prefix = "msg") {
27
+ return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
28
+ }
29
+ function isPortMessage(value) {
30
+ if (typeof value !== "object" || value === null) {
31
+ return false;
32
+ }
33
+ const data = value;
34
+ return data.protocol === PROTOCOL && data.version === VERSION && typeof data.instanceId === "string" && typeof data.messageId === "string" && typeof data.kind === "string" && typeof data.type === "string";
35
+ }
36
+
37
+ // src/child.ts
38
+ function createChildPort(config = {}) {
39
+ const emitter = new Emitter();
40
+ let instanceId = null;
41
+ let hostWindow = null;
42
+ let hostOrigin = config.allowedOrigin ?? null;
43
+ const listener = (event) => {
44
+ if (!isPortMessage(event.data)) {
45
+ return;
46
+ }
47
+ const message = event.data;
48
+ if (message.kind === "system" && message.type === "port:hello") {
49
+ instanceId = message.instanceId;
50
+ hostWindow = event.source;
51
+ hostOrigin = hostOrigin ?? event.origin;
52
+ if (hostOrigin !== event.origin) {
53
+ return;
54
+ }
55
+ ready();
56
+ return;
57
+ }
58
+ if (!instanceId || !hostWindow || message.instanceId !== instanceId) {
59
+ return;
60
+ }
61
+ if (event.source !== hostWindow || event.origin !== hostOrigin) {
62
+ return;
63
+ }
64
+ if (message.kind === "event") {
65
+ void emitter.emit(message.type, message.payload);
66
+ return;
67
+ }
68
+ if (message.kind === "request") {
69
+ void emitter.emit(`request:${message.type}`, message);
70
+ }
71
+ };
72
+ window.addEventListener("message", listener);
73
+ function post(message) {
74
+ if (!instanceId || !hostWindow || !hostOrigin) {
75
+ return;
76
+ }
77
+ hostWindow.postMessage(
78
+ {
79
+ protocol: PROTOCOL,
80
+ version: VERSION,
81
+ instanceId,
82
+ messageId: randomId(),
83
+ ...message
84
+ },
85
+ hostOrigin
86
+ );
87
+ }
88
+ function ready() {
89
+ post({ kind: "system", type: "port:ready" });
90
+ }
91
+ function emit(type, payload) {
92
+ post({ kind: "event", type, payload });
93
+ }
94
+ function on(type, handler) {
95
+ emitter.on(type, handler);
96
+ }
97
+ function respond(messageId, payload) {
98
+ post({ kind: "response", type: "port:response", payload, replyTo: messageId });
99
+ }
100
+ function resize(height) {
101
+ if (!Number.isFinite(height) || height < 0) {
102
+ return;
103
+ }
104
+ post({ kind: "event", type: "port:resize", payload: height });
105
+ }
106
+ function destroy() {
107
+ window.removeEventListener("message", listener);
108
+ }
109
+ return { ready, emit, on, respond, resize, destroy };
110
+ }
111
+ export {
112
+ createChildPort
113
+ };
114
+ //# sourceMappingURL=child.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/emitter.ts","../src/utils.ts","../src/child.ts"],"sourcesContent":["import type { EventHandler } from './types';\n\nexport class Emitter {\n private listeners = new Map<string, Set<EventHandler>>();\n\n on(type: string, handler: EventHandler): void {\n const set = this.listeners.get(type) ?? new Set<EventHandler>();\n set.add(handler);\n this.listeners.set(type, set);\n }\n\n off(type: string, handler: EventHandler): void {\n this.listeners.get(type)?.delete(handler);\n }\n\n async emit(type: string, payload: unknown): Promise<void> {\n const list = this.listeners.get(type);\n if (!list) {\n return;\n }\n await Promise.all([...list].map(async (handler) => handler(payload)));\n }\n}\n","import type { PortMessage } from './types';\n\nexport const PROTOCOL = 'crup.port';\nexport const VERSION = '1';\n\nexport function randomId(prefix = 'msg'): string {\n return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function isPortMessage(value: unknown): value is PortMessage {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const data = value as Partial<PortMessage>;\n return (\n data.protocol === PROTOCOL &&\n data.version === VERSION &&\n typeof data.instanceId === 'string' &&\n typeof data.messageId === 'string' &&\n typeof data.kind === 'string' &&\n typeof data.type === 'string'\n );\n}\n","import { Emitter } from './emitter';\nimport type { ChildPortConfig, EventHandler, PortMessage } from './types';\nimport { isPortMessage, PROTOCOL, randomId, VERSION } from './utils';\n\nexport interface ChildPort {\n ready(): void;\n emit(type: string, payload?: unknown): void;\n on(type: string, handler: EventHandler): void;\n respond(messageId: string, payload: unknown): void;\n resize(height: number): void;\n destroy(): void;\n}\n\nexport function createChildPort(config: ChildPortConfig = {}): ChildPort {\n const emitter = new Emitter();\n let instanceId: string | null = null;\n let hostWindow: Window | null = null;\n let hostOrigin: string | null = config.allowedOrigin ?? null;\n\n const listener = (event: MessageEvent): void => {\n if (!isPortMessage(event.data)) {\n return;\n }\n\n const message = event.data;\n\n if (message.kind === 'system' && message.type === 'port:hello') {\n instanceId = message.instanceId;\n hostWindow = event.source as Window;\n hostOrigin = hostOrigin ?? event.origin;\n\n if (hostOrigin !== event.origin) {\n return;\n }\n\n ready();\n return;\n }\n\n if (!instanceId || !hostWindow || message.instanceId !== instanceId) {\n return;\n }\n\n if (event.source !== hostWindow || event.origin !== hostOrigin) {\n return;\n }\n\n if (message.kind === 'event') {\n void emitter.emit(message.type, message.payload);\n return;\n }\n\n if (message.kind === 'request') {\n void emitter.emit(`request:${message.type}`, message);\n }\n };\n\n window.addEventListener('message', listener);\n\n function post(message: Pick<PortMessage, 'kind' | 'type' | 'payload' | 'replyTo'>): void {\n if (!instanceId || !hostWindow || !hostOrigin) {\n return;\n }\n\n hostWindow.postMessage(\n {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId: randomId(),\n ...message\n } satisfies PortMessage,\n hostOrigin\n );\n }\n\n function ready(): void {\n post({ kind: 'system', type: 'port:ready' });\n }\n\n function emit(type: string, payload?: unknown): void {\n post({ kind: 'event', type, payload });\n }\n\n function on(type: string, handler: EventHandler): void {\n emitter.on(type, handler);\n }\n\n function respond(messageId: string, payload: unknown): void {\n post({ kind: 'response', type: 'port:response', payload, replyTo: messageId });\n }\n\n function resize(height: number): void {\n if (!Number.isFinite(height) || height < 0) {\n return;\n }\n post({ kind: 'event', type: 'port:resize', payload: height });\n }\n\n function destroy(): void {\n window.removeEventListener('message', listener);\n }\n\n return { ready, emit, on, respond, resize, destroy };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,46 @@
1
+ type PortState = 'idle' | 'mounting' | 'mounted' | 'handshaking' | 'ready' | 'open' | 'closed' | 'destroyed';
2
+ type PortErrorCode = 'INVALID_CONFIG' | 'INVALID_STATE' | 'IFRAME_LOAD_TIMEOUT' | 'HANDSHAKE_TIMEOUT' | 'CALL_TIMEOUT' | 'ORIGIN_MISMATCH' | 'MESSAGE_REJECTED' | 'PORT_DESTROYED';
3
+ type MessageKind = 'event' | 'request' | 'response' | 'error' | 'system';
4
+ interface PortMessage {
5
+ protocol: 'crup.port';
6
+ version: '1';
7
+ instanceId: string;
8
+ messageId: string;
9
+ replyTo?: string;
10
+ kind: MessageKind;
11
+ type: string;
12
+ payload?: unknown;
13
+ }
14
+ interface PortConfig {
15
+ url: string;
16
+ allowedOrigin: string;
17
+ target: string | HTMLElement;
18
+ mode?: 'inline' | 'modal';
19
+ handshakeTimeoutMs?: number;
20
+ callTimeoutMs?: number;
21
+ iframeLoadTimeoutMs?: number;
22
+ minHeight?: number;
23
+ maxHeight?: number;
24
+ }
25
+ type EventHandler<T = unknown> = (payload: T) => void | Promise<void>;
26
+
27
+ interface Port {
28
+ mount(): Promise<void>;
29
+ open(): Promise<void>;
30
+ close(): Promise<void>;
31
+ destroy(): void;
32
+ send(type: string, payload?: unknown): void;
33
+ call<T = unknown>(type: string, payload?: unknown): Promise<T>;
34
+ on(type: string, handler: EventHandler): void;
35
+ off(type: string, handler: EventHandler): void;
36
+ update(config: Partial<PortConfig>): void;
37
+ getState(): PortState;
38
+ }
39
+ declare function createPort(input: PortConfig): Port;
40
+
41
+ declare class PortError extends Error {
42
+ readonly code: PortErrorCode;
43
+ constructor(code: PortErrorCode, message: string);
44
+ }
45
+
46
+ export { type Port, type PortConfig, PortError, type PortErrorCode, type PortMessage, type PortState, createPort };
@@ -0,0 +1,365 @@
1
+ "use strict";
2
+ var CrupPort = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ PortError: () => PortError,
25
+ createPort: () => createPort
26
+ });
27
+
28
+ // src/emitter.ts
29
+ var Emitter = class {
30
+ constructor() {
31
+ this.listeners = /* @__PURE__ */ new Map();
32
+ }
33
+ on(type, handler) {
34
+ const set = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
35
+ set.add(handler);
36
+ this.listeners.set(type, set);
37
+ }
38
+ off(type, handler) {
39
+ this.listeners.get(type)?.delete(handler);
40
+ }
41
+ async emit(type, payload) {
42
+ const list = this.listeners.get(type);
43
+ if (!list) {
44
+ return;
45
+ }
46
+ await Promise.all([...list].map(async (handler) => handler(payload)));
47
+ }
48
+ };
49
+
50
+ // src/errors.ts
51
+ var PortError = class extends Error {
52
+ constructor(code, message) {
53
+ super(message);
54
+ this.code = code;
55
+ this.name = "PortError";
56
+ }
57
+ };
58
+
59
+ // src/utils.ts
60
+ var PROTOCOL = "crup.port";
61
+ var VERSION = "1";
62
+ function randomId(prefix = "msg") {
63
+ return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
64
+ }
65
+ function isPortMessage(value) {
66
+ if (typeof value !== "object" || value === null) {
67
+ return false;
68
+ }
69
+ const data = value;
70
+ return data.protocol === PROTOCOL && data.version === VERSION && typeof data.instanceId === "string" && typeof data.messageId === "string" && typeof data.kind === "string" && typeof data.type === "string";
71
+ }
72
+
73
+ // src/host.ts
74
+ var DEFAULT_HANDSHAKE_TIMEOUT = 8e3;
75
+ var DEFAULT_CALL_TIMEOUT = 8e3;
76
+ var DEFAULT_IFRAME_LOAD_TIMEOUT = 8e3;
77
+ function createPort(input) {
78
+ validateConfig(input);
79
+ const config = {
80
+ ...input,
81
+ mode: input.mode ?? "inline",
82
+ handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,
83
+ callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,
84
+ iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,
85
+ minHeight: input.minHeight ?? 0,
86
+ maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER
87
+ };
88
+ const instanceId = randomId("port");
89
+ const emitter = new Emitter();
90
+ const pending = /* @__PURE__ */ new Map();
91
+ let state = "idle";
92
+ let iframe = null;
93
+ let targetNode = null;
94
+ let modalRoot = null;
95
+ const listener = (event) => {
96
+ if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {
97
+ return;
98
+ }
99
+ if (event.origin !== config.allowedOrigin) {
100
+ return;
101
+ }
102
+ if (!isPortMessage(event.data)) {
103
+ return;
104
+ }
105
+ const msg = event.data;
106
+ if (msg.instanceId !== instanceId) {
107
+ return;
108
+ }
109
+ if (msg.kind === "system" && msg.type === "port:ready") {
110
+ if (state === "handshaking") {
111
+ state = "ready";
112
+ }
113
+ return;
114
+ }
115
+ if (msg.kind === "response" && msg.replyTo) {
116
+ const call2 = pending.get(msg.replyTo);
117
+ if (!call2) {
118
+ return;
119
+ }
120
+ clearTimeout(call2.timeout);
121
+ pending.delete(msg.replyTo);
122
+ call2.resolve(msg.payload);
123
+ return;
124
+ }
125
+ if (msg.kind === "error" && msg.replyTo) {
126
+ const call2 = pending.get(msg.replyTo);
127
+ if (!call2) {
128
+ return;
129
+ }
130
+ clearTimeout(call2.timeout);
131
+ pending.delete(msg.replyTo);
132
+ const reason = typeof msg.payload === "string" ? msg.payload : "Rejected";
133
+ call2.reject(new PortError("MESSAGE_REJECTED", reason));
134
+ return;
135
+ }
136
+ if (msg.kind === "event" && msg.type === "port:resize") {
137
+ applyResize(msg.payload);
138
+ return;
139
+ }
140
+ if (msg.kind === "event") {
141
+ void emitter.emit(msg.type, msg.payload);
142
+ }
143
+ };
144
+ window.addEventListener("message", listener);
145
+ function ensureState(valid, nextAction) {
146
+ if (!valid.includes(state)) {
147
+ throw new PortError("INVALID_STATE", `Cannot ${nextAction} from state ${state}`);
148
+ }
149
+ }
150
+ function resolveTarget(target) {
151
+ if (typeof target === "string") {
152
+ const node = document.querySelector(target);
153
+ if (!node) {
154
+ throw new PortError("INVALID_CONFIG", `Target ${target} was not found`);
155
+ }
156
+ return node;
157
+ }
158
+ return target;
159
+ }
160
+ async function mount() {
161
+ ensureState(["idle"], "mount");
162
+ state = "mounting";
163
+ targetNode = resolveTarget(config.target);
164
+ iframe = document.createElement("iframe");
165
+ iframe.src = config.url;
166
+ iframe.style.width = "100%";
167
+ iframe.style.border = "0";
168
+ iframe.style.display = config.mode === "modal" ? "none" : "block";
169
+ if (config.mode === "modal") {
170
+ modalRoot = document.createElement("div");
171
+ modalRoot.style.position = "fixed";
172
+ modalRoot.style.inset = "0";
173
+ modalRoot.style.background = "rgba(0,0,0,0.5)";
174
+ modalRoot.style.display = "none";
175
+ modalRoot.style.alignItems = "center";
176
+ modalRoot.style.justifyContent = "center";
177
+ const container = document.createElement("div");
178
+ container.style.width = "min(900px, 95vw)";
179
+ container.style.height = "min(85vh, 900px)";
180
+ container.style.background = "#fff";
181
+ container.style.borderRadius = "8px";
182
+ container.style.overflow = "hidden";
183
+ iframe.style.display = "block";
184
+ iframe.style.height = "100%";
185
+ container.append(iframe);
186
+ modalRoot.append(container);
187
+ targetNode.append(modalRoot);
188
+ modalRoot.addEventListener("click", (event) => {
189
+ if (event.target === modalRoot) {
190
+ void close();
191
+ }
192
+ });
193
+ window.addEventListener("keydown", (event) => {
194
+ if (event.key === "Escape" && state === "open") {
195
+ void close();
196
+ }
197
+ });
198
+ } else {
199
+ targetNode.append(iframe);
200
+ }
201
+ await new Promise((resolve, reject) => {
202
+ const timer = setTimeout(() => {
203
+ reject(new PortError("IFRAME_LOAD_TIMEOUT", "iframe did not load in time"));
204
+ }, config.iframeLoadTimeoutMs);
205
+ iframe?.addEventListener(
206
+ "load",
207
+ () => {
208
+ clearTimeout(timer);
209
+ resolve();
210
+ },
211
+ { once: true }
212
+ );
213
+ });
214
+ state = "mounted";
215
+ await handshake();
216
+ }
217
+ async function handshake() {
218
+ ensureState(["mounted"], "handshake");
219
+ if (!iframe?.contentWindow) {
220
+ throw new PortError("INVALID_STATE", "iframe is unavailable for handshake");
221
+ }
222
+ state = "handshaking";
223
+ post({ kind: "system", type: "port:hello" });
224
+ await new Promise((resolve, reject) => {
225
+ const timer = setTimeout(() => {
226
+ clearInterval(poll);
227
+ reject(new PortError("HANDSHAKE_TIMEOUT", "handshake timed out"));
228
+ }, config.handshakeTimeoutMs);
229
+ const poll = setInterval(() => {
230
+ if (state === "ready") {
231
+ clearTimeout(timer);
232
+ clearInterval(poll);
233
+ resolve();
234
+ }
235
+ }, 10);
236
+ });
237
+ if (config.mode === "inline") {
238
+ state = "open";
239
+ }
240
+ }
241
+ function open() {
242
+ ensureState(["ready", "closed"], "open");
243
+ if (config.mode !== "modal") {
244
+ state = "open";
245
+ return Promise.resolve();
246
+ }
247
+ if (!modalRoot) {
248
+ throw new PortError("INVALID_STATE", "modal root missing");
249
+ }
250
+ modalRoot.style.display = "flex";
251
+ state = "open";
252
+ return Promise.resolve();
253
+ }
254
+ function close() {
255
+ ensureState(["open"], "close");
256
+ if (config.mode === "modal" && modalRoot) {
257
+ modalRoot.style.display = "none";
258
+ }
259
+ state = "closed";
260
+ return Promise.resolve();
261
+ }
262
+ function destroy() {
263
+ if (state === "destroyed") {
264
+ return;
265
+ }
266
+ pending.forEach((entry) => {
267
+ clearTimeout(entry.timeout);
268
+ entry.reject(new PortError("PORT_DESTROYED", "Port has been destroyed"));
269
+ });
270
+ pending.clear();
271
+ window.removeEventListener("message", listener);
272
+ iframe?.remove();
273
+ modalRoot?.remove();
274
+ iframe = null;
275
+ modalRoot = null;
276
+ targetNode = null;
277
+ state = "destroyed";
278
+ }
279
+ function post(message) {
280
+ if (!iframe?.contentWindow) {
281
+ throw new PortError("INVALID_STATE", "iframe is not available");
282
+ }
283
+ const finalMessage = {
284
+ protocol: PROTOCOL,
285
+ version: VERSION,
286
+ instanceId,
287
+ messageId: randomId(),
288
+ ...message
289
+ };
290
+ iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);
291
+ }
292
+ function send(type, payload) {
293
+ if (state === "destroyed") {
294
+ throw new PortError("PORT_DESTROYED", "Port is destroyed");
295
+ }
296
+ ensureState(["ready", "open", "closed"], "send");
297
+ post({ kind: "event", type, payload });
298
+ }
299
+ function call(type, payload) {
300
+ if (state === "destroyed") {
301
+ return Promise.reject(new PortError("PORT_DESTROYED", "Port is destroyed"));
302
+ }
303
+ ensureState(["ready", "open", "closed"], "call");
304
+ const messageId = randomId();
305
+ const message = {
306
+ protocol: PROTOCOL,
307
+ version: VERSION,
308
+ instanceId,
309
+ messageId,
310
+ kind: "request",
311
+ type,
312
+ payload
313
+ };
314
+ iframe?.contentWindow?.postMessage(message, config.allowedOrigin);
315
+ return new Promise((resolve, reject) => {
316
+ const timeout = setTimeout(() => {
317
+ pending.delete(messageId);
318
+ reject(new PortError("CALL_TIMEOUT", `${type} timed out`));
319
+ }, config.callTimeoutMs);
320
+ pending.set(messageId, { resolve, reject, timeout });
321
+ });
322
+ }
323
+ function on(type, handler) {
324
+ emitter.on(type, handler);
325
+ }
326
+ function off(type, handler) {
327
+ emitter.off(type, handler);
328
+ }
329
+ function update(next) {
330
+ Object.assign(config, next);
331
+ }
332
+ function getState() {
333
+ return state;
334
+ }
335
+ function applyResize(payload) {
336
+ if (!iframe) {
337
+ return;
338
+ }
339
+ if (typeof payload !== "number" || Number.isNaN(payload)) {
340
+ return;
341
+ }
342
+ const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));
343
+ iframe.style.height = `${bounded}px`;
344
+ }
345
+ return {
346
+ mount,
347
+ open,
348
+ close,
349
+ destroy,
350
+ send,
351
+ call,
352
+ on,
353
+ off,
354
+ update,
355
+ getState
356
+ };
357
+ }
358
+ function validateConfig(config) {
359
+ if (!config.url || !config.allowedOrigin || !config.target) {
360
+ throw new PortError("INVALID_CONFIG", "url, target, and allowedOrigin are required");
361
+ }
362
+ }
363
+ return __toCommonJS(src_exports);
364
+ })();
365
+ //# sourceMappingURL=index.global.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/emitter.ts","../src/errors.ts","../src/utils.ts","../src/host.ts"],"sourcesContent":["export { createPort } from './host';\nexport { PortError } from './errors';\nexport type { Port } from './host';\nexport type { PortConfig, PortErrorCode, PortMessage, PortState } from './types';\n","import type { EventHandler } from './types';\n\nexport class Emitter {\n private listeners = new Map<string, Set<EventHandler>>();\n\n on(type: string, handler: EventHandler): void {\n const set = this.listeners.get(type) ?? new Set<EventHandler>();\n set.add(handler);\n this.listeners.set(type, set);\n }\n\n off(type: string, handler: EventHandler): void {\n this.listeners.get(type)?.delete(handler);\n }\n\n async emit(type: string, payload: unknown): Promise<void> {\n const list = this.listeners.get(type);\n if (!list) {\n return;\n }\n await Promise.all([...list].map(async (handler) => handler(payload)));\n }\n}\n","import type { PortErrorCode } from './types';\n\nexport class PortError extends Error {\n readonly code: PortErrorCode;\n\n constructor(code: PortErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'PortError';\n }\n}\n","import type { PortMessage } from './types';\n\nexport const PROTOCOL = 'crup.port';\nexport const VERSION = '1';\n\nexport function randomId(prefix = 'msg'): string {\n return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function isPortMessage(value: unknown): value is PortMessage {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const data = value as Partial<PortMessage>;\n return (\n data.protocol === PROTOCOL &&\n data.version === VERSION &&\n typeof data.instanceId === 'string' &&\n typeof data.messageId === 'string' &&\n typeof data.kind === 'string' &&\n typeof data.type === 'string'\n );\n}\n","import { Emitter } from './emitter';\nimport { PortError } from './errors';\nimport type { EventHandler, PortConfig, PortMessage, PortState } from './types';\nimport { isPortMessage, PROTOCOL, randomId, VERSION } from './utils';\n\ninterface PendingCall {\n resolve: (value: unknown) => void;\n reject: (reason?: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n}\n\nconst DEFAULT_HANDSHAKE_TIMEOUT = 8_000;\nconst DEFAULT_CALL_TIMEOUT = 8_000;\nconst DEFAULT_IFRAME_LOAD_TIMEOUT = 8_000;\n\nexport interface Port {\n mount(): Promise<void>;\n open(): Promise<void>;\n close(): Promise<void>;\n destroy(): void;\n send(type: string, payload?: unknown): void;\n call<T = unknown>(type: string, payload?: unknown): Promise<T>;\n on(type: string, handler: EventHandler): void;\n off(type: string, handler: EventHandler): void;\n update(config: Partial<PortConfig>): void;\n getState(): PortState;\n}\n\nexport function createPort(input: PortConfig): Port {\n validateConfig(input);\n\n const config: Required<\n Pick<PortConfig, 'mode' | 'handshakeTimeoutMs' | 'callTimeoutMs' | 'iframeLoadTimeoutMs' | 'minHeight' | 'maxHeight'>\n > &\n PortConfig = {\n ...input,\n mode: input.mode ?? 'inline',\n handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,\n callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,\n iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,\n minHeight: input.minHeight ?? 0,\n maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER\n };\n\n const instanceId = randomId('port');\n const emitter = new Emitter();\n const pending = new Map<string, PendingCall>();\n let state: PortState = 'idle';\n let iframe: HTMLIFrameElement | null = null;\n let targetNode: HTMLElement | null = null;\n let modalRoot: HTMLDivElement | null = null;\n\n const listener = (event: MessageEvent): void => {\n if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {\n return;\n }\n\n if (event.origin !== config.allowedOrigin) {\n return;\n }\n\n if (!isPortMessage(event.data)) {\n return;\n }\n\n const msg = event.data;\n if (msg.instanceId !== instanceId) {\n return;\n }\n\n if (msg.kind === 'system' && msg.type === 'port:ready') {\n if (state === 'handshaking') {\n state = 'ready';\n }\n return;\n }\n\n if (msg.kind === 'response' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n call.resolve(msg.payload);\n return;\n }\n\n if (msg.kind === 'error' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n const reason = typeof msg.payload === 'string' ? msg.payload : 'Rejected';\n call.reject(new PortError('MESSAGE_REJECTED', reason));\n return;\n }\n\n if (msg.kind === 'event' && msg.type === 'port:resize') {\n applyResize(msg.payload);\n return;\n }\n\n if (msg.kind === 'event') {\n void emitter.emit(msg.type, msg.payload);\n }\n };\n\n window.addEventListener('message', listener);\n\n function ensureState(valid: PortState[], nextAction: string): void {\n if (!valid.includes(state)) {\n throw new PortError('INVALID_STATE', `Cannot ${nextAction} from state ${state}`);\n }\n }\n\n function resolveTarget(target: PortConfig['target']): HTMLElement {\n if (typeof target === 'string') {\n const node = document.querySelector<HTMLElement>(target);\n if (!node) {\n throw new PortError('INVALID_CONFIG', `Target ${target} was not found`);\n }\n return node;\n }\n return target;\n }\n\n async function mount(): Promise<void> {\n ensureState(['idle'], 'mount');\n state = 'mounting';\n\n targetNode = resolveTarget(config.target);\n iframe = document.createElement('iframe');\n iframe.src = config.url;\n iframe.style.width = '100%';\n iframe.style.border = '0';\n iframe.style.display = config.mode === 'modal' ? 'none' : 'block';\n\n if (config.mode === 'modal') {\n modalRoot = document.createElement('div');\n modalRoot.style.position = 'fixed';\n modalRoot.style.inset = '0';\n modalRoot.style.background = 'rgba(0,0,0,0.5)';\n modalRoot.style.display = 'none';\n modalRoot.style.alignItems = 'center';\n modalRoot.style.justifyContent = 'center';\n\n const container = document.createElement('div');\n container.style.width = 'min(900px, 95vw)';\n container.style.height = 'min(85vh, 900px)';\n container.style.background = '#fff';\n container.style.borderRadius = '8px';\n container.style.overflow = 'hidden';\n iframe.style.display = 'block';\n iframe.style.height = '100%';\n\n container.append(iframe);\n modalRoot.append(container);\n targetNode.append(modalRoot);\n\n modalRoot.addEventListener('click', (event) => {\n if (event.target === modalRoot) {\n void close();\n }\n });\n\n window.addEventListener('keydown', (event) => {\n if (event.key === 'Escape' && state === 'open') {\n void close();\n }\n });\n } else {\n targetNode.append(iframe);\n }\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new PortError('IFRAME_LOAD_TIMEOUT', 'iframe did not load in time'));\n }, config.iframeLoadTimeoutMs);\n\n iframe?.addEventListener(\n 'load',\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true }\n );\n });\n\n state = 'mounted';\n await handshake();\n }\n\n async function handshake(): Promise<void> {\n ensureState(['mounted'], 'handshake');\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is unavailable for handshake');\n }\n\n state = 'handshaking';\n post({ kind: 'system', type: 'port:hello' });\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n clearInterval(poll);\n reject(new PortError('HANDSHAKE_TIMEOUT', 'handshake timed out'));\n }, config.handshakeTimeoutMs);\n\n const poll = setInterval(() => {\n if (state === 'ready') {\n clearTimeout(timer);\n clearInterval(poll);\n resolve();\n }\n }, 10);\n });\n\n if (config.mode === 'inline') {\n state = 'open';\n }\n }\n\n function open(): Promise<void> {\n ensureState(['ready', 'closed'], 'open');\n if (config.mode !== 'modal') {\n state = 'open';\n return Promise.resolve();\n }\n if (!modalRoot) {\n throw new PortError('INVALID_STATE', 'modal root missing');\n }\n modalRoot.style.display = 'flex';\n state = 'open';\n return Promise.resolve();\n }\n\n function close(): Promise<void> {\n ensureState(['open'], 'close');\n if (config.mode === 'modal' && modalRoot) {\n modalRoot.style.display = 'none';\n }\n state = 'closed';\n return Promise.resolve();\n }\n\n function destroy(): void {\n if (state === 'destroyed') {\n return;\n }\n\n pending.forEach((entry) => {\n clearTimeout(entry.timeout);\n entry.reject(new PortError('PORT_DESTROYED', 'Port has been destroyed'));\n });\n pending.clear();\n\n window.removeEventListener('message', listener);\n iframe?.remove();\n modalRoot?.remove();\n iframe = null;\n modalRoot = null;\n targetNode = null;\n state = 'destroyed';\n }\n\n function post(message: Pick<PortMessage, 'kind' | 'type' | 'payload' | 'replyTo'>): void {\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is not available');\n }\n\n const finalMessage: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId: randomId(),\n ...message\n };\n\n iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);\n }\n\n function send(type: string, payload?: unknown): void {\n if (state === 'destroyed') {\n throw new PortError('PORT_DESTROYED', 'Port is destroyed');\n }\n\n ensureState(['ready', 'open', 'closed'], 'send');\n post({ kind: 'event', type, payload });\n }\n\n function call<T = unknown>(type: string, payload?: unknown): Promise<T> {\n if (state === 'destroyed') {\n return Promise.reject(new PortError('PORT_DESTROYED', 'Port is destroyed'));\n }\n\n ensureState(['ready', 'open', 'closed'], 'call');\n\n const messageId = randomId();\n const message: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId,\n kind: 'request',\n type,\n payload\n };\n\n iframe?.contentWindow?.postMessage(message, config.allowedOrigin);\n\n return new Promise<T>((resolve, reject) => {\n const timeout = setTimeout(() => {\n pending.delete(messageId);\n reject(new PortError('CALL_TIMEOUT', `${type} timed out`));\n }, config.callTimeoutMs);\n\n pending.set(messageId, { resolve: resolve as (value: unknown) => void, reject, timeout });\n });\n }\n\n function on(type: string, handler: EventHandler): void {\n emitter.on(type, handler);\n }\n\n function off(type: string, handler: EventHandler): void {\n emitter.off(type, handler);\n }\n\n function update(next: Partial<PortConfig>): void {\n Object.assign(config, next);\n }\n\n function getState(): PortState {\n return state;\n }\n\n function applyResize(payload: unknown): void {\n if (!iframe) {\n return;\n }\n if (typeof payload !== 'number' || Number.isNaN(payload)) {\n return;\n }\n\n const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));\n iframe.style.height = `${bounded}px`;\n }\n\n return {\n mount,\n open,\n close,\n destroy,\n send,\n call,\n on,\n off,\n update,\n getState\n };\n}\n\nfunction validateConfig(config: PortConfig): void {\n if (!config.url || !config.allowedOrigin || !config.target) {\n throw new PortError('INVALID_CONFIG', 'url, target, and allowedOrigin are required');\n }\n}\n"],"mappings":"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","names":["call"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,340 @@
1
+ // src/emitter.ts
2
+ var Emitter = class {
3
+ constructor() {
4
+ this.listeners = /* @__PURE__ */ new Map();
5
+ }
6
+ on(type, handler) {
7
+ const set = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
8
+ set.add(handler);
9
+ this.listeners.set(type, set);
10
+ }
11
+ off(type, handler) {
12
+ this.listeners.get(type)?.delete(handler);
13
+ }
14
+ async emit(type, payload) {
15
+ const list = this.listeners.get(type);
16
+ if (!list) {
17
+ return;
18
+ }
19
+ await Promise.all([...list].map(async (handler) => handler(payload)));
20
+ }
21
+ };
22
+
23
+ // src/errors.ts
24
+ var PortError = class extends Error {
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.code = code;
28
+ this.name = "PortError";
29
+ }
30
+ };
31
+
32
+ // src/utils.ts
33
+ var PROTOCOL = "crup.port";
34
+ var VERSION = "1";
35
+ function randomId(prefix = "msg") {
36
+ return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
37
+ }
38
+ function isPortMessage(value) {
39
+ if (typeof value !== "object" || value === null) {
40
+ return false;
41
+ }
42
+ const data = value;
43
+ return data.protocol === PROTOCOL && data.version === VERSION && typeof data.instanceId === "string" && typeof data.messageId === "string" && typeof data.kind === "string" && typeof data.type === "string";
44
+ }
45
+
46
+ // src/host.ts
47
+ var DEFAULT_HANDSHAKE_TIMEOUT = 8e3;
48
+ var DEFAULT_CALL_TIMEOUT = 8e3;
49
+ var DEFAULT_IFRAME_LOAD_TIMEOUT = 8e3;
50
+ function createPort(input) {
51
+ validateConfig(input);
52
+ const config = {
53
+ ...input,
54
+ mode: input.mode ?? "inline",
55
+ handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,
56
+ callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,
57
+ iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,
58
+ minHeight: input.minHeight ?? 0,
59
+ maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER
60
+ };
61
+ const instanceId = randomId("port");
62
+ const emitter = new Emitter();
63
+ const pending = /* @__PURE__ */ new Map();
64
+ let state = "idle";
65
+ let iframe = null;
66
+ let targetNode = null;
67
+ let modalRoot = null;
68
+ const listener = (event) => {
69
+ if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {
70
+ return;
71
+ }
72
+ if (event.origin !== config.allowedOrigin) {
73
+ return;
74
+ }
75
+ if (!isPortMessage(event.data)) {
76
+ return;
77
+ }
78
+ const msg = event.data;
79
+ if (msg.instanceId !== instanceId) {
80
+ return;
81
+ }
82
+ if (msg.kind === "system" && msg.type === "port:ready") {
83
+ if (state === "handshaking") {
84
+ state = "ready";
85
+ }
86
+ return;
87
+ }
88
+ if (msg.kind === "response" && msg.replyTo) {
89
+ const call2 = pending.get(msg.replyTo);
90
+ if (!call2) {
91
+ return;
92
+ }
93
+ clearTimeout(call2.timeout);
94
+ pending.delete(msg.replyTo);
95
+ call2.resolve(msg.payload);
96
+ return;
97
+ }
98
+ if (msg.kind === "error" && msg.replyTo) {
99
+ const call2 = pending.get(msg.replyTo);
100
+ if (!call2) {
101
+ return;
102
+ }
103
+ clearTimeout(call2.timeout);
104
+ pending.delete(msg.replyTo);
105
+ const reason = typeof msg.payload === "string" ? msg.payload : "Rejected";
106
+ call2.reject(new PortError("MESSAGE_REJECTED", reason));
107
+ return;
108
+ }
109
+ if (msg.kind === "event" && msg.type === "port:resize") {
110
+ applyResize(msg.payload);
111
+ return;
112
+ }
113
+ if (msg.kind === "event") {
114
+ void emitter.emit(msg.type, msg.payload);
115
+ }
116
+ };
117
+ window.addEventListener("message", listener);
118
+ function ensureState(valid, nextAction) {
119
+ if (!valid.includes(state)) {
120
+ throw new PortError("INVALID_STATE", `Cannot ${nextAction} from state ${state}`);
121
+ }
122
+ }
123
+ function resolveTarget(target) {
124
+ if (typeof target === "string") {
125
+ const node = document.querySelector(target);
126
+ if (!node) {
127
+ throw new PortError("INVALID_CONFIG", `Target ${target} was not found`);
128
+ }
129
+ return node;
130
+ }
131
+ return target;
132
+ }
133
+ async function mount() {
134
+ ensureState(["idle"], "mount");
135
+ state = "mounting";
136
+ targetNode = resolveTarget(config.target);
137
+ iframe = document.createElement("iframe");
138
+ iframe.src = config.url;
139
+ iframe.style.width = "100%";
140
+ iframe.style.border = "0";
141
+ iframe.style.display = config.mode === "modal" ? "none" : "block";
142
+ if (config.mode === "modal") {
143
+ modalRoot = document.createElement("div");
144
+ modalRoot.style.position = "fixed";
145
+ modalRoot.style.inset = "0";
146
+ modalRoot.style.background = "rgba(0,0,0,0.5)";
147
+ modalRoot.style.display = "none";
148
+ modalRoot.style.alignItems = "center";
149
+ modalRoot.style.justifyContent = "center";
150
+ const container = document.createElement("div");
151
+ container.style.width = "min(900px, 95vw)";
152
+ container.style.height = "min(85vh, 900px)";
153
+ container.style.background = "#fff";
154
+ container.style.borderRadius = "8px";
155
+ container.style.overflow = "hidden";
156
+ iframe.style.display = "block";
157
+ iframe.style.height = "100%";
158
+ container.append(iframe);
159
+ modalRoot.append(container);
160
+ targetNode.append(modalRoot);
161
+ modalRoot.addEventListener("click", (event) => {
162
+ if (event.target === modalRoot) {
163
+ void close();
164
+ }
165
+ });
166
+ window.addEventListener("keydown", (event) => {
167
+ if (event.key === "Escape" && state === "open") {
168
+ void close();
169
+ }
170
+ });
171
+ } else {
172
+ targetNode.append(iframe);
173
+ }
174
+ await new Promise((resolve, reject) => {
175
+ const timer = setTimeout(() => {
176
+ reject(new PortError("IFRAME_LOAD_TIMEOUT", "iframe did not load in time"));
177
+ }, config.iframeLoadTimeoutMs);
178
+ iframe?.addEventListener(
179
+ "load",
180
+ () => {
181
+ clearTimeout(timer);
182
+ resolve();
183
+ },
184
+ { once: true }
185
+ );
186
+ });
187
+ state = "mounted";
188
+ await handshake();
189
+ }
190
+ async function handshake() {
191
+ ensureState(["mounted"], "handshake");
192
+ if (!iframe?.contentWindow) {
193
+ throw new PortError("INVALID_STATE", "iframe is unavailable for handshake");
194
+ }
195
+ state = "handshaking";
196
+ post({ kind: "system", type: "port:hello" });
197
+ await new Promise((resolve, reject) => {
198
+ const timer = setTimeout(() => {
199
+ clearInterval(poll);
200
+ reject(new PortError("HANDSHAKE_TIMEOUT", "handshake timed out"));
201
+ }, config.handshakeTimeoutMs);
202
+ const poll = setInterval(() => {
203
+ if (state === "ready") {
204
+ clearTimeout(timer);
205
+ clearInterval(poll);
206
+ resolve();
207
+ }
208
+ }, 10);
209
+ });
210
+ if (config.mode === "inline") {
211
+ state = "open";
212
+ }
213
+ }
214
+ function open() {
215
+ ensureState(["ready", "closed"], "open");
216
+ if (config.mode !== "modal") {
217
+ state = "open";
218
+ return Promise.resolve();
219
+ }
220
+ if (!modalRoot) {
221
+ throw new PortError("INVALID_STATE", "modal root missing");
222
+ }
223
+ modalRoot.style.display = "flex";
224
+ state = "open";
225
+ return Promise.resolve();
226
+ }
227
+ function close() {
228
+ ensureState(["open"], "close");
229
+ if (config.mode === "modal" && modalRoot) {
230
+ modalRoot.style.display = "none";
231
+ }
232
+ state = "closed";
233
+ return Promise.resolve();
234
+ }
235
+ function destroy() {
236
+ if (state === "destroyed") {
237
+ return;
238
+ }
239
+ pending.forEach((entry) => {
240
+ clearTimeout(entry.timeout);
241
+ entry.reject(new PortError("PORT_DESTROYED", "Port has been destroyed"));
242
+ });
243
+ pending.clear();
244
+ window.removeEventListener("message", listener);
245
+ iframe?.remove();
246
+ modalRoot?.remove();
247
+ iframe = null;
248
+ modalRoot = null;
249
+ targetNode = null;
250
+ state = "destroyed";
251
+ }
252
+ function post(message) {
253
+ if (!iframe?.contentWindow) {
254
+ throw new PortError("INVALID_STATE", "iframe is not available");
255
+ }
256
+ const finalMessage = {
257
+ protocol: PROTOCOL,
258
+ version: VERSION,
259
+ instanceId,
260
+ messageId: randomId(),
261
+ ...message
262
+ };
263
+ iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);
264
+ }
265
+ function send(type, payload) {
266
+ if (state === "destroyed") {
267
+ throw new PortError("PORT_DESTROYED", "Port is destroyed");
268
+ }
269
+ ensureState(["ready", "open", "closed"], "send");
270
+ post({ kind: "event", type, payload });
271
+ }
272
+ function call(type, payload) {
273
+ if (state === "destroyed") {
274
+ return Promise.reject(new PortError("PORT_DESTROYED", "Port is destroyed"));
275
+ }
276
+ ensureState(["ready", "open", "closed"], "call");
277
+ const messageId = randomId();
278
+ const message = {
279
+ protocol: PROTOCOL,
280
+ version: VERSION,
281
+ instanceId,
282
+ messageId,
283
+ kind: "request",
284
+ type,
285
+ payload
286
+ };
287
+ iframe?.contentWindow?.postMessage(message, config.allowedOrigin);
288
+ return new Promise((resolve, reject) => {
289
+ const timeout = setTimeout(() => {
290
+ pending.delete(messageId);
291
+ reject(new PortError("CALL_TIMEOUT", `${type} timed out`));
292
+ }, config.callTimeoutMs);
293
+ pending.set(messageId, { resolve, reject, timeout });
294
+ });
295
+ }
296
+ function on(type, handler) {
297
+ emitter.on(type, handler);
298
+ }
299
+ function off(type, handler) {
300
+ emitter.off(type, handler);
301
+ }
302
+ function update(next) {
303
+ Object.assign(config, next);
304
+ }
305
+ function getState() {
306
+ return state;
307
+ }
308
+ function applyResize(payload) {
309
+ if (!iframe) {
310
+ return;
311
+ }
312
+ if (typeof payload !== "number" || Number.isNaN(payload)) {
313
+ return;
314
+ }
315
+ const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));
316
+ iframe.style.height = `${bounded}px`;
317
+ }
318
+ return {
319
+ mount,
320
+ open,
321
+ close,
322
+ destroy,
323
+ send,
324
+ call,
325
+ on,
326
+ off,
327
+ update,
328
+ getState
329
+ };
330
+ }
331
+ function validateConfig(config) {
332
+ if (!config.url || !config.allowedOrigin || !config.target) {
333
+ throw new PortError("INVALID_CONFIG", "url, target, and allowedOrigin are required");
334
+ }
335
+ }
336
+ export {
337
+ PortError,
338
+ createPort
339
+ };
340
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/emitter.ts","../src/errors.ts","../src/utils.ts","../src/host.ts"],"sourcesContent":["import type { EventHandler } from './types';\n\nexport class Emitter {\n private listeners = new Map<string, Set<EventHandler>>();\n\n on(type: string, handler: EventHandler): void {\n const set = this.listeners.get(type) ?? new Set<EventHandler>();\n set.add(handler);\n this.listeners.set(type, set);\n }\n\n off(type: string, handler: EventHandler): void {\n this.listeners.get(type)?.delete(handler);\n }\n\n async emit(type: string, payload: unknown): Promise<void> {\n const list = this.listeners.get(type);\n if (!list) {\n return;\n }\n await Promise.all([...list].map(async (handler) => handler(payload)));\n }\n}\n","import type { PortErrorCode } from './types';\n\nexport class PortError extends Error {\n readonly code: PortErrorCode;\n\n constructor(code: PortErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'PortError';\n }\n}\n","import type { PortMessage } from './types';\n\nexport const PROTOCOL = 'crup.port';\nexport const VERSION = '1';\n\nexport function randomId(prefix = 'msg'): string {\n return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function isPortMessage(value: unknown): value is PortMessage {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const data = value as Partial<PortMessage>;\n return (\n data.protocol === PROTOCOL &&\n data.version === VERSION &&\n typeof data.instanceId === 'string' &&\n typeof data.messageId === 'string' &&\n typeof data.kind === 'string' &&\n typeof data.type === 'string'\n );\n}\n","import { Emitter } from './emitter';\nimport { PortError } from './errors';\nimport type { EventHandler, PortConfig, PortMessage, PortState } from './types';\nimport { isPortMessage, PROTOCOL, randomId, VERSION } from './utils';\n\ninterface PendingCall {\n resolve: (value: unknown) => void;\n reject: (reason?: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n}\n\nconst DEFAULT_HANDSHAKE_TIMEOUT = 8_000;\nconst DEFAULT_CALL_TIMEOUT = 8_000;\nconst DEFAULT_IFRAME_LOAD_TIMEOUT = 8_000;\n\nexport interface Port {\n mount(): Promise<void>;\n open(): Promise<void>;\n close(): Promise<void>;\n destroy(): void;\n send(type: string, payload?: unknown): void;\n call<T = unknown>(type: string, payload?: unknown): Promise<T>;\n on(type: string, handler: EventHandler): void;\n off(type: string, handler: EventHandler): void;\n update(config: Partial<PortConfig>): void;\n getState(): PortState;\n}\n\nexport function createPort(input: PortConfig): Port {\n validateConfig(input);\n\n const config: Required<\n Pick<PortConfig, 'mode' | 'handshakeTimeoutMs' | 'callTimeoutMs' | 'iframeLoadTimeoutMs' | 'minHeight' | 'maxHeight'>\n > &\n PortConfig = {\n ...input,\n mode: input.mode ?? 'inline',\n handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,\n callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,\n iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,\n minHeight: input.minHeight ?? 0,\n maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER\n };\n\n const instanceId = randomId('port');\n const emitter = new Emitter();\n const pending = new Map<string, PendingCall>();\n let state: PortState = 'idle';\n let iframe: HTMLIFrameElement | null = null;\n let targetNode: HTMLElement | null = null;\n let modalRoot: HTMLDivElement | null = null;\n\n const listener = (event: MessageEvent): void => {\n if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {\n return;\n }\n\n if (event.origin !== config.allowedOrigin) {\n return;\n }\n\n if (!isPortMessage(event.data)) {\n return;\n }\n\n const msg = event.data;\n if (msg.instanceId !== instanceId) {\n return;\n }\n\n if (msg.kind === 'system' && msg.type === 'port:ready') {\n if (state === 'handshaking') {\n state = 'ready';\n }\n return;\n }\n\n if (msg.kind === 'response' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n call.resolve(msg.payload);\n return;\n }\n\n if (msg.kind === 'error' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n const reason = typeof msg.payload === 'string' ? msg.payload : 'Rejected';\n call.reject(new PortError('MESSAGE_REJECTED', reason));\n return;\n }\n\n if (msg.kind === 'event' && msg.type === 'port:resize') {\n applyResize(msg.payload);\n return;\n }\n\n if (msg.kind === 'event') {\n void emitter.emit(msg.type, msg.payload);\n }\n };\n\n window.addEventListener('message', listener);\n\n function ensureState(valid: PortState[], nextAction: string): void {\n if (!valid.includes(state)) {\n throw new PortError('INVALID_STATE', `Cannot ${nextAction} from state ${state}`);\n }\n }\n\n function resolveTarget(target: PortConfig['target']): HTMLElement {\n if (typeof target === 'string') {\n const node = document.querySelector<HTMLElement>(target);\n if (!node) {\n throw new PortError('INVALID_CONFIG', `Target ${target} was not found`);\n }\n return node;\n }\n return target;\n }\n\n async function mount(): Promise<void> {\n ensureState(['idle'], 'mount');\n state = 'mounting';\n\n targetNode = resolveTarget(config.target);\n iframe = document.createElement('iframe');\n iframe.src = config.url;\n iframe.style.width = '100%';\n iframe.style.border = '0';\n iframe.style.display = config.mode === 'modal' ? 'none' : 'block';\n\n if (config.mode === 'modal') {\n modalRoot = document.createElement('div');\n modalRoot.style.position = 'fixed';\n modalRoot.style.inset = '0';\n modalRoot.style.background = 'rgba(0,0,0,0.5)';\n modalRoot.style.display = 'none';\n modalRoot.style.alignItems = 'center';\n modalRoot.style.justifyContent = 'center';\n\n const container = document.createElement('div');\n container.style.width = 'min(900px, 95vw)';\n container.style.height = 'min(85vh, 900px)';\n container.style.background = '#fff';\n container.style.borderRadius = '8px';\n container.style.overflow = 'hidden';\n iframe.style.display = 'block';\n iframe.style.height = '100%';\n\n container.append(iframe);\n modalRoot.append(container);\n targetNode.append(modalRoot);\n\n modalRoot.addEventListener('click', (event) => {\n if (event.target === modalRoot) {\n void close();\n }\n });\n\n window.addEventListener('keydown', (event) => {\n if (event.key === 'Escape' && state === 'open') {\n void close();\n }\n });\n } else {\n targetNode.append(iframe);\n }\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new PortError('IFRAME_LOAD_TIMEOUT', 'iframe did not load in time'));\n }, config.iframeLoadTimeoutMs);\n\n iframe?.addEventListener(\n 'load',\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true }\n );\n });\n\n state = 'mounted';\n await handshake();\n }\n\n async function handshake(): Promise<void> {\n ensureState(['mounted'], 'handshake');\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is unavailable for handshake');\n }\n\n state = 'handshaking';\n post({ kind: 'system', type: 'port:hello' });\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n clearInterval(poll);\n reject(new PortError('HANDSHAKE_TIMEOUT', 'handshake timed out'));\n }, config.handshakeTimeoutMs);\n\n const poll = setInterval(() => {\n if (state === 'ready') {\n clearTimeout(timer);\n clearInterval(poll);\n resolve();\n }\n }, 10);\n });\n\n if (config.mode === 'inline') {\n state = 'open';\n }\n }\n\n function open(): Promise<void> {\n ensureState(['ready', 'closed'], 'open');\n if (config.mode !== 'modal') {\n state = 'open';\n return Promise.resolve();\n }\n if (!modalRoot) {\n throw new PortError('INVALID_STATE', 'modal root missing');\n }\n modalRoot.style.display = 'flex';\n state = 'open';\n return Promise.resolve();\n }\n\n function close(): Promise<void> {\n ensureState(['open'], 'close');\n if (config.mode === 'modal' && modalRoot) {\n modalRoot.style.display = 'none';\n }\n state = 'closed';\n return Promise.resolve();\n }\n\n function destroy(): void {\n if (state === 'destroyed') {\n return;\n }\n\n pending.forEach((entry) => {\n clearTimeout(entry.timeout);\n entry.reject(new PortError('PORT_DESTROYED', 'Port has been destroyed'));\n });\n pending.clear();\n\n window.removeEventListener('message', listener);\n iframe?.remove();\n modalRoot?.remove();\n iframe = null;\n modalRoot = null;\n targetNode = null;\n state = 'destroyed';\n }\n\n function post(message: Pick<PortMessage, 'kind' | 'type' | 'payload' | 'replyTo'>): void {\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is not available');\n }\n\n const finalMessage: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId: randomId(),\n ...message\n };\n\n iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);\n }\n\n function send(type: string, payload?: unknown): void {\n if (state === 'destroyed') {\n throw new PortError('PORT_DESTROYED', 'Port is destroyed');\n }\n\n ensureState(['ready', 'open', 'closed'], 'send');\n post({ kind: 'event', type, payload });\n }\n\n function call<T = unknown>(type: string, payload?: unknown): Promise<T> {\n if (state === 'destroyed') {\n return Promise.reject(new PortError('PORT_DESTROYED', 'Port is destroyed'));\n }\n\n ensureState(['ready', 'open', 'closed'], 'call');\n\n const messageId = randomId();\n const message: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId,\n kind: 'request',\n type,\n payload\n };\n\n iframe?.contentWindow?.postMessage(message, config.allowedOrigin);\n\n return new Promise<T>((resolve, reject) => {\n const timeout = setTimeout(() => {\n pending.delete(messageId);\n reject(new PortError('CALL_TIMEOUT', `${type} timed out`));\n }, config.callTimeoutMs);\n\n pending.set(messageId, { resolve: resolve as (value: unknown) => void, reject, timeout });\n });\n }\n\n function on(type: string, handler: EventHandler): void {\n emitter.on(type, handler);\n }\n\n function off(type: string, handler: EventHandler): void {\n emitter.off(type, handler);\n }\n\n function update(next: Partial<PortConfig>): void {\n Object.assign(config, next);\n }\n\n function getState(): PortState {\n return state;\n }\n\n function applyResize(payload: unknown): void {\n if (!iframe) {\n return;\n }\n if (typeof payload !== 'number' || Number.isNaN(payload)) {\n return;\n }\n\n const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));\n iframe.style.height = `${bounded}px`;\n }\n\n return {\n mount,\n open,\n close,\n destroy,\n send,\n call,\n on,\n off,\n update,\n getState\n };\n}\n\nfunction validateConfig(config: PortConfig): void {\n if (!config.url || !config.allowedOrigin || !config.target) {\n throw new PortError('INVALID_CONFIG', 'url, target, and allowedOrigin are required');\n }\n}\n"],"mappings":"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","names":["call"]}
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@crup/port",
3
+ "version": "0.1.1",
4
+ "description": "A lightweight protocol-first iframe runtime for building secure host/child embeds with explicit lifecycle and messaging.",
5
+ "homepage": "https://crup.github.io/port/",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/crup/port.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/crup/port/issues"
12
+ },
13
+ "author": "Rajender Joshi <connect@rajender.pro>",
14
+ "keywords": [
15
+ "iframe",
16
+ "iframe-runtime",
17
+ "postmessage",
18
+ "embed",
19
+ "protocol",
20
+ "messaging",
21
+ "rpc",
22
+ "cross-origin",
23
+ "widget",
24
+ "sdk",
25
+ "typescript",
26
+ "browser",
27
+ "runtime"
28
+ ],
29
+ "type": "module",
30
+ "license": "MIT",
31
+ "main": "./dist/index.mjs",
32
+ "module": "./dist/index.mjs",
33
+ "types": "./dist/index.d.ts",
34
+ "sideEffects": false,
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.mjs"
39
+ },
40
+ "./child": {
41
+ "types": "./dist/child.d.ts",
42
+ "import": "./dist/child.mjs"
43
+ }
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "devDependencies": {
54
+ "@changesets/cli": "^2.29.7",
55
+ "@eslint/js": "^9.25.1",
56
+ "@types/node": "^24.5.2",
57
+ "eslint": "^9.25.1",
58
+ "globals": "^16.0.0",
59
+ "husky": "^9.1.7",
60
+ "jsdom": "^25.0.1",
61
+ "tsup": "^8.3.5",
62
+ "typescript": "^5.6.3",
63
+ "typescript-eslint": "^8.31.1",
64
+ "vite": "^7.1.10",
65
+ "vitest": "^2.1.8"
66
+ },
67
+ "engines": {
68
+ "node": ">=18.0.0"
69
+ },
70
+ "scripts": {
71
+ "build": "tsup",
72
+ "dev": "tsup --watch",
73
+ "demo:dev": "vite --config vite.demo.config.ts --host",
74
+ "demo:build": "vite build --config vite.demo.config.ts",
75
+ "demo:preview": "vite preview --config vite.demo.config.ts",
76
+ "docs:dev": "pnpm demo:dev",
77
+ "docs:build": "pnpm demo:build",
78
+ "docs:preview": "pnpm demo:preview",
79
+ "lint": "eslint .",
80
+ "check": "pnpm lint && pnpm typecheck && pnpm test && pnpm build",
81
+ "test:coverage": "vitest run --coverage",
82
+ "readme:check": "node scripts/check-readme.mjs",
83
+ "size": "node scripts/size-report.mjs",
84
+ "size:json": "node scripts/size-report.mjs --json",
85
+ "changeset": "changeset",
86
+ "release": "changeset publish",
87
+ "typecheck": "tsc --noEmit",
88
+ "test": "vitest run",
89
+ "test:watch": "vitest",
90
+ "examples": "echo \"Open examples/ for host/child integration snippets\""
91
+ }
92
+ }