@smithers-orchestrator/react-reconciler 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 William Cory
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/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@smithers-orchestrator/react-reconciler",
3
+ "version": "0.16.0",
4
+ "description": "Custom React reconciler that renders Smithers host trees",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.d.ts",
10
+ "import": "./src/index.js",
11
+ "default": "./src/index.js"
12
+ },
13
+ "./context": {
14
+ "types": "./src/index.d.ts",
15
+ "import": "./src/context.js",
16
+ "default": "./src/context.js"
17
+ },
18
+ "./devtools": {
19
+ "types": "./src/index.d.ts",
20
+ "import": "./src/devtools/index.js",
21
+ "default": "./src/devtools/index.js"
22
+ },
23
+ "./devtools/preload": {
24
+ "import": "./src/devtools/preload.js",
25
+ "default": "./src/devtools/preload.js"
26
+ },
27
+ "./driver": {
28
+ "types": "./src/index.d.ts",
29
+ "import": "./src/driver.js",
30
+ "default": "./src/driver.js"
31
+ },
32
+ "./jsx-runtime": {
33
+ "types": "./src/index.d.ts",
34
+ "import": "./src/jsx-runtime.js",
35
+ "default": "./src/jsx-runtime.js"
36
+ },
37
+ "./*": {
38
+ "types": "./src/index.d.ts",
39
+ "import": "./src/*.js",
40
+ "default": "./src/*.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "src/"
45
+ ],
46
+ "dependencies": {
47
+ "react": "^18.3.0 || ^19.0.0",
48
+ "react-reconciler": "^0.31.0",
49
+ "bippy": "^0.5.32",
50
+ "@smithers-orchestrator/devtools": "0.16.0",
51
+ "@smithers-orchestrator/errors": "0.16.0",
52
+ "@smithers-orchestrator/driver": "0.16.0",
53
+ "@smithers-orchestrator/graph": "0.16.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/bun": "latest",
57
+ "typescript": "~5.9.3"
58
+ },
59
+ "scripts": {
60
+ "build": "rm -f src/index.d.ts && tsup --dts-only",
61
+ "test": "bun test tests",
62
+ "typecheck": "tsc -p tsconfig.json --noEmit"
63
+ }
64
+ }
@@ -0,0 +1,5 @@
1
+ import type { HostNode } from "@smithers-orchestrator/graph/types";
2
+
3
+ export type HostContainer = {
4
+ root: HostNode | null;
5
+ };
@@ -0,0 +1 @@
1
+ export type { OutputSnapshot } from "@smithers-orchestrator/driver/OutputSnapshot";
@@ -0,0 +1 @@
1
+ export type { SmithersCtxOptions } from "@smithers-orchestrator/driver/SmithersCtx";
@@ -0,0 +1,5 @@
1
+ import type { ExtractGraph } from "@smithers-orchestrator/graph/types";
2
+
3
+ export type SmithersRendererOptions = {
4
+ extractGraph?: ExtractGraph;
5
+ };
package/src/context.js ADDED
@@ -0,0 +1,32 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./OutputSnapshot.ts").OutputSnapshot} OutputSnapshot */
3
+ /** @typedef {import("./SmithersCtxOptions.ts").SmithersCtxOptions} SmithersCtxOptions */
4
+ // @smithers-type-exports-end
5
+
6
+ import React from "react";
7
+ import { SmithersCtx } from "@smithers-orchestrator/driver/SmithersCtx";
8
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
9
+ export { SmithersCtx } from "@smithers-orchestrator/driver/SmithersCtx";
10
+ /** @type {React.Context<SmithersCtx<any> | null>} */
11
+ export const SmithersContext = React.createContext(null);
12
+ SmithersContext.displayName = "SmithersContext";
13
+ /**
14
+ * @template Schema
15
+ * @returns {{ SmithersContext: React.Context<SmithersCtx<Schema> | null>, useCtx: () => SmithersCtx<Schema> }}
16
+ */
17
+ export function createSmithersContext() {
18
+ /** @type {React.Context<SmithersCtx<Schema> | null>} */
19
+ const Context = React.createContext(null);
20
+ Context.displayName = "SmithersContext";
21
+ /**
22
+ * @returns {SmithersCtx<Schema>}
23
+ */
24
+ function useCtx() {
25
+ const ctx = React.useContext(Context);
26
+ if (!ctx) {
27
+ throw new SmithersError("CONTEXT_OUTSIDE_WORKFLOW", "useCtx() must be called inside a <Workflow> created by createSmithers()");
28
+ }
29
+ return ctx;
30
+ }
31
+ return { SmithersContext: Context, useCtx };
32
+ }
@@ -0,0 +1,33 @@
1
+
2
+ /** @typedef {import("@smithers-orchestrator/graph/types").ExtractGraph} ExtractGraph */
3
+ const GRAPH_SPECIFIER = "@smithers-orchestrator/graph";
4
+ const LOCAL_GRAPH_SPECIFIER = "../../graph/src/index.js";
5
+ /**
6
+ * @param {string} specifier
7
+ * @returns {Promise<CoreModule | null>}
8
+ */
9
+ async function importCoreModule(specifier) {
10
+ try {
11
+ return (await import(specifier));
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ /**
18
+ * @returns {Promise<ExtractGraph>}
19
+ */
20
+ export async function resolveExtractGraph() {
21
+ const modules = [
22
+ await importCoreModule(GRAPH_SPECIFIER),
23
+ await importCoreModule(LOCAL_GRAPH_SPECIFIER),
24
+ ];
25
+ for (const mod of modules) {
26
+ const fn = mod?.extractGraph;
27
+ if (typeof fn === "function") {
28
+ return fn;
29
+ }
30
+ }
31
+ throw new Error("Unable to load extractGraph from @smithers-orchestrator/graph. " +
32
+ "Install @smithers-orchestrator/graph and ensure it exports extractGraph.");
33
+ }
@@ -0,0 +1 @@
1
+ export * from "@smithers-orchestrator/graph";
@@ -0,0 +1,360 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("@smithers-orchestrator/devtools").DevToolsEventBus} DevToolsEventBus */
3
+ /** @typedef {import("@smithers-orchestrator/devtools").DevToolsEventHandler} DevToolsEventHandler */
4
+ /** @typedef {import("@smithers-orchestrator/devtools").DevToolsSnapshot} DevToolsSnapshot */
5
+ /** @typedef {import("@smithers-orchestrator/devtools").RunExecutionState} RunExecutionState */
6
+ /** @typedef {import("@smithers-orchestrator/devtools").SmithersNodeType} SmithersNodeType */
7
+ /** @typedef {import("@smithers-orchestrator/devtools").TaskExecutionState} TaskExecutionState */
8
+ // @smithers-type-exports-end
9
+
10
+ import { instrument, secure, installRDTHook, traverseFiber, getDisplayName, isHostFiber, getFiberId, setFiberId, } from "bippy";
11
+ import { SmithersDevToolsCore, printTree, } from "@smithers-orchestrator/devtools";
12
+ /** @typedef {import("@smithers-orchestrator/devtools").DevToolsNode} DevToolsNode */
13
+ /** @typedef {import("@smithers-orchestrator/devtools").SmithersDevToolsCore} SmithersDevToolsCoreType */
14
+ /** @typedef {import("@smithers-orchestrator/devtools").SmithersDevToolsOptions} SmithersDevToolsOptions */
15
+ /** @typedef {import("bippy").Fiber} Fiber */
16
+ /** @typedef {import("bippy").FiberRoot} FiberRoot */
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // React host tag mapping
20
+ // ---------------------------------------------------------------------------
21
+ const HOST_TAG_MAP = {
22
+ "smithers:workflow": "workflow",
23
+ "smithers:task": "task",
24
+ "smithers:sequence": "sequence",
25
+ "smithers:parallel": "parallel",
26
+ "smithers:merge-queue": "merge-queue",
27
+ "smithers:branch": "branch",
28
+ "smithers:ralph": "loop",
29
+ "smithers:worktree": "worktree",
30
+ "smithers:approval": "approval",
31
+ "smithers:timer": "timer",
32
+ "smithers:subflow": "subflow",
33
+ "smithers:wait-for-event": "wait-for-event",
34
+ "smithers:saga": "saga",
35
+ "smithers:try-catch-finally": "try-catch",
36
+ };
37
+ /**
38
+ * @param {Fiber} fiber
39
+ * @returns {SmithersNodeType | null}
40
+ */
41
+ function resolveNodeType(fiber) {
42
+ // Only match host fibers (smithers:* tags).
43
+ // Composite fibers (Task, Workflow, etc.) are pass-throughs that always
44
+ // create a host fiber underneath, so matching both would double-count.
45
+ if (isHostFiber(fiber)) {
46
+ const tag = fiber.type;
47
+ return HOST_TAG_MAP[tag] ?? null;
48
+ }
49
+ return null;
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Fiber to DevToolsNode
53
+ // ---------------------------------------------------------------------------
54
+ /**
55
+ * @param {Fiber} fiber
56
+ * @returns {Record<string, unknown>}
57
+ */
58
+ function extractSerializableProps(fiber) {
59
+ const raw = fiber.memoizedProps;
60
+ if (!raw || typeof raw !== "object")
61
+ return {};
62
+ const out = {};
63
+ for (const [key, value] of Object.entries(raw)) {
64
+ if (key.startsWith("__"))
65
+ continue;
66
+ if (key === "children")
67
+ continue;
68
+ if (typeof value === "function") {
69
+ out[key] = "[Function]";
70
+ continue;
71
+ }
72
+ if (typeof value === "object" && value !== null) {
73
+ // Agent objects, Zod schemas, Drizzle tables: record a stable label only.
74
+ if ("modelId" in value || "model" in value) {
75
+ out[key] = `[Agent: ${value.modelId ?? value.model ?? "unknown"}]`;
76
+ continue;
77
+ }
78
+ if ("shape" in value) {
79
+ out[key] = "[ZodSchema]";
80
+ continue;
81
+ }
82
+ out[key] = "[Object]";
83
+ continue;
84
+ }
85
+ out[key] = value;
86
+ }
87
+ return out;
88
+ }
89
+ /**
90
+ * @param {Fiber} fiber
91
+ * @returns {DevToolsNode["task"] | undefined}
92
+ */
93
+ function extractTaskInfo(fiber) {
94
+ const raw = fiber.memoizedProps;
95
+ if (!raw || typeof raw !== "object")
96
+ return undefined;
97
+ const nodeId = raw.id;
98
+ if (typeof nodeId !== "string")
99
+ return undefined;
100
+ const kind = raw.__smithersKind === "agent"
101
+ ? "agent"
102
+ : raw.__smithersKind === "compute"
103
+ ? "compute"
104
+ : "static";
105
+ const agent = raw.agent;
106
+ let agentName;
107
+ if (agent) {
108
+ if (Array.isArray(agent)) {
109
+ agentName = agent
110
+ .map((a) => a?.modelId ?? a?.model ?? "?")
111
+ .join(" → ");
112
+ }
113
+ else {
114
+ agentName = agent.modelId ?? agent.model ?? "agent";
115
+ }
116
+ }
117
+ return {
118
+ nodeId,
119
+ kind,
120
+ agent: agentName,
121
+ label: raw.label,
122
+ outputTableName: typeof raw.output === "string" ? raw.output : undefined,
123
+ iteration: raw.iteration,
124
+ };
125
+ }
126
+ /**
127
+ * @param {Fiber} fiber
128
+ * @param {number} depth
129
+ * @returns {DevToolsNode | null}
130
+ */
131
+ function fiberToNode(fiber, depth) {
132
+ const nodeType = resolveNodeType(fiber);
133
+ if (!nodeType)
134
+ return null;
135
+ const id = getFiberId(fiber) ?? setFiberId(fiber);
136
+ const children = [];
137
+ let child = fiber.child;
138
+ while (child) {
139
+ const childNode = fiberToNode(child, depth + 1);
140
+ if (childNode) {
141
+ children.push(childNode);
142
+ }
143
+ else {
144
+ // Recurse through non-Smithers fibers to find nested Smithers nodes.
145
+ let grandchild = child.child;
146
+ while (grandchild) {
147
+ const gc = fiberToNode(grandchild, depth + 1);
148
+ if (gc)
149
+ children.push(gc);
150
+ grandchild = grandchild.sibling;
151
+ }
152
+ }
153
+ child = child.sibling;
154
+ }
155
+ return {
156
+ id,
157
+ type: nodeType,
158
+ name: getDisplayName(fiber) ?? fiber.type ?? "unknown",
159
+ props: extractSerializableProps(fiber),
160
+ task: nodeType === "task" ? extractTaskInfo(fiber) : undefined,
161
+ children,
162
+ depth,
163
+ };
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Walk fiber root to find Smithers root
167
+ // ---------------------------------------------------------------------------
168
+ /**
169
+ * @param {FiberRoot | null | undefined} fiberRoot
170
+ * @returns {Fiber | null}
171
+ */
172
+ function findSmithersRoot(fiberRoot) {
173
+ const current = fiberRoot?.current;
174
+ if (!current)
175
+ return null;
176
+ let result = null;
177
+ traverseFiber(current, (fiber) => {
178
+ const nodeType = resolveNodeType(fiber);
179
+ if (nodeType === "workflow") {
180
+ result = fiber;
181
+ return true;
182
+ }
183
+ return false;
184
+ });
185
+ return result;
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // React adapter
189
+ // ---------------------------------------------------------------------------
190
+ export class SmithersDevTools {
191
+ /** @type {SmithersDevToolsOptions} */
192
+ options;
193
+ /** @type {SmithersDevToolsCoreType} */
194
+ core;
195
+ _active = false;
196
+ _cleanup = null;
197
+ /**
198
+ * @param {SmithersDevToolsOptions} [options]
199
+ */
200
+ constructor(options = {}) {
201
+ this.options = options;
202
+ this.core = new SmithersDevToolsCore(options);
203
+ }
204
+ /**
205
+ * Start instrumenting. Installs the React DevTools global hook (if not
206
+ * already present) and begins listening for fiber commits.
207
+ *
208
+ * For best results, call this before `SmithersRenderer` is first imported.
209
+ * If the renderer is already loaded, it will still work as long as the
210
+ * renderer called `injectIntoDevTools` (which it does by default).
211
+ */
212
+ start() {
213
+ if (this._active)
214
+ return this;
215
+ this._active = true;
216
+ // Avoid reinstalling the hook after renderers have already injected
217
+ // themselves, otherwise we lose their registrations.
218
+ if (!("__REACT_DEVTOOLS_GLOBAL_HOOK__" in globalThis)) {
219
+ installRDTHook();
220
+ }
221
+ const self = this;
222
+ const verbose = this.options.verbose ?? false;
223
+ const hookHost = globalThis;
224
+ const hook = hookHost.__REACT_DEVTOOLS_GLOBAL_HOOK__;
225
+ const previousRootHandler = hook?.onCommitFiberRoot;
226
+ const previousUnmountHandler = hook?.onCommitFiberUnmount;
227
+ instrument(secure({
228
+ /**
229
+ * @param {number} rendererID
230
+ * @param {FiberRoot} root
231
+ * @returns {void}
232
+ */
233
+ onCommitFiberRoot(rendererID, root) {
234
+ const smithersRoot = findSmithersRoot(root);
235
+ const tree = smithersRoot ? fiberToNode(smithersRoot, 0) : null;
236
+ const snapshot = self.core.captureSnapshot(tree);
237
+ if (verbose && tree) {
238
+ console.log("\n🔍 [smithers-devtools] Commit detected:\n" +
239
+ printTree(tree) +
240
+ ` ${snapshot.nodeCount} nodes, ${snapshot.taskCount} tasks\n`);
241
+ }
242
+ self.core.emitCommit(snapshot);
243
+ },
244
+ /**
245
+ * @param {number} _rendererID
246
+ * @param {Fiber} fiber
247
+ * @returns {void}
248
+ */
249
+ onCommitFiberUnmount(_rendererID, fiber) {
250
+ const nodeType = resolveNodeType(fiber);
251
+ if (nodeType && verbose) {
252
+ const name = getDisplayName(fiber) ?? fiber.type;
253
+ console.log(`🗑️ [smithers-devtools] Unmounted: ${nodeType} (${name})`);
254
+ }
255
+ self.core.emitUnmount();
256
+ },
257
+ }));
258
+ const installedRootHandler = hook?.onCommitFiberRoot;
259
+ const installedUnmountHandler = hook?.onCommitFiberUnmount;
260
+ this._cleanup = () => {
261
+ if (!hook)
262
+ return;
263
+ if (hook.onCommitFiberRoot === installedRootHandler) {
264
+ if (previousRootHandler) {
265
+ hook.onCommitFiberRoot = previousRootHandler;
266
+ }
267
+ else {
268
+ delete hook.onCommitFiberRoot;
269
+ }
270
+ }
271
+ if (hook.onCommitFiberUnmount === installedUnmountHandler) {
272
+ if (previousUnmountHandler) {
273
+ hook.onCommitFiberUnmount = previousUnmountHandler;
274
+ }
275
+ else {
276
+ delete hook.onCommitFiberUnmount;
277
+ }
278
+ }
279
+ };
280
+ return this;
281
+ }
282
+ /** Stop instrumenting and clean up. */
283
+ stop() {
284
+ this._cleanup?.();
285
+ this._cleanup = null;
286
+ this._active = false;
287
+ this.core.detachEventBuses();
288
+ }
289
+ /**
290
+ * Attach to a Smithers EventBus to track task execution state.
291
+ * Listens for SmithersEvent emissions and builds up a run state model.
292
+ * @param {DevToolsEventBus} bus
293
+ * @returns {this}
294
+ */
295
+ attachEventBus(bus) {
296
+ this.core.attachEventBus(bus);
297
+ return this;
298
+ }
299
+ /**
300
+ * Get execution state for a specific run.
301
+ * @param {string} runId
302
+ * @returns {RunExecutionState | undefined}
303
+ */
304
+ getRun(runId) {
305
+ return this.core.getRun(runId);
306
+ }
307
+ /**
308
+ * Get all tracked runs.
309
+ * @returns {Map<string, RunExecutionState>}
310
+ */
311
+ get runs() {
312
+ return this.core.runs;
313
+ }
314
+ /**
315
+ * Get task execution state by nodeId within a run. Searches all iterations.
316
+ * @param {string} runId
317
+ * @param {string} nodeId
318
+ * @param {number} [iteration]
319
+ * @returns {TaskExecutionState | undefined}
320
+ */
321
+ getTaskState(runId, nodeId, iteration) {
322
+ return this.core.getTaskState(runId, nodeId, iteration);
323
+ }
324
+ /**
325
+ * Get the last captured snapshot.
326
+ * @returns {DevToolsSnapshot | null}
327
+ */
328
+ get snapshot() {
329
+ return this.core.snapshot;
330
+ }
331
+ /**
332
+ * Get the current tree (shorthand).
333
+ * @returns {DevToolsNode | null}
334
+ */
335
+ get tree() {
336
+ return this.core.tree;
337
+ }
338
+ /**
339
+ * Pretty-print the current tree to a string.
340
+ * @returns {string}
341
+ */
342
+ printTree() {
343
+ return this.core.printTree();
344
+ }
345
+ /**
346
+ * Find a node by task nodeId.
347
+ * @param {string} nodeId
348
+ * @returns {DevToolsNode | null}
349
+ */
350
+ findTask(nodeId) {
351
+ return this.core.findTask(nodeId);
352
+ }
353
+ /**
354
+ * List all tasks in the current tree.
355
+ * @returns {DevToolsNode[]}
356
+ */
357
+ listTasks() {
358
+ return this.core.listTasks();
359
+ }
360
+ }
@@ -0,0 +1,12 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("@smithers-orchestrator/devtools").DevToolsEventBus} DevToolsEventBus */
3
+ /** @typedef {import("@smithers-orchestrator/devtools").DevToolsEventHandler} DevToolsEventHandler */
4
+ /** @typedef {import("@smithers-orchestrator/devtools").DevToolsNode} DevToolsNode */
5
+ /** @typedef {import("@smithers-orchestrator/devtools").DevToolsSnapshot} DevToolsSnapshot */
6
+ /** @typedef {import("@smithers-orchestrator/devtools").RunExecutionState} RunExecutionState */
7
+ /** @typedef {import("@smithers-orchestrator/devtools").SmithersDevToolsOptions} SmithersDevToolsOptions */
8
+ /** @typedef {import("@smithers-orchestrator/devtools").SmithersNodeType} SmithersNodeType */
9
+ /** @typedef {import("@smithers-orchestrator/devtools").TaskExecutionState} TaskExecutionState */
10
+ // @smithers-type-exports-end
11
+
12
+ export { SmithersDevTools } from "./SmithersDevTools.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Preload script that installs the React DevTools global hook before
3
+ * any React code runs. Use this as a Bun preload:
4
+ *
5
+ * bun --preload ./src/devtools/preload.js run myworkflow.tsx
6
+ *
7
+ * Or in bunfig.toml:
8
+ * preload = ["./src/devtools/preload.js"]
9
+ */
10
+ import { installRDTHook } from "bippy";
11
+ installRDTHook();
@@ -0,0 +1 @@
1
+ export * from "../reconciler.js";
package/src/driver.js ADDED
@@ -0,0 +1,19 @@
1
+ import { WorkflowDriver, } from "@smithers-orchestrator/driver";
2
+ import { SmithersRenderer } from "./reconciler.js";
3
+
4
+ /**
5
+ * @template [Schema=unknown]
6
+ * @extends {WorkflowDriver<Schema>}
7
+ */
8
+ export class ReactWorkflowDriver extends WorkflowDriver {
9
+ /**
10
+ * @param {import("@smithers-orchestrator/driver").WorkflowDriverOptions<Schema>} options
11
+ */
12
+ constructor(options) {
13
+ const renderer = options.renderer ?? new SmithersRenderer();
14
+ super({
15
+ ...options,
16
+ renderer,
17
+ });
18
+ }
19
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,87 @@
1
+ import * as _smithers_orchestrator_graph_types from '@smithers-orchestrator/graph/types';
2
+ import { ExtractGraph as ExtractGraph$1, HostNode as HostNode$1 } from '@smithers-orchestrator/graph/types';
3
+ import * as React$1 from 'react';
4
+ import React__default from 'react';
5
+ import { WorkflowDriver } from '@smithers-orchestrator/driver';
6
+ import { SmithersCtx } from '@smithers-orchestrator/driver/SmithersCtx';
7
+ export { SmithersCtx } from '@smithers-orchestrator/driver/SmithersCtx';
8
+
9
+ type SmithersRendererOptions$1 = {
10
+ extractGraph?: ExtractGraph$1;
11
+ };
12
+
13
+ type HostContainer$1 = {
14
+ root: HostNode$1 | null;
15
+ };
16
+
17
+ declare class SmithersRenderer {
18
+ /**
19
+ * @param {SmithersRendererOptions} [options]
20
+ */
21
+ constructor(options?: SmithersRendererOptions);
22
+ /** @type {HostContainer} */
23
+ container: HostContainer;
24
+ /** @type {unknown} */
25
+ root: unknown;
26
+ /** @type {ExtractGraph | undefined} */
27
+ extractGraph: ExtractGraph | undefined;
28
+ /**
29
+ * @param {React.ReactElement} element
30
+ * @param {ExtractOptions} [opts]
31
+ * @returns {Promise<WorkflowGraph>}
32
+ */
33
+ render(element: React.ReactElement, opts?: ExtractOptions): Promise<WorkflowGraph>;
34
+ /**
35
+ * @returns {HostNode | null}
36
+ */
37
+ getRoot(): HostNode | null;
38
+ }
39
+ type ExtractGraph = _smithers_orchestrator_graph_types.ExtractGraph;
40
+ type ExtractOptions = _smithers_orchestrator_graph_types.ExtractOptions;
41
+ type HostContainer = HostContainer$1;
42
+ type MutableHostElement = _smithers_orchestrator_graph_types.HostElement & {
43
+ props: Record<string, string>;
44
+ rawProps: Record<string, unknown>;
45
+ children: HostNode[];
46
+ };
47
+ type HostNode = _smithers_orchestrator_graph_types.HostNode;
48
+ type MutableHostText = _smithers_orchestrator_graph_types.HostText & {
49
+ text: string;
50
+ };
51
+ type React = React$1.default;
52
+ type SmithersRendererOptions = SmithersRendererOptions$1;
53
+ type WorkflowGraph = _smithers_orchestrator_graph_types.WorkflowGraph;
54
+ /**
55
+ * Minimal local shape for a react-reconciler instance. `@types/react-reconciler`
56
+ * is not installed here, so we describe only the methods we call.
57
+ */
58
+ type ReconcilerInstance = {
59
+ createContainer: (rootContainer: unknown, tag: number, hydrationCallbacks: unknown, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: unknown, identifierPrefix: string, onUncaughtError: unknown, onCaughtError: unknown, onRecoverableError: unknown, transitionCallbacks: unknown) => unknown;
60
+ updateContainerSync: (element: unknown, container: unknown, parentComponent: unknown, callback: () => void) => void;
61
+ flushSyncWork: () => void;
62
+ injectIntoDevTools: (devtools: unknown) => void;
63
+ defaultOnUncaughtError: unknown;
64
+ defaultOnCaughtError: unknown;
65
+ defaultOnRecoverableError: unknown;
66
+ };
67
+
68
+ /**
69
+ * @template [Schema=unknown]
70
+ * @extends {WorkflowDriver<Schema>}
71
+ */
72
+ declare class ReactWorkflowDriver<Schema = unknown> extends WorkflowDriver<Schema> {
73
+ }
74
+
75
+ /**
76
+ * @template Schema
77
+ * @returns {{ SmithersContext: React.Context<SmithersCtx<Schema> | null>, useCtx: () => SmithersCtx<Schema> }}
78
+ */
79
+ declare function createSmithersContext<Schema>(): {
80
+ SmithersContext: React__default.Context<SmithersCtx<Schema> | null>;
81
+ useCtx: () => SmithersCtx<Schema>;
82
+ };
83
+
84
+ /** @type {React.Context<SmithersCtx<any> | null>} */
85
+ declare const SmithersContext: React__default.Context<SmithersCtx<any> | null>;
86
+
87
+ export { type ExtractGraph, type ExtractOptions, type HostContainer, type HostNode, type MutableHostElement, type MutableHostText, type React, ReactWorkflowDriver, type ReconcilerInstance, SmithersContext, SmithersRenderer, type SmithersRendererOptions, type WorkflowGraph, createSmithersContext };
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./reconciler.js";
2
+ export { ReactWorkflowDriver } from "./driver.js";
3
+ export { SmithersContext, SmithersCtx, createSmithersContext } from "./context.js";
@@ -0,0 +1,3 @@
1
+ /** @typedef {import("react").ReactElement} JSX.Element */
2
+ export { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
+ export { jsxDEV } from "react/jsx-dev-runtime";
@@ -0,0 +1,387 @@
1
+ import Reconciler from "react-reconciler";
2
+ import { installRDTHook } from "bippy";
3
+ import { resolveExtractGraph } from "./core-peer.js";
4
+ /** @typedef {import("@smithers-orchestrator/graph/types").ExtractGraph} ExtractGraph */
5
+ /** @typedef {import("@smithers-orchestrator/graph/types").ExtractOptions} ExtractOptions */
6
+ /** @typedef {import("./HostContainer.ts").HostContainer} HostContainer */
7
+ /** @typedef {import("@smithers-orchestrator/graph/types").HostElement & { props: Record<string, string>; rawProps: Record<string, unknown>; children: HostNode[] }} MutableHostElement */
8
+ /** @typedef {import("@smithers-orchestrator/graph/types").HostNode} HostNode */
9
+ /** @typedef {import("@smithers-orchestrator/graph/types").HostText & { text: string }} MutableHostText */
10
+ /** @typedef {import("react").default} React */
11
+ /** @typedef {import("./SmithersRendererOptions.ts").SmithersRendererOptions} SmithersRendererOptions */
12
+ /** @typedef {import("@smithers-orchestrator/graph/types").WorkflowGraph} WorkflowGraph */
13
+ /**
14
+ * Minimal local shape for a react-reconciler instance. `@types/react-reconciler`
15
+ * is not installed here, so we describe only the methods we call.
16
+ * @typedef {{
17
+ * createContainer: (
18
+ * rootContainer: unknown,
19
+ * tag: number,
20
+ * hydrationCallbacks: unknown,
21
+ * isStrictMode: boolean,
22
+ * concurrentUpdatesByDefaultOverride: unknown,
23
+ * identifierPrefix: string,
24
+ * onUncaughtError: unknown,
25
+ * onCaughtError: unknown,
26
+ * onRecoverableError: unknown,
27
+ * transitionCallbacks: unknown,
28
+ * ) => unknown;
29
+ * updateContainerSync: (element: unknown, container: unknown, parentComponent: unknown, callback: () => void) => void;
30
+ * flushSyncWork: () => void;
31
+ * injectIntoDevTools: (devtools: unknown) => void;
32
+ * defaultOnUncaughtError: unknown;
33
+ * defaultOnCaughtError: unknown;
34
+ * defaultOnRecoverableError: unknown;
35
+ * }} ReconcilerInstance */
36
+
37
+ /**
38
+ * @param {string} type
39
+ * @param {Record<string, unknown>} props
40
+ * @returns {MutableHostElement}
41
+ */
42
+ function createElement(type, props) {
43
+ const { children: _children, ...rest } = props || {};
44
+ const stringProps = {};
45
+ for (const [key, value] of Object.entries(rest)) {
46
+ if (value === undefined || typeof value === "function")
47
+ continue;
48
+ if (key.startsWith("__"))
49
+ continue;
50
+ if (typeof value === "string" ||
51
+ typeof value === "number" ||
52
+ typeof value === "boolean") {
53
+ stringProps[key] = String(value);
54
+ }
55
+ }
56
+ return {
57
+ kind: "element",
58
+ tag: type,
59
+ props: stringProps,
60
+ rawProps: props ?? {},
61
+ children: [],
62
+ };
63
+ }
64
+ let currentUpdatePriority = 1;
65
+ const hostConfig = {
66
+ supportsMutation: true,
67
+ supportsPersistence: false,
68
+ supportsHydration: false,
69
+ isPrimaryRenderer: true,
70
+ supportsMicrotasks: true,
71
+ /**
72
+ * @returns {Record<string, unknown>}
73
+ */
74
+ getRootHostContext() {
75
+ return {};
76
+ },
77
+ /**
78
+ * @returns {Record<string, unknown>}
79
+ */
80
+ getChildHostContext() {
81
+ return {};
82
+ },
83
+ /**
84
+ * @param {unknown} instance
85
+ * @returns {unknown}
86
+ */
87
+ getPublicInstance(instance) {
88
+ return instance;
89
+ },
90
+ /**
91
+ * @param {string} type
92
+ * @param {Record<string, unknown>} props
93
+ * @returns {MutableHostElement}
94
+ */
95
+ createInstance(type, props) {
96
+ return createElement(type, props);
97
+ },
98
+ /**
99
+ * @param {string} text
100
+ * @returns {MutableHostText}
101
+ */
102
+ createTextInstance(text) {
103
+ return { kind: "text", text };
104
+ },
105
+ /**
106
+ * @param {MutableHostElement} parent
107
+ * @param {HostNode} child
108
+ * @returns {void}
109
+ */
110
+ appendInitialChild(parent, child) {
111
+ parent.children.push(child);
112
+ },
113
+ /**
114
+ * @param {MutableHostElement} parent
115
+ * @param {HostNode} child
116
+ * @returns {void}
117
+ */
118
+ appendChild(parent, child) {
119
+ parent.children.push(child);
120
+ },
121
+ /**
122
+ * @param {HostContainer} container
123
+ * @param {HostNode} child
124
+ * @returns {void}
125
+ */
126
+ appendChildToContainer(container, child) {
127
+ container.root = child;
128
+ },
129
+ /**
130
+ * @param {MutableHostElement} parent
131
+ * @param {HostNode} child
132
+ * @returns {void}
133
+ */
134
+ removeChild(parent, child) {
135
+ const idx = parent.children.indexOf(child);
136
+ if (idx >= 0)
137
+ parent.children.splice(idx, 1);
138
+ },
139
+ /**
140
+ * @param {HostContainer} container
141
+ * @returns {void}
142
+ */
143
+ removeChildFromContainer(container) {
144
+ container.root = null;
145
+ },
146
+ /**
147
+ * @param {MutableHostElement} parent
148
+ * @param {HostNode} child
149
+ * @param {HostNode} beforeChild
150
+ * @returns {void}
151
+ */
152
+ insertBefore(parent, child, beforeChild) {
153
+ const idx = parent.children.indexOf(beforeChild);
154
+ if (idx >= 0)
155
+ parent.children.splice(idx, 0, child);
156
+ else
157
+ parent.children.push(child);
158
+ },
159
+ /**
160
+ * @param {HostContainer} container
161
+ * @param {HostNode} child
162
+ * @param {HostNode} _beforeChild
163
+ * @returns {void}
164
+ */
165
+ insertInContainerBefore(container, child, _beforeChild) {
166
+ container.root = child;
167
+ },
168
+ /**
169
+ * @param {MutableHostElement} _instance
170
+ * @param {string} _type
171
+ * @param {unknown} oldProps
172
+ * @param {unknown} newProps
173
+ * @returns {unknown}
174
+ */
175
+ prepareUpdate(_instance, _type, oldProps, newProps) {
176
+ if (oldProps === newProps)
177
+ return null;
178
+ return newProps;
179
+ },
180
+ /**
181
+ * @param {MutableHostElement} instance
182
+ * @param {unknown[]} args
183
+ * @returns {void}
184
+ */
185
+ commitUpdate(instance, ...args) {
186
+ let nextProps = null;
187
+ const first = args[0];
188
+ const second = args[1];
189
+ if (typeof second === "string" && first && typeof first === "object") {
190
+ nextProps = first;
191
+ }
192
+ else if (typeof first === "string") {
193
+ const maybeNewProps = args[2];
194
+ if (maybeNewProps && typeof maybeNewProps === "object") {
195
+ nextProps = maybeNewProps;
196
+ }
197
+ }
198
+ else if (first && typeof first === "object") {
199
+ nextProps = first;
200
+ }
201
+ if (!nextProps)
202
+ return;
203
+ const next = createElement(instance.tag, nextProps);
204
+ instance.props = next.props;
205
+ instance.rawProps = next.rawProps;
206
+ },
207
+ /**
208
+ * @param {MutableHostText} textInstance
209
+ * @param {string} _oldText
210
+ * @param {string} newText
211
+ * @returns {void}
212
+ */
213
+ commitTextUpdate(textInstance, _oldText, newText) {
214
+ textInstance.text = newText;
215
+ },
216
+ /**
217
+ * @returns {boolean}
218
+ */
219
+ finalizeInitialChildren() {
220
+ return false;
221
+ },
222
+ /**
223
+ * @returns {null}
224
+ */
225
+ prepareForCommit() {
226
+ return null;
227
+ },
228
+ /**
229
+ * @returns {void}
230
+ */
231
+ resetAfterCommit() { },
232
+ /**
233
+ * @returns {boolean}
234
+ */
235
+ shouldSetTextContent() {
236
+ return false;
237
+ },
238
+ /**
239
+ * @param {HostContainer} container
240
+ * @returns {void}
241
+ */
242
+ clearContainer(container) {
243
+ container.root = null;
244
+ },
245
+ /**
246
+ * @returns {number}
247
+ */
248
+ getCurrentEventPriority() {
249
+ return 1;
250
+ },
251
+ /**
252
+ * @returns {boolean}
253
+ */
254
+ shouldAttemptEagerTransition() {
255
+ return false;
256
+ },
257
+ /**
258
+ * @returns {boolean}
259
+ */
260
+ maySuspendCommit() {
261
+ return false;
262
+ },
263
+ /**
264
+ * @returns {void}
265
+ */
266
+ preloadInstance() { },
267
+ /**
268
+ * @returns {void}
269
+ */
270
+ startSuspendingCommit() { },
271
+ /**
272
+ * @returns {void}
273
+ */
274
+ suspendInstance() { },
275
+ /**
276
+ * @returns {null}
277
+ */
278
+ waitForCommitToBeReady() {
279
+ return null;
280
+ },
281
+ /**
282
+ * @returns {void}
283
+ */
284
+ resetFormInstance() { },
285
+ /**
286
+ * @returns {void}
287
+ */
288
+ detachDeletedInstance() { },
289
+ /**
290
+ * @param {"error" | "warn" | "info" | "log"} type
291
+ * @param {unknown[]} args
292
+ * @returns {() => void}
293
+ */
294
+ bindToConsole(type, args) {
295
+ return () => {
296
+ const fn = console[type] ?? console.log;
297
+ fn(...args);
298
+ };
299
+ },
300
+ /**
301
+ * @returns {number}
302
+ */
303
+ getCurrentUpdatePriority() {
304
+ return currentUpdatePriority;
305
+ },
306
+ /**
307
+ * @param {number} priority
308
+ * @returns {void}
309
+ */
310
+ setCurrentUpdatePriority(priority) {
311
+ currentUpdatePriority = priority;
312
+ },
313
+ /**
314
+ * @returns {number}
315
+ */
316
+ resolveUpdatePriority() {
317
+ return currentUpdatePriority;
318
+ },
319
+ /**
320
+ * @param {(...args: unknown[]) => void} fn
321
+ * @param {number} [delay]
322
+ * @returns {ReturnType<typeof setTimeout>}
323
+ */
324
+ scheduleTimeout(fn, delay) {
325
+ return setTimeout(fn, delay ?? 0);
326
+ },
327
+ /**
328
+ * @param {() => void} fn
329
+ * @returns {void}
330
+ */
331
+ scheduleMicrotask(fn) {
332
+ queueMicrotask(fn);
333
+ },
334
+ /**
335
+ * @param {ReturnType<typeof setTimeout>} id
336
+ * @returns {void}
337
+ */
338
+ cancelTimeout(id) {
339
+ clearTimeout(id);
340
+ },
341
+ noTimeout: -1,
342
+ };
343
+ /** @type {ReconcilerInstance} */
344
+ const reconciler = /** @type {ReconcilerInstance} */ (Reconciler(hostConfig));
345
+ const hookHost = globalThis;
346
+ if (!hookHost.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
347
+ installRDTHook();
348
+ }
349
+ reconciler.injectIntoDevTools({
350
+ bundleType: typeof process !== "undefined" && process.env.NODE_ENV === "production" ? 0 : 1,
351
+ version: "0.0.0",
352
+ rendererPackageName: "@smithers-orchestrator/core-react",
353
+ findFiberByHostInstance: () => null,
354
+ });
355
+ export class SmithersRenderer {
356
+ /** @type {HostContainer} */
357
+ container;
358
+ /** @type {unknown} */
359
+ root;
360
+ /** @type {ExtractGraph | undefined} */
361
+ extractGraph;
362
+ /**
363
+ * @param {SmithersRendererOptions} [options]
364
+ */
365
+ constructor(options = {}) {
366
+ this.extractGraph = options.extractGraph;
367
+ this.container = { root: null };
368
+ this.root = reconciler.createContainer(this.container, 0, null, false, null, "", reconciler.defaultOnUncaughtError, reconciler.defaultOnCaughtError, reconciler.defaultOnRecoverableError, null);
369
+ }
370
+ /**
371
+ * @param {React.ReactElement} element
372
+ * @param {ExtractOptions} [opts]
373
+ * @returns {Promise<WorkflowGraph>}
374
+ */
375
+ async render(element, opts) {
376
+ reconciler.updateContainerSync(element, this.root, null, () => { });
377
+ reconciler.flushSyncWork();
378
+ const extractGraph = this.extractGraph ?? (await resolveExtractGraph());
379
+ return extractGraph(this.container.root, opts);
380
+ }
381
+ /**
382
+ * @returns {HostNode | null}
383
+ */
384
+ getRoot() {
385
+ return this.container.root;
386
+ }
387
+ }