mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-12-23 10:11:57 +00:00
feat(renderer): implement very basic reconciler
This commit is contained in:
parent
74fbb4f364
commit
3a319cf333
6 changed files with 178 additions and 60 deletions
2
build.rs
2
build.rs
|
|
@ -2,7 +2,7 @@ fn main() {
|
|||
println!("cargo::rerun-if-changed=renderer/src");
|
||||
|
||||
let status = std::process::Command::new("bun")
|
||||
.args(["run", "build"])
|
||||
.args(["run", "dev"])
|
||||
.current_dir("renderer")
|
||||
.status()
|
||||
.expect("Failed to build renderer code");
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@
|
|||
"name": "renderer",
|
||||
"dependencies": {
|
||||
"react": "^19.2.0",
|
||||
"react-reconciler": "^0.33.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@raycast/api": "^1.103.5",
|
||||
"@types/bun": "latest",
|
||||
"@types/react-reconciler": "^0.32.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
|
|
@ -116,6 +118,8 @@
|
|||
|
||||
"@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="],
|
||||
|
||||
"@types/react-reconciler": ["@types/react-reconciler@0.32.2", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-gjcm6O0aUknhYaogEl8t5pecPfiOTD8VQkbjOhgbZas/E6qGY+veW9iuJU/7p4Y1E0EuQ0mArga7VEOUWSlVRA=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
|
@ -194,8 +198,12 @@
|
|||
|
||||
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
|
||||
|
||||
"react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
|
|
|||
|
|
@ -5,16 +5,18 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"watch": "bun build src/index.ts --target=node --minify --watch --outdir=dist",
|
||||
"build": "bun build src/index.ts --target=node --minify --outdir=dist"
|
||||
"dev": "bun build src/index.ts --target=node --outdir=dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@raycast/api": "^1.103.5",
|
||||
"@types/bun": "latest"
|
||||
"@types/bun": "latest",
|
||||
"@types/react-reconciler": "^0.32.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.0"
|
||||
"react": "^19.2.0",
|
||||
"react-reconciler": "^0.33.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import React from "react";
|
||||
import ReactJsxRuntime from "react/jsx-runtime";
|
||||
import type * as RaycastApiType from "@raycast/api";
|
||||
import { updateContainer } from "./reconciler";
|
||||
|
||||
const LaunchType = {
|
||||
UserInitiated: "userInitiated",
|
||||
|
|
@ -43,4 +45,4 @@ const raycastApi = {
|
|||
},
|
||||
};
|
||||
|
||||
export { React, raycastApi };
|
||||
export { React, ReactJsxRuntime, raycastApi, updateContainer };
|
||||
|
|
|
|||
91
renderer/src/reconciler.ts
Normal file
91
renderer/src/reconciler.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import Reconciler from "react-reconciler";
|
||||
|
||||
type HostComponent = {
|
||||
type: string;
|
||||
children: HostComponent[];
|
||||
};
|
||||
|
||||
type Type = string;
|
||||
interface TextInstance extends HostComponent {
|
||||
type: "TEXT";
|
||||
text: string;
|
||||
}
|
||||
type HostContext = object;
|
||||
type Instance = HostComponent;
|
||||
type ChildSet = Array<string | Instance>;
|
||||
type Timeout = ReturnType<typeof setTimeout>;
|
||||
|
||||
type RootContainer = {
|
||||
id: "root";
|
||||
children: ChildSet;
|
||||
};
|
||||
|
||||
const notImpl = () => {
|
||||
throw new Error("Function not implemented.");
|
||||
};
|
||||
|
||||
const HostConfig: Reconciler.HostConfig<
|
||||
Type,
|
||||
unknown, // Props
|
||||
RootContainer,
|
||||
Instance,
|
||||
TextInstance,
|
||||
void, // SuspenseInstance
|
||||
void, // HydratableInstance
|
||||
void, // FormInstance
|
||||
Instance, // PublicInstance
|
||||
HostContext,
|
||||
ChildSet,
|
||||
Timeout, // TimeoutHandle
|
||||
-1, // NoTimeout
|
||||
void // TransitionStatus
|
||||
> = {
|
||||
supportsPersistence: true,
|
||||
supportsMutation: false,
|
||||
resolveUpdatePriority: () => 1,
|
||||
getCurrentUpdatePriority: () => 1,
|
||||
setCurrentUpdatePriority: () => {},
|
||||
resolveEventTimeStamp: () => -1.1,
|
||||
resolveEventType: () => null,
|
||||
trackSchedulerEvent: () => {},
|
||||
getRootHostContext: () => ({}),
|
||||
createInstance(type, props, rootContainer, hostContext, internalHandle) {
|
||||
console.log("createInstance", type, props);
|
||||
return {
|
||||
type: type,
|
||||
children: [],
|
||||
};
|
||||
},
|
||||
prepareForCommit: () => null,
|
||||
resetAfterCommit: () => null,
|
||||
};
|
||||
|
||||
const reconciler = Reconciler(HostConfig);
|
||||
|
||||
const root: RootContainer = { id: "root", children: [] };
|
||||
|
||||
const container = reconciler.createContainer(
|
||||
root,
|
||||
0, // LegacyRoot
|
||||
null, // hydrationCallbacks
|
||||
false, // isStrictMode
|
||||
null, // concurrentUpdatesByDefaultOverride
|
||||
|
||||
"", // identifierPrefix
|
||||
console.log, // onUncaughtError
|
||||
console.log, // onCaughtError
|
||||
console.log, // onRecoverableError
|
||||
() => {}, // onDefaultTransitionIndicator
|
||||
null
|
||||
);
|
||||
|
||||
export const updateContainer = (
|
||||
element: React.ReactElement,
|
||||
callback?: () => void
|
||||
) => {
|
||||
reconciler.updateContainer(element, container, null, callback);
|
||||
};
|
||||
|
||||
export const batchedUpdates = (callback: () => void) => {
|
||||
reconciler.batchedUpdates(callback, null);
|
||||
};
|
||||
125
src/main.rs
125
src/main.rs
|
|
@ -1,9 +1,9 @@
|
|||
use iced::alignment::Vertical;
|
||||
use iced::futures;
|
||||
use iced::futures::channel::mpsc;
|
||||
use iced::futures::{SinkExt, StreamExt};
|
||||
use iced::widget::{button, column, container, row, text};
|
||||
use iced::{Color, Element, Font, Length, Padding, Theme, border};
|
||||
use iced::{Subscription, futures};
|
||||
use iced::widget::{column, container, text};
|
||||
use iced::{Color, Element, Font, Length, Subscription, Theme};
|
||||
use rustyscript::deno_core::PollEventLoopOptions;
|
||||
use rustyscript::{Module, Runtime, RuntimeOptions, serde_json::Value};
|
||||
use std::sync::Mutex;
|
||||
|
||||
|
|
@ -70,71 +70,86 @@ fn subscription(_state: &State) -> Subscription<Message> {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), rustyscript::Error> {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (sender, receiver) = mpsc::unbounded();
|
||||
*SENDER.lock().unwrap() = Some(sender);
|
||||
*RECEIVER.lock().unwrap() = Some(receiver);
|
||||
|
||||
let mut runtime = Runtime::new(RuntimeOptions {
|
||||
..Default::default()
|
||||
})?;
|
||||
std::thread::spawn(|| {
|
||||
let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
|
||||
|
||||
let renderer_module = Module::new("renderer.js", include_str!("../renderer/dist/index.js"));
|
||||
runtime.load_module(&renderer_module)?;
|
||||
let renderer_module = Module::new("renderer.js", include_str!("../renderer/dist/index.js"));
|
||||
runtime.load_module(&renderer_module).unwrap();
|
||||
|
||||
let module = Module::new(
|
||||
"setup.js",
|
||||
"
|
||||
import { createRequire } from 'module';
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
|
||||
import { raycastApi } from './renderer.js';
|
||||
|
||||
globalThis.require = (moduleName) => {
|
||||
if (moduleName === '@raycast/api') {
|
||||
return raycastApi;
|
||||
}
|
||||
return nodeRequire(moduleName);
|
||||
};
|
||||
|
||||
globalThis.module = { exports: {} };
|
||||
",
|
||||
);
|
||||
|
||||
let module2 = Module::new("plugin.js", include_str!("../test/plugin.js"));
|
||||
|
||||
let command_runner = Module::new(
|
||||
"runner.js",
|
||||
r#"
|
||||
await module.exports.default();
|
||||
"#,
|
||||
);
|
||||
|
||||
runtime.register_async_function("showToast", |args| {
|
||||
Box::pin(async move {
|
||||
if let Ok(value) = serde_json::from_value::<ToastOptions>(args[0].clone()) {
|
||||
if let Some(mut sender) = SENDER.lock().unwrap().clone() {
|
||||
sender
|
||||
.send(Message::UpdateToast(value.title.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
let module = Module::new(
|
||||
"setup.js",
|
||||
"
|
||||
import { createRequire } from 'module';
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
|
||||
import { raycastApi, React, ReactJsxRuntime } from './renderer.js';
|
||||
|
||||
globalThis.require = (moduleName) => {
|
||||
if (moduleName === '@raycast/api') {
|
||||
return raycastApi;
|
||||
}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
})
|
||||
})?;
|
||||
|
||||
if (moduleName === 'react') return React;
|
||||
if (moduleName === 'react/jsx-runtime') return ReactJsxRuntime;
|
||||
|
||||
return nodeRequire(moduleName);
|
||||
};
|
||||
|
||||
globalThis.module = { exports: {} };
|
||||
",
|
||||
);
|
||||
runtime.load_module(&module).unwrap();
|
||||
|
||||
runtime.load_module(&module)?;
|
||||
runtime.load_module(&module2)?;
|
||||
let module2 = Module::new("plugin.js", include_str!("../test/plugin.js"));
|
||||
runtime.load_module(&module2).unwrap();
|
||||
|
||||
let tokio_runtime = runtime.tokio_runtime();
|
||||
let command_runner = Module::new(
|
||||
"runner.js",
|
||||
r#"
|
||||
import { React, updateContainer } from './renderer.js';
|
||||
|
||||
const PluginRoot = module.exports.default;
|
||||
const AppElement = React.createElement(PluginRoot);
|
||||
updateContainer(AppElement, () => {
|
||||
console.log("initial render callback fired!");
|
||||
});
|
||||
"#,
|
||||
);
|
||||
|
||||
tokio_runtime.block_on(async { runtime.load_module_async(&command_runner).await })?;
|
||||
runtime
|
||||
.register_async_function("showToast", |args| {
|
||||
Box::pin(async move {
|
||||
if let Ok(value) = serde_json::from_value::<ToastOptions>(args[0].clone()) {
|
||||
if let Some(mut sender) = SENDER.lock().unwrap().clone() {
|
||||
sender
|
||||
.send(Message::UpdateToast(value.title.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
runtime.load_module(&command_runner).unwrap();
|
||||
|
||||
runtime
|
||||
.block_on_event_loop(PollEventLoopOptions::default(), None)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
iced::application("flare", update, view)
|
||||
.subscription(subscription)
|
||||
.font(include_bytes!("./assets/Inter.ttf").as_slice())
|
||||
.default_font(iced::Font::DEFAULT)
|
||||
.run()
|
||||
.map_err(|e| rustyscript::Error::Runtime(e.to_string()))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue