@decky.fx/raven-tsx 0.1.0 → 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/README.md +53 -3
- package/package.json +28 -9
- package/src/core/events.ts +74 -18
- package/src/core/subscriptions.ts +4 -2
- package/src/core/widget-instance.ts +333 -24
- package/src/core/window-instance.ts +423 -8
- package/src/react/host-config.ts +1 -1
- package/src/solid/babel-preset-solid.d.ts +7 -0
- package/src/solid/components.generated.ts +163 -0
- package/src/solid/index.ts +29 -0
- package/src/solid/node.ts +405 -0
- package/src/solid/plugin.ts +108 -0
- package/src/solid/portal.ts +27 -0
- package/src/solid/preload.ts +14 -0
- package/src/solid/primitives.ts +43 -0
- package/src/solid/renderer.ts +37 -0
- package/src/solid/root.ts +137 -0
- package/src/solid/tag.ts +37 -0
- package/src/solid/tags.ts +101 -0
- package/src/solid/teardown.ts +50 -0
- package/src/solid/web.ts +5 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/// <reference path="./babel-preset-solid.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* The Bun plugin that runs an application of `@decky.fx/raven-tsx/solid`. Bun compiles JSX as
|
|
4
|
+
* React does; Solid compiles it to calls of its renderer, with Babel (`babel-preset-solid`, in its
|
|
5
|
+
* universal mode, for the renderer of `@decky.fx/raven-tsx/solid/renderer`). And on Bun, `solid-js`
|
|
6
|
+
* resolves to its build for servers, which does not react: the plugin loads the one for clients.
|
|
7
|
+
*
|
|
8
|
+
* To run: `preload = ["@decky.fx/raven-tsx/solid/preload"]` in bunfig.toml. To build:
|
|
9
|
+
*
|
|
10
|
+
* await Bun.build({ entrypoints: ["src/index.tsx"], plugins: [solidPlugin()], compile: true });
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { BunPlugin } from "bun";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { resolve, sep } from "node:path";
|
|
16
|
+
|
|
17
|
+
/** What the plugin can be told. */
|
|
18
|
+
export interface SolidPluginOptions {
|
|
19
|
+
/** The files it compiles for Solid. All `.tsx` and `.jsx` files outside node_modules by default. */
|
|
20
|
+
readonly filter?: RegExp | undefined;
|
|
21
|
+
/** Takes the development builds of Solid, which warn of mistakes. */
|
|
22
|
+
readonly development?: boolean | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The files compiled by default: `.tsx` and `.jsx`, but those of node_modules, which come compiled. */
|
|
26
|
+
const SOURCES = /^(?!.*[\\/]node_modules[\\/]).*\.[jt]sx$/;
|
|
27
|
+
|
|
28
|
+
/** What `exports` of a package.json gives for a subpath, or a choice of conditions. */
|
|
29
|
+
type ExportsEntry = string | null | { readonly [condition: string]: ExportsEntry };
|
|
30
|
+
|
|
31
|
+
/** The entry of the first condition that is one of these, as Node picks one. */
|
|
32
|
+
function pick(entry: ExportsEntry | undefined, conditions: readonly string[]): string | undefined {
|
|
33
|
+
if (typeof entry === "string") {
|
|
34
|
+
return entry;
|
|
35
|
+
}
|
|
36
|
+
for (const [condition, value] of Object.entries(entry ?? {})) {
|
|
37
|
+
const found = conditions.includes(condition) ? pick(value, conditions) : undefined;
|
|
38
|
+
if (found) {
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The files of Solid for servers, by package folder and build, each with the file for clients that replaces it. */
|
|
46
|
+
const clientFiles = new Map<string, ReadonlyMap<string, string>>();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What replaces each build of a solid-js for servers: the one a client would take for the same
|
|
50
|
+
* import, as Node picks them from `exports` (Bun takes `node` and finds the server's).
|
|
51
|
+
*/
|
|
52
|
+
function clientFilesOf(root: string, development: boolean): ReadonlyMap<string, string> {
|
|
53
|
+
const key = `${root}\0${development}`;
|
|
54
|
+
let files = clientFiles.get(key);
|
|
55
|
+
if (!files) {
|
|
56
|
+
const { exports } = JSON.parse(readFileSync(`${root}/package.json`, "utf8")) as { readonly exports: Readonly<Record<string, ExportsEntry>> };
|
|
57
|
+
const map = new Map<string, string>();
|
|
58
|
+
for (const entry of Object.values(exports)) {
|
|
59
|
+
for (const style of ["import", "require"]) {
|
|
60
|
+
const server = pick(entry, ["node", style, "default"]);
|
|
61
|
+
const client = pick(entry, development ? ["browser", "development", style, "default"] : ["browser", style, "default"]);
|
|
62
|
+
if (server && client && server !== client) {
|
|
63
|
+
map.set(resolve(root, server), resolve(root, client));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
files = map;
|
|
68
|
+
clientFiles.set(key, files);
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The folder of the solid-js package a file of it is in. */
|
|
74
|
+
const solidRoot = (path: string): string => path.slice(0, path.lastIndexOf(`${sep}solid-js${sep}`) + `${sep}solid-js`.length);
|
|
75
|
+
|
|
76
|
+
/** The plugin: compiles TSX for Solid, and takes Solid's build for clients. */
|
|
77
|
+
export function solidPlugin(options: SolidPluginOptions = {}): BunPlugin {
|
|
78
|
+
const development = options.development ?? false;
|
|
79
|
+
return {
|
|
80
|
+
name: "raven-tsx-solid",
|
|
81
|
+
setup(build) {
|
|
82
|
+
// (Bun runs no onResolve of a plugin for a package: the server's file is read as the client's)
|
|
83
|
+
build.onLoad({ filter: /[\\/]solid-js[\\/].*server\.c?js$/ }, ({ path }) => {
|
|
84
|
+
const client = clientFilesOf(solidRoot(path), development).get(path);
|
|
85
|
+
return { contents: readFileSync(client ?? path, "utf8"), loader: "js" };
|
|
86
|
+
});
|
|
87
|
+
build.onLoad({ filter: options.filter ?? SOURCES }, async ({ path }) => {
|
|
88
|
+
// (loaded here, so that an application of React never needs Babel)
|
|
89
|
+
const [{ transformAsync }, { default: solid }] = await Promise.all([import("@babel/core"), import("babel-preset-solid")]);
|
|
90
|
+
const typescript = /\.tsx$/.test(path);
|
|
91
|
+
const result = await transformAsync(await Bun.file(path).text(), {
|
|
92
|
+
filename: path,
|
|
93
|
+
babelrc: false,
|
|
94
|
+
configFile: false,
|
|
95
|
+
// (an error says the line of the source: Bun does not read Babel's source maps)
|
|
96
|
+
retainLines: true,
|
|
97
|
+
// (the types stay, for Bun to take away: Babel only compiles the JSX)
|
|
98
|
+
parserOpts: { plugins: typescript ? ["jsx", "typescript"] : ["jsx"] },
|
|
99
|
+
presets: [[solid, { generate: "universal", moduleName: "@decky.fx/raven-tsx/solid/renderer" }]],
|
|
100
|
+
});
|
|
101
|
+
if (typeof result?.code !== "string") {
|
|
102
|
+
throw new Error(`raven-tsx: Babel compiled nothing from ${path}`);
|
|
103
|
+
}
|
|
104
|
+
return { contents: result.code, loader: typescript ? "ts" : "js" };
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { untrack, type JSX } from "solid-js";
|
|
2
|
+
import type * as r from "../core/raven";
|
|
3
|
+
import { SolidNode } from "./node";
|
|
4
|
+
import { onTeardown } from "./teardown";
|
|
5
|
+
import { insert } from "./renderer";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders its children into a widget outside this part of the tree, as `<Portal>` of solid-js/web:
|
|
9
|
+
* context goes on as if they were here, which a second `RavenRoot` cannot do. For a widget given
|
|
10
|
+
* to Raven by hand, the content of a graph node for one:
|
|
11
|
+
*
|
|
12
|
+
* const box = raven.Rwidget_create_from_xml("<VBox/>");
|
|
13
|
+
* onMount(() => raven.Rnode_graph_set_node_content(graph.widget, node, box));
|
|
14
|
+
* return <Portal widget={box}><Button>Run</Button></Portal>;
|
|
15
|
+
*
|
|
16
|
+
* What holds the widget owns it: the portal only adds and removes the children. The widget is the
|
|
17
|
+
* one it was first given.
|
|
18
|
+
*/
|
|
19
|
+
export function Portal(props: { readonly widget: r.Rid; readonly children?: JSX.Element | undefined }): JSX.Element {
|
|
20
|
+
const container = SolidNode.container(untrack(() => props.widget));
|
|
21
|
+
// (what it showed goes with its own owner: an element kept in a variable can be shown again. Set
|
|
22
|
+
// first: children that fail after some were placed take those away too. With the root as well:
|
|
23
|
+
// a cleanup beside it that throws stops Solid's others)
|
|
24
|
+
onTeardown(() => container.releaseAll());
|
|
25
|
+
insert(container, () => props.children);
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers the plugin of `@decky.fx/raven-tsx/solid/plugin` for what Bun runs and tests:
|
|
3
|
+
*
|
|
4
|
+
* # bunfig.toml
|
|
5
|
+
* preload = ["@decky.fx/raven-tsx/solid/preload"]
|
|
6
|
+
*
|
|
7
|
+
* [test]
|
|
8
|
+
* preload = ["@decky.fx/raven-tsx/solid/preload"]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { plugin } from "bun";
|
|
12
|
+
import { solidPlugin } from "./plugin";
|
|
13
|
+
|
|
14
|
+
plugin(solidPlugin());
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What React has as hooks, for Solid: each lasts as long as the component (or the root) that
|
|
3
|
+
* calls it, and goes with its cleanup.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createEffect } from "solid-js";
|
|
7
|
+
import type * as r from "../core/raven";
|
|
8
|
+
import type { FrameHandler } from "../core/frame";
|
|
9
|
+
import { onAction, onFrame, onShortcut } from "../core/subscriptions";
|
|
10
|
+
import type { WindowInstance } from "../core/window-instance";
|
|
11
|
+
import { onTeardown } from "./teardown";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gives a name to a function, for what fires actions by name: `Rwidget_set_action`, the
|
|
15
|
+
* `Action` of a widget made from XML, `Raction_fire`. It receives the widget that fired the action.
|
|
16
|
+
*/
|
|
17
|
+
export function createAction(name: string, handler: (sender: r.Rid) => void): void {
|
|
18
|
+
onTeardown(onAction(name, handler));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A keyboard shortcut of a window: `createShortcut(() => window, "Ctrl+S", save)`, with `window`
|
|
23
|
+
* set by the `ref` of a `<Window>`. It starts once the window is open. With a signal for the ref
|
|
24
|
+
* (`<Window ref={setWindow}>`, `createShortcut(window, ...)`), it follows a window that comes later.
|
|
25
|
+
*/
|
|
26
|
+
export function createShortcut(window: () => WindowInstance | null | undefined, keys: string, handler: () => void): void {
|
|
27
|
+
createEffect(() => {
|
|
28
|
+
const owner = window();
|
|
29
|
+
if (owner) {
|
|
30
|
+
// (each time the window opens: a window taken out of the tree closes, and opens when shown again)
|
|
31
|
+
onTeardown(owner.whenOpen(() => onShortcut(owner.window, keys, handler)));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A function called once per frame, before the layout, with the seconds since the last frame. It
|
|
38
|
+
* also runs when nothing is happening, about 60 times a second: `runApp` wakes that often so that
|
|
39
|
+
* Bun's timers run beside Raven. Any number of them can live side by side, and with `Rset_frame_callback`.
|
|
40
|
+
*/
|
|
41
|
+
export function createFrame(handler: FrameHandler): void {
|
|
42
|
+
onTeardown(onFrame(handler));
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solid's renderer for Raven: what TSX compiled for Solid calls (`createElement`, `insert`,
|
|
3
|
+
* `spread`...). The plugin of `@decky.fx/raven-tsx/solid/plugin` compiles it to import from here;
|
|
4
|
+
* an application imports `@decky.fx/raven-tsx/solid`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRenderer } from "solid-js/universal";
|
|
8
|
+
import { SolidNode } from "./node";
|
|
9
|
+
|
|
10
|
+
export const {
|
|
11
|
+
render,
|
|
12
|
+
effect,
|
|
13
|
+
memo,
|
|
14
|
+
createComponent,
|
|
15
|
+
createElement,
|
|
16
|
+
createTextNode,
|
|
17
|
+
insertNode,
|
|
18
|
+
insert,
|
|
19
|
+
spread,
|
|
20
|
+
setProp,
|
|
21
|
+
mergeProps,
|
|
22
|
+
use,
|
|
23
|
+
} = createRenderer<SolidNode>({
|
|
24
|
+
createElement: (tag) => SolidNode.element(tag),
|
|
25
|
+
createTextNode: (value) => SolidNode.text(value),
|
|
26
|
+
replaceText: (node, value) => node.setText(value),
|
|
27
|
+
isTextNode: (node) => node.isText,
|
|
28
|
+
setProperty: (node, name, value) => node.setProp(name, value),
|
|
29
|
+
insertNode: (parent, node, anchor) => parent.insert(node, anchor ?? null),
|
|
30
|
+
removeNode: (parent, node) => parent.release(node),
|
|
31
|
+
getParentNode: (node) => node.parent ?? undefined,
|
|
32
|
+
getFirstChild: (node) => node.children[0],
|
|
33
|
+
getNextSibling(node) {
|
|
34
|
+
const siblings = node.parent?.children ?? [];
|
|
35
|
+
return siblings[siblings.indexOf(node) + 1];
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createComponent, createRoot, getOwner, type JSX, type Owner } from "solid-js";
|
|
2
|
+
import * as r from "../core/raven";
|
|
3
|
+
import { RavenError } from "../core/errors";
|
|
4
|
+
import { resolveProps, type RavenProps } from "../core/props";
|
|
5
|
+
import type { WindowOptions } from "../core/props.generated";
|
|
6
|
+
import { makeWindow } from "../core/window-instance";
|
|
7
|
+
import { Window } from "./components.generated";
|
|
8
|
+
import { SolidNode } from "./node";
|
|
9
|
+
import { insert } from "./renderer";
|
|
10
|
+
import { stopLeft, trackRoot } from "./teardown";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A tree of Solid, as `render` of solid-js/web. Given a widget, it renders into it.
|
|
14
|
+
* Without one, it renders `<Window>` elements, each its own window.
|
|
15
|
+
*/
|
|
16
|
+
export class RavenRoot {
|
|
17
|
+
private readonly node: SolidNode;
|
|
18
|
+
private dispose: (() => void) | null = null;
|
|
19
|
+
/** The owner of the tree, which keeps what it subscribed to in Raven: see `teardown`. */
|
|
20
|
+
private owner: Owner | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(parent: r.Rid = r.R_INVALID_ID) {
|
|
23
|
+
this.node = SolidNode.container(parent);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Renders a tree, in place of the one it had. If the tree throws, what it made goes (its cleanups
|
|
28
|
+
* run, its widgets go) and the error goes on. (as Solid's `render`, but the root is kept before the
|
|
29
|
+
* code runs: `render` gives it back only once the code has returned)
|
|
30
|
+
*/
|
|
31
|
+
render(code: () => JSX.Element): void {
|
|
32
|
+
this.unmount();
|
|
33
|
+
// (a cleanup may have rendered a tree again: this render replaces that one too)
|
|
34
|
+
while (this.dispose !== null) {
|
|
35
|
+
this.unmount();
|
|
36
|
+
}
|
|
37
|
+
let own: (() => void) | null = null;
|
|
38
|
+
try {
|
|
39
|
+
createRoot((dispose) => {
|
|
40
|
+
own = dispose;
|
|
41
|
+
this.dispose = dispose;
|
|
42
|
+
this.owner = getOwner();
|
|
43
|
+
if (this.owner) {
|
|
44
|
+
trackRoot(this.owner);
|
|
45
|
+
}
|
|
46
|
+
const view = code();
|
|
47
|
+
// (a render in the code replaced this one, its root with it: what it made went then)
|
|
48
|
+
if (this.dispose === dispose) {
|
|
49
|
+
insert(this.node, view);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
try {
|
|
54
|
+
// (a render in the code that failed replaced it: that tree stays, this one went then)
|
|
55
|
+
if (this.dispose === own) {
|
|
56
|
+
this.unmount();
|
|
57
|
+
}
|
|
58
|
+
} catch (cleanup) {
|
|
59
|
+
console.error("raven-tsx: cleaning up a tree that failed:", cleanup);
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Removes the tree: its cleanups run, and its widgets and windows go. */
|
|
66
|
+
unmount(): void {
|
|
67
|
+
const dispose = this.dispose;
|
|
68
|
+
const owner = this.owner;
|
|
69
|
+
this.dispose = null;
|
|
70
|
+
this.owner = null;
|
|
71
|
+
try {
|
|
72
|
+
dispose?.();
|
|
73
|
+
} finally {
|
|
74
|
+
// (what it subscribed to stops, also what a cleanup that threw kept from stopping)
|
|
75
|
+
if (owner) {
|
|
76
|
+
stopLeft(owner);
|
|
77
|
+
}
|
|
78
|
+
// (the widgets and windows go, also when a cleanup threw. Unless a cleanup rendered a tree
|
|
79
|
+
// again: that one stays, and it took this one away first)
|
|
80
|
+
if (this.dispose === null) {
|
|
81
|
+
this.node.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Makes a window with a place for a tree, and returns the handle of the window with its root. */
|
|
88
|
+
export function createWindow(options: WindowOptions = {}): { window: r.Rid; root: RavenRoot } {
|
|
89
|
+
const { window, content } = makeWindow(resolveProps(options as RavenProps).attributes);
|
|
90
|
+
return { window, root: new RavenRoot(content) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** How `runApp` starts Raven. */
|
|
94
|
+
export interface RunOptions {
|
|
95
|
+
/** The name of the application: `RInitDesc::app_name`, which system notifications show. */
|
|
96
|
+
readonly appName?: string | undefined;
|
|
97
|
+
/** Runs after Raven started and before the first render: for a look, a theme, a registered tag. */
|
|
98
|
+
readonly setup?: (() => void) | undefined;
|
|
99
|
+
/** Raven's allocator: functions of C only, as it allocates on several threads. See `Rset_allocator`. */
|
|
100
|
+
readonly allocator?: Parameters<typeof r.Rset_allocator>[0] | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Starts Raven, shows a tree and runs until the last window closes, the event loop of Bun
|
|
105
|
+
* running beside it. Then the tree is cleaned up and Raven stops, also when the run ends with an
|
|
106
|
+
* error, so that it can start again.
|
|
107
|
+
*
|
|
108
|
+
* With options, the tree goes in one window made from them. Without, the tree says what windows
|
|
109
|
+
* there are, with `<Window>` elements.
|
|
110
|
+
*
|
|
111
|
+
* await runApp(() => <App />, { Title: "Hello", Width: 480, Height: 240 });
|
|
112
|
+
*/
|
|
113
|
+
export async function runApp(code: () => JSX.Element, options?: WindowOptions, start?: RunOptions): Promise<void> {
|
|
114
|
+
if (start?.allocator) {
|
|
115
|
+
r.Rset_allocator(start.allocator);
|
|
116
|
+
}
|
|
117
|
+
const desc = new r.RInitDesc();
|
|
118
|
+
desc.backend = r.R_BACKEND_VULKAN;
|
|
119
|
+
desc.app_name = start?.appName ?? null;
|
|
120
|
+
if (!r.Rinit_ex(desc)) {
|
|
121
|
+
RavenError.fromLibrary("Raven cannot start");
|
|
122
|
+
}
|
|
123
|
+
let root: RavenRoot | null = null;
|
|
124
|
+
try {
|
|
125
|
+
start?.setup?.();
|
|
126
|
+
root = new RavenRoot();
|
|
127
|
+
root.render(options ? () => createComponent(Window, { ...options, get children() { return code(); } }) : code);
|
|
128
|
+
// (Rwait_frame ends once the last window closed: until there was one, it goes on)
|
|
129
|
+
await r.runLoop();
|
|
130
|
+
} finally {
|
|
131
|
+
try {
|
|
132
|
+
root?.unmount();
|
|
133
|
+
} finally {
|
|
134
|
+
r.Rshutdown();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/solid/tag.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** How a tag of Raven becomes a Solid component: what the generated components are made with. */
|
|
2
|
+
|
|
3
|
+
import { getOwner, runWithOwner, splitProps, type JSX } from "solid-js";
|
|
4
|
+
import { WidgetInstance, type HostNode } from "../core/widget-instance";
|
|
5
|
+
import { SolidNode } from "./node";
|
|
6
|
+
import { spread } from "./renderer";
|
|
7
|
+
|
|
8
|
+
/** The props every component has from Solid. */
|
|
9
|
+
export interface SolidProps<R> {
|
|
10
|
+
/** Receives the widget (or the window) as React's `ref` does: `WidgetInstance`, `WindowInstance`. */
|
|
11
|
+
readonly ref?: R | ((instance: R) => void) | undefined;
|
|
12
|
+
readonly children?: JSX.Element | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A component for a tag of Raven, or one given to `Rxml_register_tag` with its props.
|
|
17
|
+
* JSX takes a capitalized name for a component, not for a tag.
|
|
18
|
+
*/
|
|
19
|
+
export function tag<P extends object, R = WidgetInstance>(name: string): (props: P & SolidProps<R>) => JSX.Element {
|
|
20
|
+
return (props) => {
|
|
21
|
+
const node = SolidNode.element(name);
|
|
22
|
+
const [local, others] = splitProps(props, ["ref"]);
|
|
23
|
+
spread(node, others);
|
|
24
|
+
// (Solid compiles `ref={variable}` to a function that sets the variable. As React's, it comes
|
|
25
|
+
// once Raven has the widget, placed: a window comes at once, as it opens only when placed)
|
|
26
|
+
const ref = local.ref;
|
|
27
|
+
if (typeof ref === "function") {
|
|
28
|
+
// (under the owner of the element, also when it comes later: what it makes goes with the
|
|
29
|
+
// element, and what it throws reaches the ErrorBoundary around it)
|
|
30
|
+
const owner = getOwner();
|
|
31
|
+
const give = (instance: HostNode): void => void runWithOwner(owner, () => (ref as (instance: R) => void)(instance as R));
|
|
32
|
+
node.withInstance((instance) => (instance instanceof WidgetInstance ? instance.whenPlaced(give) : give(instance)));
|
|
33
|
+
}
|
|
34
|
+
// (a node of Raven: Solid's JSX types know only the nodes of the DOM)
|
|
35
|
+
return node as unknown as JSX.Element;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solid components as tags of Raven: what `<Widget Name="...">` does in XML, with a component.
|
|
3
|
+
* The core does the part that speaks to Raven; this renders the component, a root of its own per
|
|
4
|
+
* element, whose props follow what the element in XML gives.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { $PROXY, createComponent, createRoot, createSignal, getOwner, runWithOwner, type Component, type JSX, type Owner } from "solid-js";
|
|
8
|
+
import type * as r from "../core/raven";
|
|
9
|
+
import { registerTagWith, type TagAdapter, type TagContent, type TagOptions as CoreTagOptions } from "../core/tags";
|
|
10
|
+
import { RavenError } from "../core/errors";
|
|
11
|
+
import { SolidNode } from "./node";
|
|
12
|
+
import { RavenRoot } from "./root";
|
|
13
|
+
|
|
14
|
+
/** What a tag takes from its element. */
|
|
15
|
+
export type TagOptions<P> = CoreTagOptions<P, JSX.Element>;
|
|
16
|
+
|
|
17
|
+
/** What a registered component is given: its attributes, and its places with their widgets. */
|
|
18
|
+
type TagProps = Readonly<Record<string, string | number | boolean | JSX.Element>>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The props of a component, read from what its element gives now: a prop read in an effect or in
|
|
22
|
+
* JSX follows the element. The widgets of its children are placed where it renders them.
|
|
23
|
+
* Their nodes belong to `owner`, which lasts as long as the element: hiding one does not delete it.
|
|
24
|
+
*/
|
|
25
|
+
function propsOf(content: () => TagContent, owner: Owner | null): TagProps {
|
|
26
|
+
// (a widget keeps its node, so that Solid sees the same child and does not move it)
|
|
27
|
+
const nodes = new Map<r.Rid, SolidNode>();
|
|
28
|
+
const adopted = (widgets: readonly r.Rid[]): JSX.Element => {
|
|
29
|
+
if (widgets.length === 0) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return widgets.map((widget) => {
|
|
33
|
+
let node = nodes.get(widget);
|
|
34
|
+
if (!node) {
|
|
35
|
+
// (not the owner of what reads `children`: `{shown() && props.children}` would delete it when hidden)
|
|
36
|
+
const made = runWithOwner(owner, () => SolidNode.adopted(widget));
|
|
37
|
+
// (Solid gives nothing back when what it ran threw to a boundary of the owner, which has none)
|
|
38
|
+
if (!made) {
|
|
39
|
+
RavenError.throw("raven-tsx: the child of a registered tag could not be adopted");
|
|
40
|
+
}
|
|
41
|
+
node = made;
|
|
42
|
+
nodes.set(widget, node);
|
|
43
|
+
}
|
|
44
|
+
return node as unknown as JSX.Element;
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const read = (): Record<string, string | number | boolean | JSX.Element> => {
|
|
48
|
+
const { attributes, slots, children } = content();
|
|
49
|
+
const places = Object.fromEntries(Object.entries(slots).map(([name, widgets]) => [name, adopted(widgets)]));
|
|
50
|
+
return { ...attributes, ...places, children: adopted(children) };
|
|
51
|
+
};
|
|
52
|
+
// (`$PROXY` says to Solid that the keys can change: `splitProps` then reads through, and sees a key added later)
|
|
53
|
+
return new Proxy({} as TagProps, {
|
|
54
|
+
get: (_, key, receiver) => (key === $PROXY ? receiver : typeof key === "string" ? read()[key] : undefined),
|
|
55
|
+
has: (_, key) => key === $PROXY || (typeof key === "string" && key in read()),
|
|
56
|
+
ownKeys: () => Object.keys(read()),
|
|
57
|
+
getOwnPropertyDescriptor: (_, key) => (typeof key === "string" && key in read() ? { configurable: true, enumerable: true, get: () => read()[key] } : undefined),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Renders a registered component in a root of its own. */
|
|
62
|
+
const solidAdapter: TagAdapter<Component<TagProps>> = {
|
|
63
|
+
mount(component, content, container) {
|
|
64
|
+
const [current, setCurrent] = createSignal(content, { equals: false });
|
|
65
|
+
// (the nodes of its children: they go after the tree, those it holds deleted with it, the others then)
|
|
66
|
+
const [owner, disposeNodes] = createRoot((dispose) => [getOwner(), dispose] as const);
|
|
67
|
+
const props = propsOf(current, owner);
|
|
68
|
+
const root = new RavenRoot(container);
|
|
69
|
+
try {
|
|
70
|
+
root.render(() => createComponent(component, props));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// (the render cleaned up its tree; the nodes of the children have a root of their own)
|
|
73
|
+
try {
|
|
74
|
+
disposeNodes();
|
|
75
|
+
} catch (cleanup) {
|
|
76
|
+
console.error("raven-tsx: cleaning up a tag that failed:", cleanup);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
update: (next) => setCurrent(next),
|
|
82
|
+
unmount: () => {
|
|
83
|
+
try {
|
|
84
|
+
root.unmount();
|
|
85
|
+
} finally {
|
|
86
|
+
disposeNodes();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Makes a component a tag Raven knows, built from XML as its own tags are: in documents, in
|
|
95
|
+
* `Markup`, in files. Its attributes become props of the kinds given; the widgets of its children
|
|
96
|
+
* are placed where it renders `children`, and those of `<Into Name="...">` where it renders the
|
|
97
|
+
* prop of that name. Registering a name again replaces it. Returns what unregisters it.
|
|
98
|
+
*/
|
|
99
|
+
export function registerTag<P extends object>(name: string, component: Component<P>, options: TagOptions<P> = {}): () => void {
|
|
100
|
+
return registerTagWith<Component<TagProps>, P, JSX.Element>(solidAdapter, name, component as unknown as Component<TagProps>, options);
|
|
101
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the primitives subscribe to in Raven, kept by the root of the tree as well as by Solid's
|
|
3
|
+
* cleanup. Solid runs the cleanups of an owner without catching: one that throws stops the others,
|
|
4
|
+
* and an action would still be bound after its component went. The root stops what is left.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getOwner, onCleanup, type Owner } from "solid-js";
|
|
8
|
+
|
|
9
|
+
/** What each root of a tree keeps, to stop at its end what its cleanups did not. */
|
|
10
|
+
const roots = new WeakMap<Owner, Set<() => void>>();
|
|
11
|
+
|
|
12
|
+
/** Says that an owner is the root of a tree, which keeps what is subscribed under it. */
|
|
13
|
+
export function trackRoot(owner: Owner): void {
|
|
14
|
+
roots.set(owner, new Set());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stops a subscription with the cleanup of the current owner, or with the end of its root if a
|
|
19
|
+
* cleanup that throws comes first. It stops once.
|
|
20
|
+
*/
|
|
21
|
+
export function onTeardown(stop: () => void): void {
|
|
22
|
+
let kept: Set<() => void> | undefined;
|
|
23
|
+
let done = false;
|
|
24
|
+
const run = (): void => {
|
|
25
|
+
if (done) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
done = true;
|
|
29
|
+
kept?.delete(run);
|
|
30
|
+
stop();
|
|
31
|
+
};
|
|
32
|
+
for (let owner = getOwner(); owner && !kept; owner = owner.owner) {
|
|
33
|
+
kept = roots.get(owner);
|
|
34
|
+
}
|
|
35
|
+
kept?.add(run);
|
|
36
|
+
onCleanup(run);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Stops what a root still keeps, after its cleanups ran: those a cleanup that threw left. */
|
|
40
|
+
export function stopLeft(owner: Owner): void {
|
|
41
|
+
const kept = roots.get(owner);
|
|
42
|
+
roots.delete(owner);
|
|
43
|
+
for (const run of [...(kept ?? [])]) {
|
|
44
|
+
try {
|
|
45
|
+
run();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("raven-tsx: stopping what a tree subscribed to:", error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|