@public-tauri/raycast-convert 1.0.1 → 1.1.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.
@@ -0,0 +1,240 @@
1
+
2
+ /**
3
+ * Generated into converted plugins as `.raycast-build/raycast-worker-runtime.ts`.
4
+ * React custom renderer: Raycast List/Detail/Action UI runs in the Node Worker; snapshots go to the wujie view client.
5
+ *
6
+ * 由 `server.ts` 引入;插件业务代码通过 `@raycast/api`(别名到 `@public-tauri/api/raycast`)导入 UI 与 API。
7
+ */
8
+ import React from 'react';
9
+ import Reconciler from 'react-reconciler';
10
+ import { DefaultEventPriority } from 'react-reconciler/constants.js';
11
+ import type { RaycastViewSnapshot } from './raycast-view-protocol';
12
+ import type { JsonPatchOp } from './json-patch';
13
+ import { generateJsonPatch } from './json-patch';
14
+ import { __getRaycastContext, __setRaycastContext } from '@public-tauri/api/raycast';
15
+ import type {
16
+ HostElementInstance,
17
+ HostInstance,
18
+ HostRootContainer,
19
+ HostTextInstance,
20
+ } from './host-instance';
21
+ import { buildSnapshotFromHostRoot } from './virtual-serialize';
22
+
23
+ let viewLaunchProps: Record<string, unknown> = {};
24
+
25
+ export function __setRaycastViewContext(context: {
26
+ pluginName?: string;
27
+ commandName?: string;
28
+ preferences?: Record<string, unknown>;
29
+ supportPath?: string;
30
+ assetsPath?: string;
31
+ launchProps?: Record<string, unknown>;
32
+ }) {
33
+ viewLaunchProps = context.launchProps || {};
34
+ __setRaycastContext({
35
+ pluginName: context.pluginName,
36
+ commandName: context.commandName,
37
+ commandMode: 'view',
38
+ preferences: context.preferences,
39
+ supportPath: context.supportPath,
40
+ assetsPath: context.assetsPath,
41
+ });
42
+ }
43
+
44
+ const noop = () => {};
45
+
46
+ const swallowError = (_error: Error, _info: unknown) => {};
47
+
48
+ export function createRaycastViewSession(options: {
49
+ emitSnapshot: (snapshot: RaycastViewSnapshot) => void;
50
+ emitPatch: (patches: JsonPatchOp[]) => void;
51
+ }) {
52
+ const handlers = new Map<string, (...args: unknown[]) => void | Promise<void>>();
53
+ let hostIdSeq = 0;
54
+ const nextHostId = () => {
55
+ hostIdSeq += 1;
56
+ return `rv:n:${hostIdSeq}`;
57
+ };
58
+
59
+ let latestSnapshot: RaycastViewSnapshot | null = null;
60
+ let sentInitialSnapshot = false;
61
+ const rootNode: HostRootContainer = { children: [] };
62
+ let snapshotQueued = false;
63
+
64
+ const serializeRoot = (): RaycastViewSnapshot => buildSnapshotFromHostRoot(rootNode.children, handlers, {
65
+ commandName: __getRaycastContext().commandName || '',
66
+ });
67
+
68
+ const emitSnapshot = () => {
69
+ const nextSnapshot = serializeRoot();
70
+
71
+ if (!sentInitialSnapshot || !latestSnapshot) {
72
+ latestSnapshot = nextSnapshot;
73
+ sentInitialSnapshot = true;
74
+ options.emitSnapshot(latestSnapshot);
75
+ return;
76
+ }
77
+
78
+ const patches = generateJsonPatch(latestSnapshot, nextSnapshot);
79
+ latestSnapshot = nextSnapshot;
80
+ if (patches.length > 0) {
81
+ options.emitPatch(patches);
82
+ }
83
+ };
84
+
85
+ const scheduleSnapshotAfterCommit = () => {
86
+ if (snapshotQueued) return;
87
+ snapshotQueued = true;
88
+ queueMicrotask(() => {
89
+ snapshotQueued = false;
90
+ emitSnapshot();
91
+ reconciler.flushSyncWork?.();
92
+ });
93
+ };
94
+
95
+ const hostConfig = {
96
+ getRootHostContext: () => ({}),
97
+ prepareForCommit: () => null,
98
+ preparePortalMount: () => null,
99
+ clearContainer: () => false,
100
+ resetAfterCommit: () => {
101
+ scheduleSnapshotAfterCommit();
102
+ },
103
+ getChildHostContext: () => ({}),
104
+ shouldSetTextContent: () => false,
105
+ createInstance: (type: string, props: Record<string, unknown>): HostElementInstance => {
106
+ console.log('createInstance', type, props);
107
+ return {
108
+ type,
109
+ hostId: nextHostId(),
110
+ props,
111
+ parent: null,
112
+ children: [],
113
+ };
114
+ },
115
+ createTextInstance: (text: string): HostTextInstance => ({
116
+ type: 'text',
117
+ hostId: nextHostId(),
118
+ text,
119
+ parent: null,
120
+ }),
121
+ resetTextContent: noop,
122
+ hideTextInstance: noop,
123
+ unhideTextInstance: noop,
124
+ getPublicInstance: (instance: HostInstance) => instance,
125
+ hideInstance: noop,
126
+ unhideInstance: noop,
127
+ appendInitialChild: (parent: HostElementInstance | HostRootContainer, child: HostInstance) => {
128
+ child.parent = parent;
129
+ parent.children.push(child);
130
+ },
131
+ appendChild: (parent: HostElementInstance | HostRootContainer, child: HostInstance) => {
132
+ child.parent = parent;
133
+ parent.children.push(child);
134
+ },
135
+ insertBefore: (parent: HostElementInstance | HostRootContainer, child: HostInstance, beforeChild: HostInstance) => {
136
+ child.parent = parent;
137
+ parent.children = parent.children.filter(item => item !== child);
138
+ parent.children.splice(parent.children.indexOf(beforeChild), 0, child);
139
+ },
140
+ finalizeInitialChildren: () => false,
141
+ isPrimaryRenderer: true,
142
+ supportsMutation: true,
143
+ supportsPersistence: false,
144
+ supportsHydration: false,
145
+ scheduleTimeout: setTimeout,
146
+ cancelTimeout: clearTimeout,
147
+ noTimeout: -1,
148
+ getCurrentEventPriority: () => DefaultEventPriority,
149
+ getCurrentUpdatePriority: () => DefaultEventPriority,
150
+ setCurrentUpdatePriority: noop,
151
+ resolveUpdatePriority: () => DefaultEventPriority,
152
+ shouldAttemptEagerTransition: () => false,
153
+ maySuspendCommit: () => false,
154
+ preloadInstance: () => true,
155
+ startSuspendingCommit: noop,
156
+ suspendInstance: noop,
157
+ waitForCommitToBeReady: () => noop,
158
+ mayResourceSuspendCommit: () => false,
159
+ beforeActiveInstanceBlur: noop,
160
+ afterActiveInstanceBlur: noop,
161
+ detachDeletedInstance: noop,
162
+ getInstanceFromNode: () => null,
163
+ prepareScopeUpdate: noop,
164
+ getInstanceFromScope: () => null,
165
+ appendChildToContainer: (container: HostRootContainer, child: HostInstance) => {
166
+ child.parent = container;
167
+ container.children.push(child);
168
+ },
169
+ insertInContainerBefore: (container: HostRootContainer, child: HostInstance, beforeChild: HostInstance) => {
170
+ child.parent = container;
171
+ container.children = container.children.filter(item => item !== child);
172
+ container.children.splice(container.children.indexOf(beforeChild), 0, child);
173
+ },
174
+ removeChildFromContainer: (container: HostRootContainer, child: HostInstance) => {
175
+ container.children = container.children.filter(item => item !== child);
176
+ child.parent = null;
177
+ },
178
+ prepareUpdate: () => true,
179
+ commitMount: noop,
180
+ // React 19 / reconciler ≥0.31:无 updatePayload,签名为 (instance, type, prevProps, nextProps, internalHandle)
181
+ commitUpdate: (instance: HostElementInstance, _type: unknown, _prevProps: unknown, nextProps: Record<string, unknown>) => {
182
+ instance.props = nextProps;
183
+ },
184
+ commitTextUpdate: (textInstance: HostTextInstance, _oldText: string, newText: string) => {
185
+ textInstance.text = newText;
186
+ },
187
+ removeChild: (parent: HostElementInstance, child: HostInstance) => {
188
+ parent.children = parent.children.filter(item => item !== child);
189
+ child.parent = null;
190
+ },
191
+ };
192
+
193
+ // HostConfig 随 react-reconciler 版本扩展字段;变异宿主只需最小子集(见官方 custom renderer 示例)。
194
+ const reconciler = Reconciler(hostConfig as any);
195
+
196
+ let rootContainer: ReturnType<typeof reconciler.createContainer> | null = null;
197
+
198
+ const mount = async (Command: React.ComponentType<Record<string, unknown>>) => {
199
+ hostIdSeq = 0;
200
+ rootContainer = reconciler.createContainer(
201
+ rootNode,
202
+ 0,
203
+ null,
204
+ false,
205
+ null,
206
+ '',
207
+ swallowError,
208
+ swallowError,
209
+ swallowError,
210
+ noop,
211
+ );
212
+ reconciler.updateContainer(
213
+ React.createElement(Command, viewLaunchProps),
214
+ rootContainer,
215
+ null,
216
+ noop,
217
+ );
218
+ reconciler.flushSyncWork?.();
219
+ };
220
+
221
+ const dispatchHostEvent = async (hostId: string, event = 'onAction', args: unknown[] = []) => {
222
+ const handler = handlers.get(`${hostId}:${event}`);
223
+ if (!handler) throw new Error(`Unknown Raycast host event: ${hostId} · ${event}`);
224
+ await handler(...args);
225
+ reconciler.flushSyncWork?.();
226
+ };
227
+
228
+ const unmount = () => {
229
+ if (rootContainer) {
230
+ reconciler.updateContainer(null, rootContainer, null, noop);
231
+ rootContainer = null;
232
+ }
233
+ hostIdSeq = 0;
234
+ latestSnapshot = null;
235
+ sentInitialSnapshot = false;
236
+ handlers.clear();
237
+ };
238
+
239
+ return { mount, dispatchHostEvent, unmount };
240
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * 将 HostInstance 树序列化为可 JSON 的快照(raycast-view-protocol)。
3
+ *
4
+ * - 元素节点来自 reconciler,自带 hostId;props 中的 React slot(detail / actions)在序列化时展开并分配 rv:p:* 合成 id。
5
+ * - 函数型 props:写入 Worker Map[`${hostId}:${propName}`],快照中序列化为 \`${RAYCAST_SERIALIZED_FUNC_PREFIX}\${propName}\`(JSON 字符串)。
6
+ */
7
+ import React from 'react';
8
+ import {
9
+ RAYCAST_SERIALIZED_FUNC_PREFIX,
10
+ type RaycastViewSnapshot,
11
+ type SerializedHostNode,
12
+ } from './raycast-view-protocol';
13
+ import type { HostInstance } from './host-instance';
14
+ import { isHostText } from './host-instance';
15
+
16
+ export type HostEventHandlerRegistry = Map<string, (...args: unknown[]) => void | Promise<void>>;
17
+
18
+ const SKIP_PROP_KEYS = new Set(['key', 'ref', '__self', '__source']);
19
+
20
+ function cloneJsonSafe(value: unknown): unknown {
21
+ if (value === null || value === undefined) return value;
22
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
23
+ if (typeof value === 'symbol' || typeof value === 'function') return undefined;
24
+ if (Array.isArray(value)) {
25
+ const arr = value.map(cloneJsonSafe).filter(v => v !== undefined);
26
+ return arr;
27
+ }
28
+ if (typeof value === 'object') {
29
+ try {
30
+ return JSON.parse(JSON.stringify(value));
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ function isHostTextLike(value: unknown): value is { hostId: string; type: 'text'; text: string } {
39
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
40
+ const obj = value as Record<string, unknown>;
41
+ return obj.type === 'text' && typeof obj.hostId === 'string' && typeof obj.text === 'string';
42
+ }
43
+
44
+ function isHostElementLike(value: unknown): value is {
45
+ hostId: string;
46
+ type: string;
47
+ props: Record<string, unknown>;
48
+ children: unknown[];
49
+ } {
50
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
51
+ const obj = value as Record<string, unknown>;
52
+ return (
53
+ typeof obj.hostId === 'string'
54
+ && typeof obj.type === 'string'
55
+ && obj.type !== 'text'
56
+ && typeof obj.props === 'object'
57
+ && obj.props !== null
58
+ && !Array.isArray(obj.props)
59
+ && Array.isArray(obj.children)
60
+ );
61
+ }
62
+
63
+ function isHostInstanceLike(value: unknown): value is HostInstance {
64
+ return isHostTextLike(value) || isHostElementLike(value);
65
+ }
66
+
67
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
68
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
69
+ const proto = Object.getPrototypeOf(value);
70
+ return proto === Object.prototype || proto === null;
71
+ }
72
+
73
+ function isReactElementLike(value: unknown): value is { $$typeof: unknown } {
74
+ return Boolean(value && typeof value === 'object' && '$$typeof' in (value as Record<string, unknown>));
75
+ }
76
+
77
+ function serializeReactElementTree(
78
+ value: unknown,
79
+ handlers: HostEventHandlerRegistry,
80
+ nextSlotId: () => string,
81
+ ): SerializedHostNode | undefined {
82
+ if (!React.isValidElement(value)) return undefined;
83
+ const el = value as React.ReactElement<Record<string, unknown>>;
84
+ const { type } = el;
85
+
86
+ if (typeof type === 'function') {
87
+ const maybeClassComponent = Boolean((type as { prototype?: { isReactComponent?: unknown } }).prototype?.isReactComponent);
88
+ if (maybeClassComponent) return undefined;
89
+ try {
90
+ const rendered = (type as React.FC<Record<string, unknown>>)(el.props ?? {});
91
+ return serializeReactElementTree(rendered, handlers, nextSlotId);
92
+ } catch {
93
+ return undefined;
94
+ }
95
+ }
96
+
97
+ if (typeof type === 'symbol') {
98
+ const children = React.Children.toArray(el.props?.children as React.ReactNode)
99
+ .map(ch => serializeReactElementTree(ch, handlers, nextSlotId))
100
+ .filter((x): x is SerializedHostNode => x !== undefined);
101
+ return children[0];
102
+ }
103
+
104
+ if (typeof type !== 'string') return undefined;
105
+
106
+ const hostId = nextSlotId();
107
+ const propsOut = serializeHostProps(el.props ?? {}, hostId, handlers, nextSlotId);
108
+ const children = React.Children.toArray(el.props?.children as React.ReactNode)
109
+ .map(ch => serializeReactElementTree(ch, handlers, nextSlotId))
110
+ .filter((x): x is SerializedHostNode => x !== undefined);
111
+
112
+ return {
113
+ hostId,
114
+ type,
115
+ props: propsOut,
116
+ children,
117
+ };
118
+ }
119
+
120
+ function serializeUnknownPropValue(
121
+ value: unknown,
122
+ handlers: HostEventHandlerRegistry,
123
+ nextSlotId: () => string,
124
+ seen: WeakSet<object>,
125
+ ): unknown {
126
+ if (value === undefined) return undefined;
127
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
128
+ return value;
129
+ }
130
+ if (isHostInstanceLike(value)) {
131
+ return serializeHostSubtree(value, handlers, nextSlotId);
132
+ }
133
+ if (Array.isArray(value)) {
134
+ return value
135
+ .map(item => serializeUnknownPropValue(item, handlers, nextSlotId, seen))
136
+ .filter(item => item !== undefined);
137
+ }
138
+ if (typeof value === 'object') {
139
+ if (seen.has(value)) return undefined;
140
+ seen.add(value);
141
+
142
+ if (isReactElementLike(value)) {
143
+ return serializeReactElementTree(value, handlers, nextSlotId);
144
+ }
145
+
146
+ if (!isPlainRecord(value)) {
147
+ return cloneJsonSafe(value);
148
+ }
149
+
150
+ const obj = value;
151
+ const out: Record<string, unknown> = {};
152
+ for (const [k, v] of Object.entries(obj)) {
153
+ if (typeof v === 'function') continue;
154
+ const serialized = serializeUnknownPropValue(v, handlers, nextSlotId, seen);
155
+ if (serialized !== undefined) out[k] = serialized;
156
+ }
157
+ return out;
158
+ }
159
+ return cloneJsonSafe(value);
160
+ }
161
+
162
+ function serializeHostProps(
163
+ props: Record<string, unknown>,
164
+ hostId: string,
165
+ handlers: HostEventHandlerRegistry,
166
+ nextSlotId: () => string,
167
+ ): Record<string, unknown> {
168
+ const out: Record<string, unknown> = {};
169
+ for (const [k, v] of Object.entries(props)) {
170
+ if (SKIP_PROP_KEYS.has(k) || k === 'children') continue;
171
+ if (typeof v === 'function') {
172
+ handlers.set(`${hostId}:${k}`, v as (...args: unknown[]) => void | Promise<void>);
173
+ out[k] = `${RAYCAST_SERIALIZED_FUNC_PREFIX}${k}`;
174
+ continue;
175
+ }
176
+ const c = serializeUnknownPropValue(v, handlers, nextSlotId, new WeakSet<object>());
177
+ if (c !== undefined) out[k] = c;
178
+ }
179
+ return out;
180
+ }
181
+
182
+ function serializeHostSubtree(
183
+ node: HostInstance | undefined,
184
+ handlers: HostEventHandlerRegistry,
185
+ nextSlotId: () => string,
186
+ ): SerializedHostNode {
187
+ if (!node) {
188
+ return { hostId: 'rv:root:empty', type: 'raycast:empty', props: {}, children: [] };
189
+ }
190
+ if (isHostText(node)) {
191
+ return { hostId: node.hostId, type: 'text', text: node.text };
192
+ }
193
+ const propsOut = serializeHostProps(node.props, node.hostId, handlers, nextSlotId);
194
+ const children = node.children.map(ch => serializeHostSubtree(ch, handlers, nextSlotId));
195
+ return { hostId: node.hostId, type: node.type, props: propsOut, children };
196
+ }
197
+
198
+ export function buildSnapshotFromHostRoot(
199
+ rootChildren: HostInstance[],
200
+ handlers: HostEventHandlerRegistry,
201
+ meta: { commandName: string },
202
+ ): RaycastViewSnapshot {
203
+ handlers.clear();
204
+ let slotSeq = 0;
205
+ const nextSlotId = () => {
206
+ slotSeq += 1;
207
+ return `rv:p:${slotSeq}`;
208
+ };
209
+ console.log('rootChildren', rootChildren);
210
+ const root = serializeHostSubtree(rootChildren[0], handlers, nextSlotId);
211
+ return {
212
+ commandName: meta.commandName,
213
+ root,
214
+ };
215
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "resolveJsonModule": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "esModuleInterop": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src/**/*.ts", "tsdown.config.ts"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }