docs: explain application entry points and tooling workflows

This commit is contained in:
2026-11-12 19:31:28 +08:00
parent ca5bc863c1
commit 8a61d27938
8 changed files with 28 additions and 5 deletions
+3 -3
View File
@@ -1,10 +1,10 @@
function heading(value: Text): View { // 辅助函数把字符串包装成大字号 TextBlock;返回值仍是托管视觉引用。
function heading(value: Text): View { // 辅助函数把字符串包装成大字号 TextBlock;返回值仍是托管视觉引用。调用方为下面的 main,参数 value 是该次调用的 Text,不是可写全局状态。
return ui.text(value, 2, "#242422") // 这里只创建文字对象,具体宽高由实际字体和出现位置约束测量。
}
function main(): View { // 兼容模块入口 main 返回视觉根,宿主负责把它显示到页面,不需要源代码显式 render。
function main(): View { // 兼容模块入口 main 返回视觉根,宿主负责把它显示到页面,不需要源代码显式 render。endor_host 的模块执行路径在定义加载完成后调用这个零参数入口。
let title = heading("Hello Endor") // 调用上述辅助函数,局部变量持有同一托管文字对象。
let rectangle = ui.rect(240, 80, 12, "#DCE8FF") // 矩形使用逻辑尺寸,圆角与颜色由原生载荷保存。
let detail = ui.row(0, 12, ui.text("Aster VM"), ui.spacer(8), ui.text("Tessera UI")) // Row 依次摆放文字、固定间隔和文字;共享几何仍由每次出现位置承担。
return ui.column(24, 12, title, rectangle, detail) // Column 的 24px 内边距和 12px 间隔决定最终命令位置;返回后发布快照拥有所需数据。
return ui.column(24, 12, title, rectangle, detail) // Column 的 24px 内边距和 12px 间隔决定最终命令位置;返回后宿主提取原生发布快照;Column 在 VM 中继续按托管引用拥有这些子对象。
}
+2
View File
@@ -1,8 +1,10 @@
// 与 page.endor 等价的 main 返回根入口:用于对比顶层显式 render 和兼容模块入口的绘图结果。
// 返回 Column 视觉对象,宿主选择/挂载页面;这里的 title 和 rectangle 只在 VM 内建立业务引用。
// endor_script 将本文件作为完整模块解析,endor_host 调用零参数 main 后把返回的视觉根交给兼容展示路径。
function main(): View {
// 文字内容与矩形参数和 page 示例一致,最终绘图命令应等价。
let heading = ui.text("Hello Endor", 2, "#242422")
// 两次构造返回独立托管业务对象;此处变量只是对象引用,出现位置在挂载到页面后由布局生成。
let rectangle = ui.rect(240, 80, 12, "#DCE8FF")
// 布局边距与间隔保持一致;不同 VM 的对象/Mount 身份独立,命令相同不表示共享对象。
return ui.column(24, 12, heading, rectangle)
+2 -2
View File
@@ -1,4 +1,4 @@
let heading = ui.text("Hello Endor", 2, "#242422") // 顶层脚本不需要 main:先创建文字对象;字号 2 和十六进制颜色传给原生文字构造器。
let heading = ui.text("Hello Endor", 2, "#242422") // 顶层脚本不需要 main:先创建文字对象;字号 2 和十六进制颜色传给原生文字构造器。endor_host 的脚本执行路径按顶层语句顺序运行本文件。
let rectangle = ui.rect(240, 80, 12, "#DCE8FF") // 创建固定逻辑尺寸矩形;12 为圆角半径,业务对象尚未出现在任何页面中。
let myui = ui.page() // 创建托管 Page 资源;它负责持有展示根和页面生命周期。
myui.render(ui.column(24, 12, heading, rectangle)) // 把文字和矩形放入 Column,padding=24、gap=12;render 选择此页并生成出现位置及发布帧。
myui.render(ui.column(24, 12, heading, rectangle)) // 把文字和矩形放入 Column,padding=24、gap=12;render 请求展示此页,宿主刷新时再完成出现位置、测量、安排与绘制;源方法调用结束不等于 OS 已呈现像素。
+5
View File
@@ -2,6 +2,7 @@
# macOS 应用打包入口:构建 release 可执行文件并组装最小 Tessera.app;脚本本身不运行测试或部署。
# 执行会产生构建产物,应在已确认的 macOS 构建环境中调用。
# 任一命令失败或使用未定义变量即退出,避免在失败编译后继续打包旧文件。
# 构建调用方显式执行此脚本才产生文件;阅读或 sh -n 仅检查语法,不会运行下方 Cargo。
set -eu
# 此入口只能在 macOS 使用;构建授权由调用方确认,脚本不自动执行测试或部署。
if [ "$(uname -s)" != Darwin ]; then
@@ -11,17 +12,21 @@ fi
# 以脚本所在目录的父目录作为仓库根;清空这一次 cd 的 CDPATH,防止用户 shell 配置改变目录解析。
cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
# 锁定依赖并生成 release 可执行文件;附加参数原样交给 Cargo。
# 这里确实触发编译,不是只验证清单;依赖锁不匹配时 --locked 要求失败而非自动更新 Cargo.lock。
cargo build --release --locked "$@"
# 从 Cargo 包元数据读取首个 version 值,空值由 test 拒绝,不生成缺版本的 bundle。
# version 从清单文本读取,后面用于两个 plist 版本键;脚本不调用构建后的 Tessera 来查询版本。
version=$(awk -F '"' '$1 ~ /^version[[:space:]]*=/ { print $2; exit }' Cargo.toml)
test -n "$version"
# macOS bundle 约定:Contents/MacOS 保存入口二进制,Contents/Info.plist 保存系统识别信息。
bundle="$(pwd)/target/release/Tessera.app"
# 创建/复用固定 release 产物目录,不清空旧 bundle;后面逐文件替换主二进制和元数据。
mkdir -p "$bundle/Contents/MacOS"
# 先写临时文件再重命名,避免复制途中将半个二进制留作 bundle 的主程序。
cp target/release/tessera "$bundle/Contents/MacOS/tessera.new"
mv -f "$bundle/Contents/MacOS/tessera.new" "$bundle/Contents/MacOS/tessera"
# 生成最小 macOS bundle 元数据;应用名、入口与高分辨率标志供系统启动/显示使用。
# heredoc 展开本次 version,XML 文本与入口文件名相互对应;这里没有签名、公证或安装动作。
cat > "$bundle/Contents/Info.plist.new" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+5
View File
@@ -6,6 +6,7 @@ import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
// 文档维护者从命令行执行本脚本;运行时入口不导入它,依赖应事先安装,本脚本不执行包管理器。
const tools = process.env.UML_RENDER_TOOLS;
// 工具依赖放在独立目录,避免给应用运行依赖增加文档渲染包。
if (!tools) throw new Error('Set UML_RENDER_TOOLS to the documentation tool directory; see docs/architecture/uml/README.md');
@@ -67,6 +68,7 @@ document.createElementNS = (namespace, tag) => {
document.body = element('body');
document.documentElement.appendChild(document.body);
globalThis.document = document;
// 全局 DOM/Canvas 适配已设置完毕才加载引擎,避免模块初始化读取到缺失的浏览器接口。
const { renderToString } = await import(pathToFileURL(require.resolve('@plantuml/core')));
// 仓库位置由脚本 URL 确定,与调用者工作目录无关;输出目录不存在时创建。
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -80,6 +82,7 @@ for (const filename of (await readdir(source)).filter(name => name.endsWith('.pu
// 将引擎 resolve/reject 回调适配为 Promise,逐图等待;渲染失败直接终止,不覆盖该图产物。
const rendered = await new Promise((resolve, reject) => renderToString(text.split('\n'), resolve, reject));
const xml = new DOMParser().parseFromString(rendered, 'image/svg+xml');
// 引擎有时以正常 SVG 输出错误页,所以不能只判断 Promise resolve;还要检查文字层的错误标签。
const labels = Array.from(xml.getElementsByTagName('text'), node => node.textContent).join('\n');
if (xml.documentElement.tagName !== 'svg' || /syntax error|error line|assumed diagram type|an error has occurr/i.test(labels)) {
throw new Error(`${filename}: renderer returned an error diagram\n${labels}`);
@@ -88,9 +91,11 @@ for (const filename of (await readdir(source)).filter(name => name.endsWith('.pu
const paper = xml.createElementNS('http://www.w3.org/2000/svg', 'rect');
for (const [key, value] of Object.entries({ x: '0', y: '0', width: '100%', height: '100%', fill: '#FFFFFF' })) paper.setAttribute(key, value);
xml.documentElement.insertBefore(paper, xml.documentElement.firstChild);
// 序列化修改后的 DOM,而不是直接写引擎原字符串,确保白底矩形进入两个导出格式。
const svg = new XMLSerializer().serializeToString(xml);
// 同时保存可缩放 SVG 和通用 PNG;写完读取 PNG 元数据确认生成尺寸。
const stem = filename.slice(0, -5);
// 两种产物顺序写入且不组成事务;SVG 成功后若 PNG 失败,前面已写出的文件不会自动回滚。
await writeFile(path.join(output, `${stem}.svg`), svg);
// 以 192 DPI 光栅化为 PNG 供通用预览;SVG 同时保留矢量文字和缩放能力。
const png = sharp(Buffer.from(svg), { density: 192 });
+3
View File
@@ -1,7 +1,10 @@
//! 语言桥接模块入口。集中导出 Endor 类型和宿主实现之间的适配层。
//! endor_host 建立会话时安装 ui_bridge 能力,源程序随后通过名义方法调用宿主;渲染线程不直接运行这些回调。
//!
// 目录确定模块归属;main 中的 crate 根别名保持内部调用路径稳定。
/// 注册托管类型/宿主方法,验证参数并连接业务对象、Mount、视觉协议与窗口/任务资源。
pub(crate) mod ui_bridge;
/// 注册图片、字体、文字流与避让形状能力,并把托管绘制参数解码为可跨线程使用的不可变资源。
/// ui_bridge 安装时调用注册入口,source_paint 解码 image 命令时复用图像检查。
pub(crate) mod graphics;
+6
View File
@@ -3,12 +3,14 @@
//!
//! Application composition root. Directory modules own implementation; these
//! crate-private aliases keep existing internal paths stable during reorganization.
// 以下目录模块只声明编译归属,真正启动服务和创建窗口发生在 main 的顺序调用中。
mod bridge;
mod model;
mod render;
mod runtime;
mod shell;
// crate 内旧路径通过私有重导出访问目录实现,不为 GUI 与 runtime 额外建立另一套对象或注册表。
pub(crate) use bridge::ui_bridge;
// 模型层负责从 VM 场景对象提取快照,渲染层 scene3d 负责原生计算;别名区分两者。
pub(crate) use model::scene3d as scene3d_model;
@@ -36,17 +38,21 @@ use document::NamespaceState;
use winit::event_loop::{ControlFlow, EventLoop};
/// 按顺序建立事件循环、共享文档、API/VM worker 和应用窗口;事件循环结束后返回原生退出清理结果。
// 操作系统启动可执行文件时进入;各阶段的 ? 将启动/循环错误返回进程入口,不把失败当作成功运行。
fn main() -> Result<()> {
// 原生窗口由事件循环所属线程驱动;Wait 让空闲时休眠,而不是持续重绘。
let event_loop = EventLoop::<AppEvent>::with_user_event().build()?;
// 后台发布通过 EventLoopProxy 唤醒等待,原生窗口请求 RedrawRequested 驱动实际绘制。
event_loop.set_control_flow(ControlFlow::Wait);
// 共享最近完整发布的文档。后台 worker 拥有 VM,GUI 不借用其托管堆。
let namespace = Arc::new(RwLock::new(NamespaceState::default()));
// worker 发布完整 NamespaceState,activity 供 Network 工具观察,control 用于原生窗口发运行时控制请求。
// start 先绑定本机 API 并启动后台设施;端口或线程启动错误会阻止后续应用状态构造。
let (activity, control) = api::start(namespace.clone(), event_loop.create_proxy())?;
// 两个 proxy 指向同一事件循环;后台线程通过纯数据通知唤醒 GUI,不直接操作原生窗口。
let mut app = TesseraApp::new(namespace, activity, event_loop.create_proxy(), control)?;
// run_app 接管线程直至退出;退出后仍需检查清理过程中记录的错误。
event_loop.run_app(&mut app)?;
// 仅 run_app 正常返回后汇总窗口/VM 清理错误;循环本身返回 Err 时上面的 ? 已直接传播。
app.exit_result()
}
+2
View File
@@ -1,4 +1,6 @@
//! 运行时模块入口。VM 生命周期、API、源程序组织、事件与异步任务在这里组合。
//! main 调用 api::start 建立服务;worker 通过 endor_host 操作所属 VM,events 客户端将原生输入排队交付。
//! 任务后台只完成纯数据 I/O,结果是否仍可交付以及如何回到源回调,由串行运行时检查和推进。
//!
// 目录确定模块归属;main 中的 crate 根别名保持内部调用路径稳定。
/// 提供 HTTP 路由、有界请求队列与串行 worker,协调候选 VM 和已发布 namespace。