@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 +21 -0
- package/README.md +212 -0
- package/dist/child.d.ts +16 -0
- package/dist/child.mjs +114 -0
- package/dist/child.mjs.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.global.js +365 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.mjs +340 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +92 -0
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
|
+
[](https://www.npmjs.com/package/@crup/port)
|
|
4
|
+
[](https://www.npmjs.com/package/@crup/port)
|
|
5
|
+
[](https://github.com/crup/port/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/crup/port/actions/workflows/ci.yml)
|
|
7
|
+
[](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).
|
package/dist/child.d.ts
ADDED
|
@@ -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":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|