@druid-ui/host 1.0.0-next.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/transpile.worker.bc75f4c9.js +69 -0
- package/dist/transpile.worker.bc75f4c9.js.map +1 -0
- package/dist/types/index.d.ts +87 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/ui.js +571 -0
- package/dist/ui.js.map +1 -0
- package/package.json +33 -0
- package/src/file-loader.ts +113 -0
- package/src/host-functions.ts +106 -0
- package/src/index.ts +10 -0
- package/src/routing-strategy.ts +36 -0
- package/src/setup-snabbdom.ts +15 -0
- package/src/transpile.ts +143 -0
- package/src/transpile.worker.ts +33 -0
- package/src/types.ts +18 -0
- package/src/ui.ts +290 -0
- package/src/utils.ts +53 -0
- package/src/vite-env.d.ts +1 -0
- package/src/window.ts +16 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
interface AuthOptions {
|
|
2
|
+
type: "bearer" | "basic" | "api-key";
|
|
3
|
+
token?: string;
|
|
4
|
+
username?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
apiKeyHeader?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FileLoaderOptions {
|
|
11
|
+
auth?: AuthOptions;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
cache?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface HttpResponse {
|
|
17
|
+
buffer: ArrayBuffer;
|
|
18
|
+
headers: Record<string, string>;
|
|
19
|
+
contentType: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FileLoader {
|
|
23
|
+
load(path: string, options?: FileLoaderOptions): Promise<HttpResponse>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class HttpFileLoader {
|
|
27
|
+
private authOptions?: AuthOptions | undefined;
|
|
28
|
+
private defaultHeaders?: Record<string, string> | undefined;
|
|
29
|
+
|
|
30
|
+
private baseUrl?: string | undefined;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
baseUrl?: string,
|
|
34
|
+
authOptions?: AuthOptions,
|
|
35
|
+
defaultHeaders?: Record<string, string>
|
|
36
|
+
) {
|
|
37
|
+
this.baseUrl = baseUrl;
|
|
38
|
+
this.authOptions = authOptions;
|
|
39
|
+
this.defaultHeaders = defaultHeaders;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected async loadHttp(
|
|
43
|
+
path: string,
|
|
44
|
+
options?: FileLoaderOptions
|
|
45
|
+
): Promise<HttpResponse> {
|
|
46
|
+
const headers: Record<string, string> = {
|
|
47
|
+
...this.defaultHeaders,
|
|
48
|
+
...options?.headers,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Use instance auth options if no options provided, or merge with provided options
|
|
52
|
+
const authToUse = options?.auth || this.authOptions;
|
|
53
|
+
|
|
54
|
+
// Add authentication headers
|
|
55
|
+
if (authToUse) {
|
|
56
|
+
switch (authToUse.type) {
|
|
57
|
+
case "bearer":
|
|
58
|
+
if (authToUse.token) {
|
|
59
|
+
headers["Authorization"] = `Bearer ${authToUse.token}`;
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case "basic":
|
|
63
|
+
if (authToUse.username && authToUse.password) {
|
|
64
|
+
const credentials = btoa(
|
|
65
|
+
`${authToUse.username}:${authToUse.password}`
|
|
66
|
+
);
|
|
67
|
+
headers["Authorization"] = `Basic ${credentials}`;
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case "api-key":
|
|
71
|
+
if (authToUse.apiKey) {
|
|
72
|
+
const headerName = authToUse.apiKeyHeader || "X-API-Key";
|
|
73
|
+
headers[headerName] = authToUse.apiKey;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const res = await fetch(path, {
|
|
80
|
+
headers,
|
|
81
|
+
cache: options?.cache === false ? "no-store" : "default",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
throw new Error(`Failed to load file: ${path}, status: ${res.status}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extract all fetch-specific data
|
|
89
|
+
const text = await res.arrayBuffer();
|
|
90
|
+
|
|
91
|
+
const responseHeaders: Record<string, string> = {};
|
|
92
|
+
// Handle both real Headers object and mocked headers
|
|
93
|
+
if (res.headers && typeof res.headers.forEach === "function") {
|
|
94
|
+
res.headers.forEach((value, key) => {
|
|
95
|
+
responseHeaders[key] = value;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const contentType = res.headers.get("Content-Type");
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
buffer: text,
|
|
103
|
+
headers: responseHeaders,
|
|
104
|
+
contentType,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async load(path: string, options?: FileLoaderOptions) {
|
|
109
|
+
const filePath = this.baseUrl ? `${this.baseUrl}/${path}` : path;
|
|
110
|
+
const response = await this.loadHttp(filePath, options);
|
|
111
|
+
return response;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import hyperid from "hyperid";
|
|
2
|
+
import type { Props } from "druid:ui/ui";
|
|
3
|
+
import { h, type VNode, type VNodeChildren, type VNodeData } from "snabbdom";
|
|
4
|
+
import { Event } from "./types";
|
|
5
|
+
|
|
6
|
+
const nodes = new Map<
|
|
7
|
+
string,
|
|
8
|
+
{
|
|
9
|
+
element: string;
|
|
10
|
+
props?: Props;
|
|
11
|
+
children?: Array<string>;
|
|
12
|
+
hooks?: string[];
|
|
13
|
+
}
|
|
14
|
+
>();
|
|
15
|
+
|
|
16
|
+
export function setHook(id: string, callback: string) {
|
|
17
|
+
console.debug(`Setting hook for id ${id} with callback ${callback}`);
|
|
18
|
+
const node = nodes.get(id);
|
|
19
|
+
if (node) {
|
|
20
|
+
node.hooks = node.hooks || [];
|
|
21
|
+
node.hooks.push(callback);
|
|
22
|
+
} else {
|
|
23
|
+
console.warn(`setHook: No node found for id ${id}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function dfunc(element: string, props: Props, children: string[]) {
|
|
28
|
+
console.debug("Creating DOM node:", element, props, children);
|
|
29
|
+
const id = hyperid();
|
|
30
|
+
|
|
31
|
+
nodes.set(id.uuid, { element, props, children });
|
|
32
|
+
return id.uuid;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function logfunc(msg: string) {
|
|
36
|
+
console.debug("UI LOG:", msg);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createDomFromIdRec(
|
|
40
|
+
id: string,
|
|
41
|
+
rerender: () => void,
|
|
42
|
+
emitEvent: (id: string, eventType: string, event: Event) => void,
|
|
43
|
+
navigate?: (href: string) => void
|
|
44
|
+
): VNode | String {
|
|
45
|
+
const node = nodes.get(id);
|
|
46
|
+
//it is a bit strange to do it like that, in theory we want to better distinguish between text nodes and element nodes
|
|
47
|
+
if (!node) {
|
|
48
|
+
console.debug("Creating text node for id:", id);
|
|
49
|
+
return id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data: VNodeData = {};
|
|
53
|
+
|
|
54
|
+
// Set properties
|
|
55
|
+
if (node.props) {
|
|
56
|
+
data.props = {};
|
|
57
|
+
for (const prop of node.props.prop) {
|
|
58
|
+
data.props[prop.key] = prop.value;
|
|
59
|
+
}
|
|
60
|
+
data.on = {};
|
|
61
|
+
for (const eventType of node.props.on) {
|
|
62
|
+
data.on[eventType] = (e) => {
|
|
63
|
+
console.debug("Emitting event:", id, eventType, e);
|
|
64
|
+
emitEvent(
|
|
65
|
+
id,
|
|
66
|
+
eventType,
|
|
67
|
+
new Event(e?.currentTarget?.value, e?.currentTarget?.checked)
|
|
68
|
+
);
|
|
69
|
+
rerender();
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const href = data.props["href"];
|
|
73
|
+
if (href && !data.on["click"]) {
|
|
74
|
+
if (navigate) {
|
|
75
|
+
data.on.click = (e) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
navigate(href);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (node.hooks) {
|
|
83
|
+
data.hook = {};
|
|
84
|
+
for (const hookName of node.hooks) {
|
|
85
|
+
data.hook[hookName as keyof typeof data.hook] = () => {
|
|
86
|
+
emitEvent(id, hookName, new Event());
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const ch: VNodeChildren = [];
|
|
93
|
+
if (node.children) {
|
|
94
|
+
for (const childId of node.children) {
|
|
95
|
+
const childEl = createDomFromIdRec(
|
|
96
|
+
childId,
|
|
97
|
+
rerender,
|
|
98
|
+
emitEvent,
|
|
99
|
+
navigate
|
|
100
|
+
);
|
|
101
|
+
ch.push(childEl);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return h(node.element, data, ch);
|
|
106
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Re-export everything for easy access
|
|
2
|
+
export * from "./ui";
|
|
3
|
+
export * from "./types";
|
|
4
|
+
export * from "./file-loader";
|
|
5
|
+
export * from "./routing-strategy";
|
|
6
|
+
export * from "./transpile";
|
|
7
|
+
export * from "./utils";
|
|
8
|
+
|
|
9
|
+
// Global Window augmentation (side-effect import for declaration output)
|
|
10
|
+
import "./window";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface RoutingStrategy {
|
|
2
|
+
getCurrentPath(): string;
|
|
3
|
+
navigateTo(path: string): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class HistoryRoutingStrategy implements RoutingStrategy {
|
|
7
|
+
getCurrentPath(): string {
|
|
8
|
+
return window.location.pathname;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
navigateTo(path: string): void {
|
|
12
|
+
window.history.pushState({}, "", path);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class CustomRoutingStrategy implements RoutingStrategy {
|
|
17
|
+
private currentPath: string = "/";
|
|
18
|
+
|
|
19
|
+
getCurrentPath(): string {
|
|
20
|
+
return this.currentPath;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
navigateTo(path: string): void {
|
|
24
|
+
this.currentPath = path;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const createRoutingStrategy = (
|
|
29
|
+
mode: "history" | "custom"
|
|
30
|
+
): RoutingStrategy => {
|
|
31
|
+
if (mode === "custom") {
|
|
32
|
+
return new CustomRoutingStrategy();
|
|
33
|
+
} else {
|
|
34
|
+
return new HistoryRoutingStrategy();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
init,
|
|
3
|
+
classModule,
|
|
4
|
+
propsModule,
|
|
5
|
+
styleModule,
|
|
6
|
+
eventListenersModule,
|
|
7
|
+
} from "snabbdom";
|
|
8
|
+
|
|
9
|
+
export const patch = init([
|
|
10
|
+
// Init patch function with chosen modules
|
|
11
|
+
classModule, // makes it easy to toggle classes
|
|
12
|
+
propsModule, // for setting properties on DOM elements
|
|
13
|
+
styleModule, // handles styling on elements with support for animations
|
|
14
|
+
eventListenersModule, // attaches event listeners
|
|
15
|
+
]);
|
package/src/transpile.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { FileLoader } from "./file-loader";
|
|
2
|
+
|
|
3
|
+
interface TranspileResult {
|
|
4
|
+
files: Array<[string, Uint8Array]>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface CacheEntry {
|
|
8
|
+
jsUrl: string;
|
|
9
|
+
fileUrls: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CACHE_KEY_PREFIX = "transpile_cache_";
|
|
13
|
+
|
|
14
|
+
// Helper functions for localStorage caching
|
|
15
|
+
const getCachedEntry = (file: string): CacheEntry | null => {
|
|
16
|
+
try {
|
|
17
|
+
const cached = localStorage.getItem(CACHE_KEY_PREFIX + file);
|
|
18
|
+
if (cached) {
|
|
19
|
+
return JSON.parse(cached);
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.warn("Failed to read from cache:", e);
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const setCachedEntry = (file: string, entry: CacheEntry): void => {
|
|
28
|
+
try {
|
|
29
|
+
localStorage.setItem(CACHE_KEY_PREFIX + file, JSON.stringify(entry));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.warn("Failed to write to cache:", e);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const transpileInWorker = async (
|
|
36
|
+
buffer: ArrayBuffer,
|
|
37
|
+
name: string,
|
|
38
|
+
): Promise<TranspileResult> => {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const worker = new Worker(
|
|
41
|
+
new URL("./transpile.worker.ts", import.meta.url),
|
|
42
|
+
{ type: "module" },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
worker.onmessage = (event: MessageEvent) => {
|
|
46
|
+
worker.terminate();
|
|
47
|
+
if (event.data.success) {
|
|
48
|
+
resolve(event.data.data);
|
|
49
|
+
} else {
|
|
50
|
+
reject(new Error(event.data.error));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
worker.onerror = (error) => {
|
|
55
|
+
worker.terminate();
|
|
56
|
+
reject(error);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Transfer the buffer ownership to the worker (zero-copy)
|
|
60
|
+
worker.postMessage({ buffer, name }, [buffer]);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const loadTranspile = async (
|
|
65
|
+
file: string,
|
|
66
|
+
fileLoader: FileLoader,
|
|
67
|
+
): Promise<[string, (filename: string) => Promise<WebAssembly.Module>]> => {
|
|
68
|
+
// Check cache first
|
|
69
|
+
const cached = getCachedEntry(file);
|
|
70
|
+
if (cached) {
|
|
71
|
+
// Verify URLs are still valid
|
|
72
|
+
try {
|
|
73
|
+
await fetch(cached.jsUrl, { method: "HEAD" });
|
|
74
|
+
return [
|
|
75
|
+
cached.jsUrl,
|
|
76
|
+
async (filename: string) => {
|
|
77
|
+
const url = cached.fileUrls[filename];
|
|
78
|
+
if (!url) {
|
|
79
|
+
throw new Error(`File ${filename} not found in transpiled output.`);
|
|
80
|
+
}
|
|
81
|
+
const wasmResponse = await fetch(url);
|
|
82
|
+
const wasmBuffer = await wasmResponse.arrayBuffer();
|
|
83
|
+
return await WebAssembly.compile(wasmBuffer);
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// Cache is stale, proceed with transpilation
|
|
88
|
+
console.warn("Cached URLs are stale, re-transpiling");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const response = await fileLoader.load(file);
|
|
93
|
+
if (!response) {
|
|
94
|
+
throw new Error(`Failed to load file: ${file}`);
|
|
95
|
+
}
|
|
96
|
+
const files: Record<string, string> = {};
|
|
97
|
+
const t = await transpileInWorker(response.buffer, "test");
|
|
98
|
+
|
|
99
|
+
for (const file of t.files) {
|
|
100
|
+
const [f, content] = file as [string, Uint8Array];
|
|
101
|
+
|
|
102
|
+
let blob: Blob | null = null;
|
|
103
|
+
if (f.endsWith(".js")) {
|
|
104
|
+
blob = new Blob([new Uint8Array(content)], {
|
|
105
|
+
type: "application/javascript",
|
|
106
|
+
});
|
|
107
|
+
} else if (f.endsWith(".wasm")) {
|
|
108
|
+
blob = new Blob([new Uint8Array(content)], {
|
|
109
|
+
type: "application/wasm",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (blob) {
|
|
113
|
+
const url = URL.createObjectURL(blob);
|
|
114
|
+
files[f] = url;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const jsFileEntry = Object.entries(files).find(([filename]) =>
|
|
118
|
+
filename.endsWith(".js"),
|
|
119
|
+
);
|
|
120
|
+
if (!jsFileEntry) {
|
|
121
|
+
throw new Error("No JavaScript file found in transpiled output.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cache the result
|
|
125
|
+
const cacheEntry: CacheEntry = {
|
|
126
|
+
jsUrl: jsFileEntry[1],
|
|
127
|
+
fileUrls: files,
|
|
128
|
+
};
|
|
129
|
+
setCachedEntry(file, cacheEntry);
|
|
130
|
+
|
|
131
|
+
return [
|
|
132
|
+
jsFileEntry[1],
|
|
133
|
+
async (filename: string) => {
|
|
134
|
+
const url = files[filename];
|
|
135
|
+
if (!url) {
|
|
136
|
+
throw new Error(`File ${filename} not found in transpiled output.`);
|
|
137
|
+
}
|
|
138
|
+
const wasmResponse = await fetch(url);
|
|
139
|
+
const wasmBuffer = await wasmResponse.arrayBuffer();
|
|
140
|
+
return await WebAssembly.compile(wasmBuffer);
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { transpile } from "@bytecodealliance/jco";
|
|
2
|
+
|
|
3
|
+
interface TranspileRequest {
|
|
4
|
+
buffer: ArrayBuffer;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface TranspileResponse {
|
|
9
|
+
files: Array<[string, Uint8Array]>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
self.onmessage = async (event: MessageEvent<TranspileRequest>) => {
|
|
13
|
+
const { buffer, name } = event.data;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = (await transpile(buffer, {
|
|
17
|
+
name,
|
|
18
|
+
instantiation: { tag: "async" },
|
|
19
|
+
})) as TranspileResponse;
|
|
20
|
+
|
|
21
|
+
// Transfer the file buffers back to the main thread (zero-copy)
|
|
22
|
+
const transferables = result.files.map(([, content]) => content.buffer);
|
|
23
|
+
self.postMessage(
|
|
24
|
+
{ success: true, data: result },
|
|
25
|
+
{ transfer: transferables }
|
|
26
|
+
);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
self.postMessage({
|
|
29
|
+
success: false,
|
|
30
|
+
error: error instanceof Error ? error.message : String(error),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class Event {
|
|
2
|
+
private _value: string;
|
|
3
|
+
private _checked: boolean;
|
|
4
|
+
|
|
5
|
+
constructor(_value: string = "", _checked: boolean = false) {
|
|
6
|
+
this._value = _value;
|
|
7
|
+
this._checked = _checked;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
preventDefault() {}
|
|
11
|
+
stopPropagation() {}
|
|
12
|
+
value() {
|
|
13
|
+
return this._value;
|
|
14
|
+
}
|
|
15
|
+
checked() {
|
|
16
|
+
return this._checked;
|
|
17
|
+
}
|
|
18
|
+
}
|