@reactpy/client 0.1.0 → 0.2.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/dist/components.d.ts +10 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +172 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +5 -0
- package/dist/messages.d.ts +24 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +1 -0
- package/dist/mount.d.ts +3 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/mount.js +6 -0
- package/dist/reactpy-client.d.ts +86 -0
- package/dist/reactpy-client.d.ts.map +1 -0
- package/dist/reactpy-client.js +133 -0
- package/dist/reactpy-vdom.d.ts +54 -0
- package/dist/reactpy-vdom.d.ts.map +1 -0
- package/dist/reactpy-vdom.js +140 -0
- package/package.json +22 -20
- package/src/components.tsx +231 -0
- package/src/index.ts +5 -0
- package/src/logger.ts +5 -0
- package/src/messages.ts +32 -0
- package/src/mount.tsx +8 -0
- package/src/reactpy-client.ts +274 -0
- package/src/reactpy-vdom.tsx +261 -0
- package/tsconfig.json +14 -0
- package/src/components.js +0 -220
- package/src/contexts.js +0 -6
- package/src/element-utils.js +0 -82
- package/src/event-to-object.js +0 -240
- package/src/import-source.js +0 -134
- package/src/index.js +0 -4
- package/src/mount.js +0 -105
- package/src/server.js +0 -46
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { OutgoingMessage, IncomingMessage } from "./messages";
|
|
2
|
+
import { ReactPyModule } from "./reactpy-vdom";
|
|
3
|
+
import logger from "./logger";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A client for communicating with a ReactPy server.
|
|
7
|
+
*/
|
|
8
|
+
export interface ReactPyClient {
|
|
9
|
+
/**
|
|
10
|
+
* Connect to the server and start receiving messages.
|
|
11
|
+
*
|
|
12
|
+
* Message handlers should be registered before calling this method in order to
|
|
13
|
+
* garuntee that messages are not missed.
|
|
14
|
+
*/
|
|
15
|
+
start: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Disconnect from the server and stop receiving messages.
|
|
18
|
+
*/
|
|
19
|
+
stop: () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Register a handler for a message type.
|
|
22
|
+
*/
|
|
23
|
+
onMessage: <M extends IncomingMessage>(
|
|
24
|
+
type: M["type"],
|
|
25
|
+
handler: (message: M) => void,
|
|
26
|
+
) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Send a message to the server.
|
|
29
|
+
*/
|
|
30
|
+
sendMessage: (message: OutgoingMessage) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Dynamically load a module from the server.
|
|
33
|
+
*/
|
|
34
|
+
loadModule: (moduleName: string) => Promise<ReactPyModule>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SimpleReactPyClientProps = {
|
|
38
|
+
serverLocation?: LocationProps;
|
|
39
|
+
reconnectOptions?: ReconnectProps;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The location of the server.
|
|
44
|
+
*
|
|
45
|
+
* This is used to determine the location of the server's API endpoints. All endpoints
|
|
46
|
+
* are expected to be found at the base URL, with the following paths:
|
|
47
|
+
*
|
|
48
|
+
* - `_reactpy/stream/${route}${query}`: The websocket endpoint for the stream.
|
|
49
|
+
* - `_reactpy/modules`: The directory containing the dynamically loaded modules.
|
|
50
|
+
* - `_reactpy/assets`: The directory containing the static assets.
|
|
51
|
+
*/
|
|
52
|
+
type LocationProps = {
|
|
53
|
+
/**
|
|
54
|
+
* The base URL of the server.
|
|
55
|
+
*
|
|
56
|
+
* @default - document.location.origin
|
|
57
|
+
*/
|
|
58
|
+
url: string;
|
|
59
|
+
/**
|
|
60
|
+
* The route to the page being rendered.
|
|
61
|
+
*
|
|
62
|
+
* @default - document.location.pathname
|
|
63
|
+
*/
|
|
64
|
+
route: string;
|
|
65
|
+
/**
|
|
66
|
+
* The query string of the page being rendered.
|
|
67
|
+
*
|
|
68
|
+
* @default - document.location.search
|
|
69
|
+
*/
|
|
70
|
+
query: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type ReconnectProps = {
|
|
74
|
+
maxInterval?: number;
|
|
75
|
+
maxRetries?: number;
|
|
76
|
+
backoffRate?: number;
|
|
77
|
+
intervalJitter?: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export class SimpleReactPyClient implements ReactPyClient {
|
|
81
|
+
private resolveShouldOpen: (value: unknown) => void;
|
|
82
|
+
private resolveShouldClose: (value: unknown) => void;
|
|
83
|
+
private readonly urls: ServerUrls;
|
|
84
|
+
private readonly handlers: {
|
|
85
|
+
[key in IncomingMessage["type"]]: ((message: any) => void)[];
|
|
86
|
+
};
|
|
87
|
+
private readonly socket: { current?: WebSocket };
|
|
88
|
+
|
|
89
|
+
constructor(props: SimpleReactPyClientProps) {
|
|
90
|
+
this.handlers = {
|
|
91
|
+
"connection-open": [],
|
|
92
|
+
"connection-close": [],
|
|
93
|
+
"layout-update": [],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
this.urls = getServerUrls(
|
|
97
|
+
props.serverLocation || {
|
|
98
|
+
url: document.location.origin,
|
|
99
|
+
route: document.location.pathname,
|
|
100
|
+
query: document.location.search,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
this.resolveShouldOpen = () => {
|
|
105
|
+
throw new Error("Could not start client");
|
|
106
|
+
};
|
|
107
|
+
this.resolveShouldClose = () => {
|
|
108
|
+
throw new Error("Could not stop client");
|
|
109
|
+
};
|
|
110
|
+
const shouldOpen = new Promise((r) => (this.resolveShouldOpen = r));
|
|
111
|
+
const shouldClose = new Promise((r) => (this.resolveShouldClose = r));
|
|
112
|
+
|
|
113
|
+
this.socket = startReconnectingWebSocket({
|
|
114
|
+
shouldOpen,
|
|
115
|
+
shouldClose,
|
|
116
|
+
url: this.urls.stream,
|
|
117
|
+
onOpen: () => this.handleIncoming({ type: "connection-open" }),
|
|
118
|
+
onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)),
|
|
119
|
+
onClose: () => this.handleIncoming({ type: "connection-close" }),
|
|
120
|
+
...props.reconnectOptions,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
start(): void {
|
|
125
|
+
logger.log("starting client...");
|
|
126
|
+
this.resolveShouldOpen(undefined);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
stop(): void {
|
|
130
|
+
logger.log("stopping client...");
|
|
131
|
+
this.resolveShouldClose(undefined);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onMessage<M extends IncomingMessage>(
|
|
135
|
+
type: M["type"],
|
|
136
|
+
handler: (message: M) => void,
|
|
137
|
+
): void {
|
|
138
|
+
this.handlers[type].push(handler);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
sendMessage(message: OutgoingMessage): void {
|
|
142
|
+
this.socket.current?.send(JSON.stringify(message));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
loadModule(moduleName: string): Promise<ReactPyModule> {
|
|
146
|
+
return import(`${this.urls.modules}/${moduleName}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private handleIncoming(message: IncomingMessage): void {
|
|
150
|
+
if (!message.type) {
|
|
151
|
+
logger.warn("Received message without type", message);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const messageHandlers: ((m: any) => void)[] | undefined =
|
|
156
|
+
this.handlers[message.type];
|
|
157
|
+
if (!messageHandlers) {
|
|
158
|
+
logger.warn("Received message without handler", message);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
messageHandlers.forEach((h) => h(message));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type ServerUrls = {
|
|
167
|
+
base: URL;
|
|
168
|
+
stream: string;
|
|
169
|
+
modules: string;
|
|
170
|
+
assets: string;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function getServerUrls(props: LocationProps): ServerUrls {
|
|
174
|
+
const base = new URL(`${props.url || document.location.origin}/_reactpy`);
|
|
175
|
+
const modules = `${base}/modules`;
|
|
176
|
+
const assets = `${base}/assets`;
|
|
177
|
+
|
|
178
|
+
const streamProtocol = `ws${base.protocol === "https:" ? "s" : ""}`;
|
|
179
|
+
const streamPath = rtrim(`${base.pathname}/stream${props.route || ""}`, "/");
|
|
180
|
+
const stream = `${streamProtocol}://${base.host}${streamPath}${props.query}`;
|
|
181
|
+
|
|
182
|
+
return { base, modules, assets, stream };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function startReconnectingWebSocket(
|
|
186
|
+
props: {
|
|
187
|
+
shouldOpen: Promise<any>;
|
|
188
|
+
shouldClose: Promise<any>;
|
|
189
|
+
url: string;
|
|
190
|
+
onOpen: () => void;
|
|
191
|
+
onMessage: (message: MessageEvent<any>) => void;
|
|
192
|
+
onClose: () => void;
|
|
193
|
+
} & ReconnectProps,
|
|
194
|
+
) {
|
|
195
|
+
const {
|
|
196
|
+
maxInterval = 60000,
|
|
197
|
+
maxRetries = 50,
|
|
198
|
+
backoffRate = 1.1,
|
|
199
|
+
intervalJitter = 0.1,
|
|
200
|
+
} = props;
|
|
201
|
+
|
|
202
|
+
const startInterval = 750;
|
|
203
|
+
let retries = 0;
|
|
204
|
+
let interval = startInterval;
|
|
205
|
+
let closed = false;
|
|
206
|
+
let everConnected = false;
|
|
207
|
+
const socket: { current?: WebSocket } = {};
|
|
208
|
+
|
|
209
|
+
const connect = () => {
|
|
210
|
+
if (closed) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
socket.current = new WebSocket(props.url);
|
|
214
|
+
socket.current.onopen = () => {
|
|
215
|
+
everConnected = true;
|
|
216
|
+
logger.log("client connected");
|
|
217
|
+
interval = startInterval;
|
|
218
|
+
retries = 0;
|
|
219
|
+
props.onOpen();
|
|
220
|
+
};
|
|
221
|
+
socket.current.onmessage = props.onMessage;
|
|
222
|
+
socket.current.onclose = () => {
|
|
223
|
+
if (!everConnected) {
|
|
224
|
+
logger.log("failed to connect");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
logger.log("client disconnected");
|
|
229
|
+
props.onClose();
|
|
230
|
+
|
|
231
|
+
if (retries >= maxRetries) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const thisInterval = addJitter(interval, intervalJitter);
|
|
236
|
+
logger.log(
|
|
237
|
+
`reconnecting in ${(thisInterval / 1000).toPrecision(4)} seconds...`,
|
|
238
|
+
);
|
|
239
|
+
setTimeout(connect, thisInterval);
|
|
240
|
+
interval = nextInterval(interval, backoffRate, maxInterval);
|
|
241
|
+
retries++;
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
props.shouldOpen.then(connect);
|
|
246
|
+
props.shouldClose.then(() => {
|
|
247
|
+
closed = true;
|
|
248
|
+
socket.current?.close();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return socket;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function nextInterval(
|
|
255
|
+
currentInterval: number,
|
|
256
|
+
backoffRate: number,
|
|
257
|
+
maxInterval: number,
|
|
258
|
+
): number {
|
|
259
|
+
return Math.min(
|
|
260
|
+
currentInterval *
|
|
261
|
+
// increase interval by backoff rate
|
|
262
|
+
backoffRate,
|
|
263
|
+
// don't exceed max interval
|
|
264
|
+
maxInterval,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function addJitter(interval: number, jitter: number): number {
|
|
269
|
+
return interval + (Math.random() * jitter * interval * 2 - jitter * interval);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function rtrim(text: string, trim: string): string {
|
|
273
|
+
return text.replace(new RegExp(`${trim}+$`), "");
|
|
274
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import React, { ComponentType } from "react";
|
|
2
|
+
import { ReactPyClient } from "./reactpy-client";
|
|
3
|
+
import serializeEvent from "event-to-object";
|
|
4
|
+
|
|
5
|
+
export async function loadImportSource(
|
|
6
|
+
vdomImportSource: ReactPyVdomImportSource,
|
|
7
|
+
client: ReactPyClient,
|
|
8
|
+
): Promise<BindImportSource> {
|
|
9
|
+
let module: ReactPyModule;
|
|
10
|
+
if (vdomImportSource.sourceType === "URL") {
|
|
11
|
+
module = await import(vdomImportSource.source);
|
|
12
|
+
} else {
|
|
13
|
+
module = await client.loadModule(vdomImportSource.source);
|
|
14
|
+
}
|
|
15
|
+
if (typeof module.bind !== "function") {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`${vdomImportSource.source} did not export a function 'bind'`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (node: HTMLElement) => {
|
|
22
|
+
const binding = module.bind(node, {
|
|
23
|
+
sendMessage: client.sendMessage,
|
|
24
|
+
onMessage: client.onMessage,
|
|
25
|
+
});
|
|
26
|
+
if (
|
|
27
|
+
!(
|
|
28
|
+
typeof binding.create === "function" &&
|
|
29
|
+
typeof binding.render === "function" &&
|
|
30
|
+
typeof binding.unmount === "function"
|
|
31
|
+
)
|
|
32
|
+
) {
|
|
33
|
+
console.error(`${vdomImportSource.source} returned an impropper binding`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
render: (model) =>
|
|
39
|
+
binding.render(
|
|
40
|
+
createImportSourceElement({
|
|
41
|
+
client,
|
|
42
|
+
module,
|
|
43
|
+
binding,
|
|
44
|
+
model,
|
|
45
|
+
currentImportSource: vdomImportSource,
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
unmount: binding.unmount,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createImportSourceElement(props: {
|
|
54
|
+
client: ReactPyClient;
|
|
55
|
+
module: ReactPyModule;
|
|
56
|
+
binding: ReactPyModuleBinding;
|
|
57
|
+
model: ReactPyVdom;
|
|
58
|
+
currentImportSource: ReactPyVdomImportSource;
|
|
59
|
+
}): any {
|
|
60
|
+
let type: any;
|
|
61
|
+
if (props.model.importSource) {
|
|
62
|
+
if (
|
|
63
|
+
!isImportSourceEqual(props.currentImportSource, props.model.importSource)
|
|
64
|
+
) {
|
|
65
|
+
console.error(
|
|
66
|
+
"Parent element import source " +
|
|
67
|
+
stringifyImportSource(props.currentImportSource) +
|
|
68
|
+
" does not match child's import source " +
|
|
69
|
+
stringifyImportSource(props.model.importSource),
|
|
70
|
+
);
|
|
71
|
+
return null;
|
|
72
|
+
} else if (!props.module[props.model.tagName]) {
|
|
73
|
+
console.error(
|
|
74
|
+
"Module from source " +
|
|
75
|
+
stringifyImportSource(props.currentImportSource) +
|
|
76
|
+
` does not export ${props.model.tagName}`,
|
|
77
|
+
);
|
|
78
|
+
return null;
|
|
79
|
+
} else {
|
|
80
|
+
type = props.module[props.model.tagName];
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
type = props.model.tagName;
|
|
84
|
+
}
|
|
85
|
+
return props.binding.create(
|
|
86
|
+
type,
|
|
87
|
+
createAttributes(props.model, props.client),
|
|
88
|
+
createChildren(props.model, (child) =>
|
|
89
|
+
createImportSourceElement({
|
|
90
|
+
...props,
|
|
91
|
+
model: child,
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isImportSourceEqual(
|
|
98
|
+
source1: ReactPyVdomImportSource,
|
|
99
|
+
source2: ReactPyVdomImportSource,
|
|
100
|
+
) {
|
|
101
|
+
return (
|
|
102
|
+
source1.source === source2.source &&
|
|
103
|
+
source1.sourceType === source2.sourceType
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stringifyImportSource(importSource: ReactPyVdomImportSource) {
|
|
108
|
+
return JSON.stringify({
|
|
109
|
+
source: importSource.source,
|
|
110
|
+
sourceType: importSource.sourceType,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createChildren<Child>(
|
|
115
|
+
model: ReactPyVdom,
|
|
116
|
+
createChild: (child: ReactPyVdom) => Child,
|
|
117
|
+
): (Child | string)[] {
|
|
118
|
+
if (!model.children) {
|
|
119
|
+
return [];
|
|
120
|
+
} else {
|
|
121
|
+
return model.children.map((child) => {
|
|
122
|
+
switch (typeof child) {
|
|
123
|
+
case "object":
|
|
124
|
+
return createChild(child);
|
|
125
|
+
case "string":
|
|
126
|
+
return child;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createAttributes(
|
|
133
|
+
model: ReactPyVdom,
|
|
134
|
+
client: ReactPyClient,
|
|
135
|
+
): { [key: string]: any } {
|
|
136
|
+
return Object.fromEntries(
|
|
137
|
+
Object.entries({
|
|
138
|
+
// Normal HTML attributes
|
|
139
|
+
...model.attributes,
|
|
140
|
+
// Construct event handlers
|
|
141
|
+
...Object.fromEntries(
|
|
142
|
+
Object.entries(model.eventHandlers || {}).map(([name, handler]) =>
|
|
143
|
+
createEventHandler(client, name, handler),
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
// Convert snake_case to camelCase names
|
|
147
|
+
}).map(normalizeAttribute),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createEventHandler(
|
|
152
|
+
client: ReactPyClient,
|
|
153
|
+
name: string,
|
|
154
|
+
{ target, preventDefault, stopPropagation }: ReactPyVdomEventHandler,
|
|
155
|
+
): [string, () => void] {
|
|
156
|
+
return [
|
|
157
|
+
name,
|
|
158
|
+
function () {
|
|
159
|
+
const data = Array.from(arguments).map((value) => {
|
|
160
|
+
if (!(typeof value === "object" && value.nativeEvent)) {
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
const event = value as React.SyntheticEvent<any>;
|
|
164
|
+
if (preventDefault) {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
}
|
|
167
|
+
if (stopPropagation) {
|
|
168
|
+
event.stopPropagation();
|
|
169
|
+
}
|
|
170
|
+
return serializeEvent(event.nativeEvent);
|
|
171
|
+
});
|
|
172
|
+
client.sendMessage({ type: "layout-event", data, target });
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function normalizeAttribute([key, value]: [string, any]): [string, any] {
|
|
178
|
+
let normKey = key;
|
|
179
|
+
let normValue = value;
|
|
180
|
+
|
|
181
|
+
if (key === "style" && typeof value === "object") {
|
|
182
|
+
normValue = Object.fromEntries(
|
|
183
|
+
Object.entries(value).map(([k, v]) => [snakeToCamel(k), v]),
|
|
184
|
+
);
|
|
185
|
+
} else if (
|
|
186
|
+
key.startsWith("data_") ||
|
|
187
|
+
key.startsWith("aria_") ||
|
|
188
|
+
DASHED_HTML_ATTRS.includes(key)
|
|
189
|
+
) {
|
|
190
|
+
normKey = key.split("_").join("-");
|
|
191
|
+
} else {
|
|
192
|
+
normKey = snakeToCamel(key);
|
|
193
|
+
}
|
|
194
|
+
return [normKey, normValue];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function snakeToCamel(str: string): string {
|
|
198
|
+
return str.replace(/([_][a-z])/g, (group) =>
|
|
199
|
+
group.toUpperCase().replace("_", ""),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// see list of HTML attributes with dashes in them:
|
|
204
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#attribute_list
|
|
205
|
+
const DASHED_HTML_ATTRS = ["accept_charset", "http_equiv"];
|
|
206
|
+
|
|
207
|
+
export type ReactPyComponent = ComponentType<{ model: ReactPyVdom }>;
|
|
208
|
+
|
|
209
|
+
export type ReactPyVdom = {
|
|
210
|
+
tagName: string;
|
|
211
|
+
key?: string;
|
|
212
|
+
attributes?: { [key: string]: string };
|
|
213
|
+
children?: (ReactPyVdom | string)[];
|
|
214
|
+
error?: string;
|
|
215
|
+
eventHandlers?: { [key: string]: ReactPyVdomEventHandler };
|
|
216
|
+
importSource?: ReactPyVdomImportSource;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export type ReactPyVdomEventHandler = {
|
|
220
|
+
target: string;
|
|
221
|
+
preventDefault?: boolean;
|
|
222
|
+
stopPropagation?: boolean;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export type ReactPyVdomImportSource = {
|
|
226
|
+
source: string;
|
|
227
|
+
sourceType?: "URL" | "NAME";
|
|
228
|
+
fallback?: string | ReactPyVdom;
|
|
229
|
+
unmountBeforeUpdate?: boolean;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export type ReactPyModule = {
|
|
233
|
+
bind: (
|
|
234
|
+
node: HTMLElement,
|
|
235
|
+
context: ReactPyModuleBindingContext,
|
|
236
|
+
) => ReactPyModuleBinding;
|
|
237
|
+
} & { [key: string]: any };
|
|
238
|
+
|
|
239
|
+
export type ReactPyModuleBindingContext = {
|
|
240
|
+
sendMessage: ReactPyClient["sendMessage"];
|
|
241
|
+
onMessage: ReactPyClient["onMessage"];
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export type ReactPyModuleBinding = {
|
|
245
|
+
create: (
|
|
246
|
+
type: any,
|
|
247
|
+
props?: any,
|
|
248
|
+
children?: (any | string | ReactPyVdom)[],
|
|
249
|
+
) => any;
|
|
250
|
+
render: (element: any) => void;
|
|
251
|
+
unmount: () => void;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export type BindImportSource = (
|
|
255
|
+
node: HTMLElement,
|
|
256
|
+
) => ImportSourceBinding | null;
|
|
257
|
+
|
|
258
|
+
export type ImportSourceBinding = {
|
|
259
|
+
render: (model: ReactPyVdom) => void;
|
|
260
|
+
unmount: () => void;
|
|
261
|
+
};
|
package/tsconfig.json
ADDED