mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
* feat(plugins): build-less TS/TSX plugins, typings and authoring tools - Plugin frames transpile .ts/.tsx/.mts/.jsx on load (sucrase on a shared worker thread with a 2 s budget, cached in main), resolve relative imports to exact files, and refuse bare package imports by name. Components and html panel/editor entries may be .ts/.tsx. - orca plugin types writes typings for the components orca module, window.orca, worker activate(orca), host.call and events into .orca/types, from one host-typings generator. - orca plugin create --template components; templates omit publisher and target 1.4.246. - orca plugin release-entry, dev inspect and restart; validate:plugin checks named files. - Guest console errors reach orca plugin logs; dev workers restart on edits; the dev watcher ignores .git, .orca and node_modules; Git and folder installs cannot replace catalog installs. - examples/plugins/hello-tsx, docs, and the plugin-buildless-tsx e2e spec. * ci: run the Windows parity probes with one tsgo checker tsconfig.node peaks near 22 GB with tsgo's default four checkers (each holds its own types) and about 16 GB with one; the 16 GB Windows runner paged the native probe past its 900 s timeout once this branch's package.json change triggered the job. One checker gives the same diagnostics (verified locally with --parity-only); the timing benchmark is unchanged.
988 lines
30 KiB
JavaScript
988 lines
30 KiB
JavaScript
// Generated by config/scripts/build-plugin-declarative-runtime.mjs from
|
|
// src/shared/plugins/declarative/runtime/. Do not edit.
|
|
|
|
// src/shared/plugins/declarative/runtime/declarative-atoms.ts
|
|
var ATOM = /* @__PURE__ */ Symbol.for("orca.declarative.atom");
|
|
var activeTracker = null;
|
|
var ATOM_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
var keyed = /* @__PURE__ */ new Map();
|
|
var snapshot = {
|
|
plugin: /* @__PURE__ */ new Map(),
|
|
workspace: /* @__PURE__ */ new Map()
|
|
};
|
|
var onPersistedChange = null;
|
|
function create(initial, fields = {}) {
|
|
return Object.freeze({
|
|
[ATOM]: {
|
|
value: initial,
|
|
initial,
|
|
listeners: /* @__PURE__ */ new Set(),
|
|
readonly: false,
|
|
persist: "memory",
|
|
version: 1,
|
|
...fields
|
|
}
|
|
});
|
|
}
|
|
function stored(inner) {
|
|
if (!inner.key || inner.persist === "memory") {
|
|
return { found: false };
|
|
}
|
|
const entry = snapshot[inner.persist].get(inner.key);
|
|
if (!entry) {
|
|
return { found: false };
|
|
}
|
|
if (entry.version === inner.version) {
|
|
return { found: true, value: entry.value };
|
|
}
|
|
return inner.migrate ? { found: true, value: inner.migrate(entry.value, entry.version) } : { found: false };
|
|
}
|
|
function atom(...args) {
|
|
if (args.length === 1) {
|
|
return create(args[0]);
|
|
}
|
|
const [key, initial, options = {}] = args;
|
|
if (typeof key !== "string" || !ATOM_KEY.test(key)) {
|
|
throw new TypeError("an atom key is 1-128 letters, digits, or . _ : -");
|
|
}
|
|
if (keyed.has(key)) {
|
|
throw new Error(`atom key "${key}" is already used`);
|
|
}
|
|
const persist = options.persist ?? "memory";
|
|
if (persist !== "memory" && persist !== "plugin" && persist !== "workspace") {
|
|
throw new TypeError('persist is "memory", "plugin" or "workspace"');
|
|
}
|
|
const result = create(initial, {
|
|
key,
|
|
persist,
|
|
version: options.version ?? 1,
|
|
migrate: options.migrate
|
|
});
|
|
const inner = result[ATOM];
|
|
const hydrated = stored(inner);
|
|
if (hydrated.found) {
|
|
inner.value = hydrated.value;
|
|
}
|
|
keyed.set(key, result);
|
|
return result;
|
|
}
|
|
function derived(compute) {
|
|
if (typeof compute !== "function") {
|
|
throw new TypeError("derived() takes a function");
|
|
}
|
|
return create(compute(), { readonly: true, compute });
|
|
}
|
|
function hostAtom(initial) {
|
|
const result = create(initial, { readonly: true });
|
|
return { atom: result, set: (value) => assign(result[ATOM], value, false) };
|
|
}
|
|
function state(target) {
|
|
const inner = typeof target === "object" && target !== null ? target[ATOM] : void 0;
|
|
if (!inner) {
|
|
throw new TypeError("expected an atom created by atom()");
|
|
}
|
|
return inner;
|
|
}
|
|
function read(target) {
|
|
const inner = state(target);
|
|
if (inner.compute) {
|
|
return inner.compute();
|
|
}
|
|
activeTracker?.(target);
|
|
return inner.value;
|
|
}
|
|
function assign(inner, value, persist) {
|
|
if (Object.is(value, inner.value)) {
|
|
return;
|
|
}
|
|
inner.value = value;
|
|
for (const listener of inner.listeners) {
|
|
listener();
|
|
}
|
|
if (persist && inner.key && inner.persist !== "memory") {
|
|
const target = keyed.get(inner.key);
|
|
if (target) {
|
|
onPersistedChange?.(target);
|
|
}
|
|
}
|
|
}
|
|
function writable(target, verb) {
|
|
if (activeTracker) {
|
|
throw new Error(`${verb}() was called while rendering; call it from an event handler instead`);
|
|
}
|
|
const inner = state(target);
|
|
if (inner.readonly) {
|
|
throw new TypeError(`${verb}() cannot change a read-only atom`);
|
|
}
|
|
return inner;
|
|
}
|
|
function set(target, value) {
|
|
assign(writable(target, "set"), value, true);
|
|
}
|
|
function update(target, updater) {
|
|
const inner = writable(target, "update");
|
|
if (typeof updater !== "function") {
|
|
throw new TypeError("update() takes an updater function; use set() to store a value");
|
|
}
|
|
assign(inner, updater(inner.value), true);
|
|
}
|
|
function subscribeAtom(target, listener) {
|
|
const inner = state(target);
|
|
inner.listeners.add(listener);
|
|
return () => inner.listeners.delete(listener);
|
|
}
|
|
function trackAtomReads(reads, render) {
|
|
const previous = activeTracker;
|
|
activeTracker = (target) => reads.add(target);
|
|
try {
|
|
return render();
|
|
} finally {
|
|
activeTracker = previous;
|
|
}
|
|
}
|
|
function persistedEntry(target) {
|
|
const inner = state(target);
|
|
return inner.key && inner.persist !== "memory" ? { scope: inner.persist, key: inner.key, version: inner.version, value: inner.value } : null;
|
|
}
|
|
function watchPersistedAtoms(listener) {
|
|
onPersistedChange = listener;
|
|
}
|
|
function hydrateAtoms(scope, values) {
|
|
snapshot[scope] = new Map(values);
|
|
for (const target of keyed.values()) {
|
|
const inner = target[ATOM];
|
|
if (inner.persist !== scope) {
|
|
continue;
|
|
}
|
|
const hydrated = stored(inner);
|
|
assign(inner, hydrated.found ? hydrated.value : inner.initial, false);
|
|
}
|
|
}
|
|
|
|
// src/shared/plugins/declarative/plugin-declarative-message-names.ts
|
|
var PLUGIN_DECLARATIVE_MESSAGES = {
|
|
/** guest → host, `{ document, revision, tree }` */
|
|
render: "ui.render",
|
|
/** host → guest, `{ document, revision, callback, args, edit? }`: `revision` is the tree shown. */
|
|
event: "ui.event",
|
|
/** guest → host, `{ document, revision, message }`: the plugin threw; the last good tree stays. */
|
|
error: "ui.error",
|
|
/** host → guest, `{ document, revision, accepted, retryAfterMs? }`: whether a render is shown. */
|
|
ack: "ui.ack",
|
|
/** guest → host, `{ document }`: the runtime is up and waits for `ui.hydrate` to render. */
|
|
ready: "ui.ready",
|
|
/** host → guest, `{ panel?, visible?, plugin?, workspace? }`: persisted state and context. */
|
|
hydrate: "ui.hydrate",
|
|
/** host → guest, `{ visible }` */
|
|
visibility: "ui.visibility",
|
|
/** guest → host, `{ entries: [{ scope, key, version, value }] }`: persisted atoms changed. */
|
|
persist: "ui.persist"
|
|
};
|
|
var PLUGIN_DECLARATIVE_CATALOG_VERSION = 1;
|
|
var PLUGIN_DECLARATIVE_COMPONENT_PROPS = {
|
|
Stack: ["gap", "align"],
|
|
Row: ["gap", "align", "justify", "wrap"],
|
|
Text: ["tone", "muted", "size", "weight", "truncate"],
|
|
Heading: ["level"],
|
|
Button: ["variant", "tone", "size", "icon", "disabled", "onPress"],
|
|
IconButton: ["icon", "label", "variant", "tone", "size", "disabled", "onPress"],
|
|
Badge: ["tone"],
|
|
Card: ["title", "description"],
|
|
List: ["label"],
|
|
ListItem: ["title", "description", "icon", "current", "disabled", "onPress"],
|
|
Input: [
|
|
"label",
|
|
"labelHidden",
|
|
"value",
|
|
"defaultValue",
|
|
"placeholder",
|
|
"type",
|
|
"maxLength",
|
|
"invalid",
|
|
"disabled",
|
|
"onChange",
|
|
"onSubmit"
|
|
],
|
|
Switch: ["label", "checked", "defaultChecked", "disabled", "onChange"],
|
|
Select: [
|
|
"label",
|
|
"labelHidden",
|
|
"value",
|
|
"defaultValue",
|
|
"placeholder",
|
|
"options",
|
|
"disabled",
|
|
"onChange"
|
|
],
|
|
Separator: ["orientation"],
|
|
Progress: ["label", "labelHidden", "value"],
|
|
Spinner: ["label", "labelHidden"],
|
|
EmptyState: ["title", "description", "icon"],
|
|
Link: ["href", "onPress"],
|
|
Code: ["block"]
|
|
};
|
|
var PLUGIN_DECLARATIVE_VALUE_CONTROLS = [
|
|
"Input",
|
|
"Switch",
|
|
"Select"
|
|
];
|
|
var PLUGIN_DECLARATIVE_EDIT_PROP = "$edit";
|
|
function isReservedPluginDeclarativeEvent(name) {
|
|
return name.startsWith("ui.") || name.startsWith("editor.") || name === "ready" || name === "hello" || name === "ping" || name === "pong";
|
|
}
|
|
|
|
// src/shared/plugins/declarative/runtime/declarative-elements.ts
|
|
var ORCA_ELEMENT = /* @__PURE__ */ Symbol.for("orca.declarative.element");
|
|
var Fragment = /* @__PURE__ */ Symbol.for("orca.declarative.fragment");
|
|
function isOrcaElement(value) {
|
|
return typeof value === "object" && value !== null && "$$typeof" in value && value.$$typeof === ORCA_ELEMENT;
|
|
}
|
|
function createElement(type, props, key) {
|
|
return Object.freeze({
|
|
$$typeof: ORCA_ELEMENT,
|
|
type,
|
|
props,
|
|
key: key === void 0 || key === null ? void 0 : String(key)
|
|
});
|
|
}
|
|
function jsx(type, props, key) {
|
|
const { key: propKey, ...rest } = props ?? {};
|
|
return createElement(type, rest, key ?? propKey);
|
|
}
|
|
function h(type, props, ...children) {
|
|
const { key, ...rest } = props ?? {};
|
|
if (children.length > 0) {
|
|
rest.children = children.length === 1 ? children[0] : children;
|
|
}
|
|
return createElement(type, rest, key);
|
|
}
|
|
|
|
// src/shared/plugins/declarative/runtime/declarative-evaluate.ts
|
|
var MAX_EVALUATION_DEPTH = 512;
|
|
var MAX_PROP_DEPTH = 16;
|
|
var IGNORED_PROPS = /* @__PURE__ */ new Set([
|
|
"children",
|
|
"ref",
|
|
"key",
|
|
PLUGIN_DECLARATIVE_EDIT_PROP
|
|
]);
|
|
function isCallback(value) {
|
|
return typeof value === "function";
|
|
}
|
|
function describe(value) {
|
|
if (value === null || typeof value !== "object") {
|
|
return typeof value === "number" ? String(value) : typeof value;
|
|
}
|
|
return Object.getPrototypeOf(value)?.constructor?.name ?? "object";
|
|
}
|
|
function assertPlainJson(value, where, depth = 0) {
|
|
if (depth > MAX_PROP_DEPTH) {
|
|
throw new TypeError(`${where} is nested too deeply`);
|
|
}
|
|
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
return;
|
|
}
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
return;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
value.forEach((item, index) => assertPlainJson(item, `${where}[${index}]`, depth + 1));
|
|
return;
|
|
}
|
|
if (typeof value === "object") {
|
|
const prototype = Object.getPrototypeOf(value);
|
|
if (prototype === Object.prototype || prototype === null) {
|
|
for (const [name, item] of Object.entries(value)) {
|
|
assertPlainJson(item, `${where}.${name}`, depth + 1);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
throw new TypeError(`${where} must be plain JSON data, not ${describe(value)}`);
|
|
}
|
|
function assertUniqueKeys(children, parent) {
|
|
const seen = /* @__PURE__ */ new Set();
|
|
for (const child of children) {
|
|
if (typeof child !== "string") {
|
|
if (seen.has(child.key)) {
|
|
throw new Error(`duplicate key "${child.key.slice(0, 64)}" among the children of ${parent}`);
|
|
}
|
|
seen.add(child.key);
|
|
}
|
|
}
|
|
}
|
|
function hashId(text) {
|
|
let a = 2166136261;
|
|
let b = 16777619;
|
|
for (let index = 0; index < text.length; index++) {
|
|
const code = text.charCodeAt(index);
|
|
a = Math.imul(a ^ code, 16777619);
|
|
b = Math.imul(b ^ code, 1540483477);
|
|
}
|
|
return `c${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
|
|
}
|
|
function evaluateOrcaTree(component, editsSeen = /* @__PURE__ */ new Map()) {
|
|
const valueControls = new Set(PLUGIN_DECLARATIVE_VALUE_CONTROLS);
|
|
const callbacks = /* @__PURE__ */ new Map();
|
|
function callbackId(path, prop) {
|
|
const base = hashId(`${path}|${prop}`);
|
|
let id = base;
|
|
for (let attempt = 1; callbacks.has(id); attempt++) {
|
|
id = `${base}.${attempt}`;
|
|
}
|
|
return id;
|
|
}
|
|
function hostNode(type, props, key, parentPath, depth) {
|
|
if (!/^[A-Z]/.test(type)) {
|
|
throw new TypeError(
|
|
`<${type.slice(0, 64)}> is not an Orca component; panels never render DOM`
|
|
);
|
|
}
|
|
const path = `${parentPath}/${key}`;
|
|
const wireProps = {};
|
|
for (const [name, value] of Object.entries(props)) {
|
|
if (IGNORED_PROPS.has(name) || value === void 0) {
|
|
continue;
|
|
}
|
|
if (isCallback(value)) {
|
|
const id = callbackId(path, name);
|
|
callbacks.set(id, value);
|
|
wireProps[name] = { $callback: id };
|
|
} else {
|
|
assertPlainJson(value, `<${type}> prop "${name}"`);
|
|
wireProps[name] = value;
|
|
}
|
|
}
|
|
const onChange = wireProps.onChange;
|
|
if (valueControls.has(type) && typeof onChange === "object" && onChange !== null) {
|
|
const id = "$callback" in onChange ? onChange.$callback : void 0;
|
|
wireProps[PLUGIN_DECLARATIVE_EDIT_PROP] = typeof id === "string" ? editsSeen.get(id) ?? 0 : 0;
|
|
}
|
|
const children = [];
|
|
const list = Array.isArray(props.children) ? props.children : [props.children];
|
|
list.forEach((child, index) => emit(child, `#${index}`, "", children, path, depth + 1));
|
|
assertUniqueKeys(children, `<${type}>`);
|
|
return { type, key, props: wireProps, children };
|
|
}
|
|
function emit(value, slot, scope, out, parentPath, depth) {
|
|
if (depth > MAX_EVALUATION_DEPTH) {
|
|
throw new Error(`component tree is deeper than ${MAX_EVALUATION_DEPTH}`);
|
|
}
|
|
if (value === null || value === void 0 || typeof value === "boolean") {
|
|
return;
|
|
}
|
|
if (typeof value === "string" || typeof value === "number") {
|
|
out.push(String(value));
|
|
return;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
value.forEach(
|
|
(item, index) => emit(item, `${slot}.${index}`, `${slot}.`, out, parentPath, depth + 1)
|
|
);
|
|
return;
|
|
}
|
|
if (!isOrcaElement(value)) {
|
|
throw new TypeError(`a ${describe(value)} is not a valid child; render a component or text`);
|
|
}
|
|
const { type, props } = value;
|
|
const key = value.key === void 0 ? slot : `${scope}${value.key}`;
|
|
if (type === Fragment) {
|
|
emit(props.children, key, `${key}.`, out, parentPath, depth + 1);
|
|
} else if (typeof type === "function") {
|
|
emit(type(props), key, `${key}:`, out, parentPath, depth + 1);
|
|
} else if (typeof type === "string") {
|
|
out.push(hostNode(type, props, key, parentPath, depth));
|
|
} else {
|
|
throw new TypeError('invalid element type; import components from "orca"');
|
|
}
|
|
}
|
|
const root = [];
|
|
emit(component({}), "#0", "", root, "", 0);
|
|
const nodes = root.map(
|
|
(child, index) => typeof child === "string" ? { type: "Text", key: `#text${index}`, props: {}, children: [child] } : child
|
|
);
|
|
assertUniqueKeys(nodes, "the panel");
|
|
const tree = nodes.length <= 1 ? nodes[0] ?? null : { type: "Stack", key: "#root", props: {}, children: nodes };
|
|
return { tree, callbacks };
|
|
}
|
|
|
|
// src/shared/plugins/declarative/runtime/declarative-persist-queue.ts
|
|
var PERSIST_DEBOUNCE_MS = 400;
|
|
function createPersistQueue(options) {
|
|
const pending = /* @__PURE__ */ new Set();
|
|
let timer = null;
|
|
function flush() {
|
|
if (timer !== null) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
const entries = [];
|
|
for (const target of pending) {
|
|
const entry = persistedEntry(target);
|
|
if (!entry) {
|
|
continue;
|
|
}
|
|
try {
|
|
assertPlainJson(entry.value, `persisted atom "${entry.key}"`);
|
|
entries.push(entry);
|
|
} catch (error) {
|
|
options.report(error);
|
|
}
|
|
}
|
|
pending.clear();
|
|
if (entries.length > 0) {
|
|
options.post(entries);
|
|
}
|
|
}
|
|
return {
|
|
add(target) {
|
|
pending.add(target);
|
|
timer ??= setTimeout(flush, PERSIST_DEBOUNCE_MS);
|
|
},
|
|
flush
|
|
};
|
|
}
|
|
|
|
// src/shared/plugins/declarative/runtime/declarative-host-messages.ts
|
|
function readHostMessage(payload) {
|
|
if (typeof payload !== "object" || payload === null) {
|
|
return {};
|
|
}
|
|
const panel = "panel" in payload ? payload.panel : void 0;
|
|
return {
|
|
document: "document" in payload ? payload.document : void 0,
|
|
revision: "revision" in payload ? payload.revision : void 0,
|
|
callback: "callback" in payload ? payload.callback : void 0,
|
|
args: "args" in payload ? payload.args : void 0,
|
|
edit: "edit" in payload ? payload.edit : void 0,
|
|
accepted: "accepted" in payload ? payload.accepted : void 0,
|
|
retryAfterMs: "retryAfterMs" in payload ? payload.retryAfterMs : void 0,
|
|
colorScheme: "colorScheme" in payload ? payload.colorScheme : void 0,
|
|
visible: "visible" in payload ? payload.visible : void 0,
|
|
panelId: typeof panel === "object" && panel !== null && "id" in panel ? panel.id : void 0,
|
|
plugin: "plugin" in payload ? payload.plugin : void 0,
|
|
workspace: "workspace" in payload ? payload.workspace : void 0
|
|
};
|
|
}
|
|
function readStoredValues(raw) {
|
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
return null;
|
|
}
|
|
const values = /* @__PURE__ */ new Map();
|
|
for (const [key, entry] of Object.entries(raw)) {
|
|
if (typeof entry !== "object" || entry === null) {
|
|
continue;
|
|
}
|
|
const version = "version" in entry ? entry.version : void 0;
|
|
if (typeof version === "number" && "value" in entry) {
|
|
values.set(key, { version, value: entry.value });
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
// src/shared/plugins/declarative/runtime/declarative-panel-context.ts
|
|
function createPanelContext() {
|
|
const visibility = hostAtom(true);
|
|
const lifetime = new AbortController();
|
|
const context = {
|
|
subscriptions: [],
|
|
signal: lifetime.signal,
|
|
visible: visibility.atom,
|
|
panel: { id: "" }
|
|
};
|
|
let hydratedNow = false;
|
|
let resolve = () => void 0;
|
|
const hydrated = new Promise((done) => {
|
|
resolve = () => {
|
|
hydratedNow = true;
|
|
done();
|
|
};
|
|
});
|
|
function setVisibility(payload) {
|
|
const { visible } = readHostMessage(payload);
|
|
if (typeof visible === "boolean") {
|
|
visibility.set(visible);
|
|
}
|
|
}
|
|
return {
|
|
context,
|
|
hydrated,
|
|
isHydrated: () => hydratedNow,
|
|
markHydrated: () => resolve(),
|
|
setVisibility,
|
|
/** `ui.hydrate`: on boot, and again with the new workspace's values after a switch. */
|
|
hydrate(payload) {
|
|
const message = readHostMessage(payload);
|
|
if (typeof message.panelId === "string") {
|
|
context.panel = { id: message.panelId };
|
|
}
|
|
setVisibility(payload);
|
|
for (const scope of ["plugin", "workspace"]) {
|
|
const values = readStoredValues(message[scope]);
|
|
if (values) {
|
|
hydrateAtoms(scope, values);
|
|
}
|
|
}
|
|
resolve();
|
|
},
|
|
abort: () => lifetime.abort()
|
|
};
|
|
}
|
|
|
|
// src/shared/plugins/declarative/runtime/declarative-renderer.ts
|
|
var MIN_RENDER_INTERVAL_MS = 16;
|
|
var MAX_RETAINED_RENDERS = 32;
|
|
var HYDRATION_TIMEOUT_MS = 3e3;
|
|
function errorMessage(error) {
|
|
return (error instanceof Error ? error.message : String(error)).slice(0, 4096);
|
|
}
|
|
function defaultSchedule(flush, delayMs) {
|
|
if (delayMs > 0) {
|
|
setTimeout(flush, delayMs);
|
|
} else {
|
|
queueMicrotask(flush);
|
|
}
|
|
}
|
|
function newDocumentId(now) {
|
|
return `${now.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
}
|
|
function isCallable(value) {
|
|
return typeof value === "function";
|
|
}
|
|
function createOrcaDeclarativeRuntime(options) {
|
|
const { post } = options;
|
|
const bridge = options.bridge ?? null;
|
|
const schedule = options.schedule ?? defaultSchedule;
|
|
const now = options.now ?? (() => Date.now());
|
|
const document2 = newDocumentId(now());
|
|
const theme = hostAtom({ colorScheme: options.colorScheme ?? "dark" });
|
|
const panel = createPanelContext();
|
|
const { context } = panel;
|
|
let component = null;
|
|
let revision = 0;
|
|
let lastRenderAt = Number.NEGATIVE_INFINITY;
|
|
let renders = /* @__PURE__ */ new Map();
|
|
let accepted = null;
|
|
const editsSeen = /* @__PURE__ */ new Map();
|
|
let subscriptions = /* @__PURE__ */ new Map();
|
|
let scheduled = false;
|
|
let setupCleanup = null;
|
|
let disposed = false;
|
|
function invalidate(delayMs = 0) {
|
|
if (scheduled || !component) {
|
|
return;
|
|
}
|
|
scheduled = true;
|
|
const wait = Math.max(delayMs, lastRenderAt + MIN_RENDER_INTERVAL_MS - now());
|
|
schedule(() => {
|
|
if (scheduled) {
|
|
render();
|
|
}
|
|
}, wait);
|
|
}
|
|
function resubscribe(reads) {
|
|
const next = /* @__PURE__ */ new Map();
|
|
for (const target of reads) {
|
|
next.set(target, subscriptions.get(target) ?? subscribeAtom(target, () => invalidate()));
|
|
}
|
|
for (const [target, unsubscribe] of subscriptions) {
|
|
if (!next.has(target)) {
|
|
unsubscribe();
|
|
}
|
|
}
|
|
subscriptions = next;
|
|
}
|
|
function report(error) {
|
|
console.error(error instanceof Error && error.stack ? error.stack : String(error));
|
|
post(PLUGIN_DECLARATIVE_MESSAGES.error, { document: document2, revision, message: errorMessage(error) });
|
|
}
|
|
function retain(at, callbacks) {
|
|
renders.set(at, callbacks);
|
|
for (const kept of renders.keys()) {
|
|
if (renders.size <= MAX_RETAINED_RENDERS) {
|
|
break;
|
|
}
|
|
if (kept !== accepted) {
|
|
renders.delete(kept);
|
|
}
|
|
}
|
|
}
|
|
function render() {
|
|
scheduled = false;
|
|
if (!component || disposed) {
|
|
return;
|
|
}
|
|
lastRenderAt = now();
|
|
const reads = /* @__PURE__ */ new Set();
|
|
const mounted = component;
|
|
let evaluation;
|
|
try {
|
|
evaluation = trackAtomReads(reads, () => evaluateOrcaTree(mounted, editsSeen));
|
|
} catch (error) {
|
|
resubscribe(/* @__PURE__ */ new Set([...subscriptions.keys(), ...reads]));
|
|
report(error);
|
|
return;
|
|
}
|
|
revision += 1;
|
|
retain(revision, evaluation.callbacks);
|
|
resubscribe(reads);
|
|
post(PLUGIN_DECLARATIVE_MESSAGES.render, { document: document2, revision, tree: evaluation.tree });
|
|
}
|
|
function dispatchEvent(payload) {
|
|
const message = readHostMessage(payload);
|
|
const { callback, args, edit } = message;
|
|
if (message.document !== document2 || typeof callback !== "string") {
|
|
return false;
|
|
}
|
|
const handler = typeof message.revision === "number" ? renders.get(message.revision)?.get(callback) : void 0;
|
|
if (!handler) {
|
|
return false;
|
|
}
|
|
if (typeof edit === "number" && Number.isSafeInteger(edit)) {
|
|
editsSeen.set(callback, Math.max(edit, editsSeen.get(callback) ?? 0));
|
|
invalidate();
|
|
}
|
|
try {
|
|
const result = handler(...Array.isArray(args) ? args : []);
|
|
if (result instanceof Promise) {
|
|
result.catch(report);
|
|
}
|
|
} catch (error) {
|
|
report(error);
|
|
}
|
|
return true;
|
|
}
|
|
function acknowledge(payload) {
|
|
const message = readHostMessage(payload);
|
|
const acked = message.revision;
|
|
if (message.document !== document2 || typeof acked !== "number" || !renders.has(acked)) {
|
|
return;
|
|
}
|
|
if (message.accepted === true) {
|
|
accepted = acked;
|
|
for (const kept of renders.keys()) {
|
|
if (kept < acked) {
|
|
renders.delete(kept);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
renders.delete(acked);
|
|
const retryAfterMs = message.retryAfterMs;
|
|
if (typeof retryAfterMs === "number" && acked === revision) {
|
|
invalidate(retryAfterMs);
|
|
}
|
|
}
|
|
const persistQueue = createPersistQueue({
|
|
report,
|
|
post: (entries) => post(PLUGIN_DECLARATIVE_MESSAGES.persist, { document: document2, entries })
|
|
});
|
|
const flushPersist = persistQueue.flush;
|
|
watchPersistedAtoms(persistQueue.add);
|
|
const bridgeSubscriptions = bridge?.on ? [
|
|
bridge.on(PLUGIN_DECLARATIVE_MESSAGES.event, dispatchEvent),
|
|
bridge.on(PLUGIN_DECLARATIVE_MESSAGES.ack, acknowledge),
|
|
bridge.on(PLUGIN_DECLARATIVE_MESSAGES.hydrate, panel.hydrate),
|
|
bridge.on(PLUGIN_DECLARATIVE_MESSAGES.visibility, panel.setVisibility),
|
|
bridge.on("theme", (payload) => {
|
|
const { colorScheme } = readHostMessage(payload);
|
|
if ((colorScheme === "light" || colorScheme === "dark") && read(theme.atom).colorScheme !== colorScheme) {
|
|
theme.set({ colorScheme });
|
|
}
|
|
})
|
|
] : [];
|
|
if (options.awaitHydration) {
|
|
post(PLUGIN_DECLARATIVE_MESSAGES.ready, { document: document2 });
|
|
setTimeout(panel.markHydrated, HYDRATION_TIMEOUT_MS);
|
|
} else {
|
|
panel.markHydrated();
|
|
}
|
|
function unsubscribeAll() {
|
|
for (const unsubscribe of subscriptions.values()) {
|
|
unsubscribe();
|
|
}
|
|
subscriptions = /* @__PURE__ */ new Map();
|
|
}
|
|
const runtime2 = {
|
|
document: document2,
|
|
async boot(module) {
|
|
await panel.hydrated;
|
|
if (isCallable(module.setup)) {
|
|
const cleanup = module.setup(context);
|
|
setupCleanup = isCallable(cleanup) ? () => void cleanup() : null;
|
|
}
|
|
const panelComponent = module.default;
|
|
if (isCallable(panelComponent)) {
|
|
runtime2.mount((props) => panelComponent(props));
|
|
} else if (!component) {
|
|
const kind = module.default === null ? "null" : typeof module.default;
|
|
throw new Error(
|
|
`The panel module must export a component function as its default export (got ${kind})`
|
|
);
|
|
}
|
|
},
|
|
mount(next) {
|
|
component = next;
|
|
if (panel.isHydrated()) {
|
|
render();
|
|
return;
|
|
}
|
|
void panel.hydrated.then(() => {
|
|
if (component === next) {
|
|
render();
|
|
}
|
|
});
|
|
},
|
|
unmount() {
|
|
component = null;
|
|
scheduled = false;
|
|
unsubscribeAll();
|
|
renders = /* @__PURE__ */ new Map();
|
|
accepted = null;
|
|
revision += 1;
|
|
post(PLUGIN_DECLARATIVE_MESSAGES.render, { document: document2, revision, tree: null });
|
|
},
|
|
mounted: () => component !== null,
|
|
dispatchEvent,
|
|
acknowledge,
|
|
hydrate: panel.hydrate,
|
|
flush() {
|
|
if (scheduled) {
|
|
render();
|
|
}
|
|
flushPersist();
|
|
},
|
|
themeAtom: theme.atom,
|
|
bridge,
|
|
dispose() {
|
|
if (disposed) {
|
|
return;
|
|
}
|
|
flushPersist();
|
|
disposed = true;
|
|
component = null;
|
|
scheduled = false;
|
|
unsubscribeAll();
|
|
watchPersistedAtoms(null);
|
|
for (const subscription of [...bridgeSubscriptions, ...context.subscriptions]) {
|
|
try {
|
|
subscription.dispose();
|
|
} catch (error) {
|
|
report(error);
|
|
}
|
|
}
|
|
try {
|
|
setupCleanup?.();
|
|
} catch (error) {
|
|
report(error);
|
|
}
|
|
panel.abort();
|
|
}
|
|
};
|
|
return runtime2;
|
|
}
|
|
|
|
// src/shared/plugins/declarative/runtime/declarative-sdk.ts
|
|
function hostError(code, message) {
|
|
return Object.assign(new Error(message), { code, retryable: code === "timeout" });
|
|
}
|
|
function unavailable(what) {
|
|
return Promise.reject(hostError("unavailable", `${what} is not available in this Orca`));
|
|
}
|
|
function isComponentTag(name) {
|
|
return Object.hasOwn(PLUGIN_DECLARATIVE_COMPONENT_PROPS, name);
|
|
}
|
|
function createOrcaSdk(source) {
|
|
const host2 = Object.freeze({
|
|
call(method, params, options = {}) {
|
|
const bridgeHost = source.bridge()?.host;
|
|
if (!bridgeHost) {
|
|
return unavailable("host.call");
|
|
}
|
|
const { signal, timeoutMs } = options;
|
|
if (timeoutMs === void 0) {
|
|
return bridgeHost.call(method, params, signal ? { signal } : void 0);
|
|
}
|
|
const controller = new AbortController();
|
|
const forward = () => controller.abort();
|
|
if (signal?.aborted) {
|
|
controller.abort();
|
|
}
|
|
signal?.addEventListener("abort", forward, { once: true });
|
|
let timedOut = false;
|
|
const timer = setTimeout(() => {
|
|
timedOut = true;
|
|
controller.abort();
|
|
}, timeoutMs);
|
|
return bridgeHost.call(method, params, { signal: controller.signal }).catch((error) => {
|
|
throw timedOut ? hostError("timeout", `${method} took longer than ${timeoutMs} ms`) : error;
|
|
}).finally(() => {
|
|
clearTimeout(timer);
|
|
signal?.removeEventListener("abort", forward);
|
|
});
|
|
},
|
|
has(method) {
|
|
return source.bridge()?.host?.has?.(method) === true;
|
|
}
|
|
});
|
|
const api2 = Object.freeze({
|
|
get major() {
|
|
return source.bridge()?.api?.major ?? 1;
|
|
},
|
|
get minor() {
|
|
return source.bridge()?.api?.minor ?? 0;
|
|
}
|
|
});
|
|
const env2 = Object.freeze({
|
|
/** Orca's UI language (BCP-47); the guest's `navigator.language` follows the OS instead. */
|
|
get locale() {
|
|
return source.bridge()?.locale ?? "en";
|
|
},
|
|
/** Reading it while rendering re-renders on light/dark switches. */
|
|
get theme() {
|
|
return source.theme();
|
|
},
|
|
client: "desktop"
|
|
});
|
|
const shell2 = Object.freeze({
|
|
openExternal(url) {
|
|
return host2.call("shell.openExternal", { url: String(url) });
|
|
}
|
|
});
|
|
const files2 = Object.freeze({
|
|
saveAs(options) {
|
|
const { data } = options;
|
|
const bytes = ArrayBuffer.isView(data) ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : data;
|
|
return host2.call("files.saveAs", { ...options, data: bytes });
|
|
}
|
|
});
|
|
function on2(event, handler) {
|
|
if (typeof event !== "string" || isReservedPluginDeclarativeEvent(event)) {
|
|
throw new TypeError(`"${String(event)}" is not an event plugins can subscribe to`);
|
|
}
|
|
return source.bridge()?.on?.(event, handler) ?? { dispose: () => void 0 };
|
|
}
|
|
const ui2 = Object.freeze({
|
|
/** Grows with the component catalog. */
|
|
catalog: PLUGIN_DECLARATIVE_CATALOG_VERSION,
|
|
/** Whether this Orca draws `component`, and accepts `prop` on it when given. */
|
|
has(component, prop) {
|
|
if (!isComponentTag(component)) {
|
|
return false;
|
|
}
|
|
const props = PLUGIN_DECLARATIVE_COMPONENT_PROPS[component];
|
|
return prop === void 0 || props.includes(prop);
|
|
}
|
|
});
|
|
return { host: host2, api: api2, env: env2, shell: shell2, files: files2, on: on2, ui: ui2 };
|
|
}
|
|
|
|
// src/shared/plugins/declarative/runtime/orca-module.ts
|
|
var runtime = null;
|
|
var pendingMount = null;
|
|
function isOrcaBridge(value) {
|
|
return typeof value === "object" && value !== null && "on" in value && typeof value.on === "function";
|
|
}
|
|
function globalBridge() {
|
|
const scope = globalThis;
|
|
const candidate = "orca" in scope ? scope.orca : void 0;
|
|
return isOrcaBridge(candidate) ? candidate : null;
|
|
}
|
|
function documentColorScheme() {
|
|
const root = typeof document === "undefined" ? null : document.documentElement;
|
|
if (!root) {
|
|
return void 0;
|
|
}
|
|
return root.classList.contains("light") ? "light" : "dark";
|
|
}
|
|
function installOrcaDeclarativeRuntime(options) {
|
|
runtime?.dispose();
|
|
const installed = createOrcaDeclarativeRuntime({
|
|
bridge: globalBridge(),
|
|
colorScheme: documentColorScheme(),
|
|
...options
|
|
});
|
|
runtime = installed;
|
|
if (typeof addEventListener === "function") {
|
|
addEventListener("pagehide", () => installed.dispose(), { once: true });
|
|
}
|
|
const queued = pendingMount;
|
|
pendingMount = null;
|
|
if (queued) {
|
|
installed.mount(queued);
|
|
}
|
|
return installed;
|
|
}
|
|
function mount(component) {
|
|
if (runtime) {
|
|
runtime.mount(component);
|
|
} else {
|
|
pendingMount = component;
|
|
}
|
|
}
|
|
var sdk = createOrcaSdk({
|
|
bridge: () => runtime?.bridge ?? globalBridge(),
|
|
theme: () => runtime ? read(runtime.themeAtom) : { colorScheme: documentColorScheme() ?? "dark" }
|
|
});
|
|
var { host, api, env, shell, files, on, ui } = sdk;
|
|
var Stack = "Stack";
|
|
var Row = "Row";
|
|
var Text = "Text";
|
|
var Heading = "Heading";
|
|
var Button = "Button";
|
|
var IconButton = "IconButton";
|
|
var Badge = "Badge";
|
|
var Card = "Card";
|
|
var List = "List";
|
|
var ListItem = "ListItem";
|
|
var Input = "Input";
|
|
var Switch = "Switch";
|
|
var Select = "Select";
|
|
var Separator = "Separator";
|
|
var Progress = "Progress";
|
|
var Spinner = "Spinner";
|
|
var EmptyState = "EmptyState";
|
|
var Link = "Link";
|
|
var Code = "Code";
|
|
export {
|
|
Badge,
|
|
Button,
|
|
Card,
|
|
Code,
|
|
EmptyState,
|
|
Fragment,
|
|
Heading,
|
|
IconButton,
|
|
Input,
|
|
Link,
|
|
List,
|
|
ListItem,
|
|
Progress,
|
|
Row,
|
|
Select,
|
|
Separator,
|
|
Spinner,
|
|
Stack,
|
|
Switch,
|
|
Text,
|
|
api,
|
|
atom,
|
|
derived,
|
|
env,
|
|
files,
|
|
h,
|
|
host,
|
|
installOrcaDeclarativeRuntime,
|
|
jsx,
|
|
jsx as jsxDEV,
|
|
jsx as jsxs,
|
|
mount,
|
|
on,
|
|
read,
|
|
set,
|
|
shell,
|
|
ui,
|
|
update
|
|
};
|