From 2f279c2082065602679cbe8aa5e3cc951ac3c1a0 Mon Sep 17 00:00:00 2001 From: cqh963852 Date: Thu, 8 Oct 2026 16:34:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=9A=E5=AE=A2=E6=A0=8F=E7=9B=AE?= =?UTF-8?q?=E2=80=94=E2=80=94navbar=20=E7=AC=AC=E4=B8=89=E8=A1=8C=E6=8C=89?= =?UTF-8?q?=E6=9C=88=E5=88=86=E7=B1=BB=EF=BC=882026-08/2025-07=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E6=97=A5=E6=9C=9F=E9=A1=B5=E9=A2=84=E8=A7=88=E6=96=87?= =?UTF-8?q?=E7=AB=A0=E5=8D=A1=E7=89=87=EF=BC=8C=E5=8D=A1=E7=89=87=E8=B7=B3?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E9=A1=B5=EF=BC=9Bmarkdown=20=E5=8E=9F?= =?UTF-8?q?=E6=96=87=E9=9A=8F=E4=BB=93=20+=20=E8=BF=B7=E4=BD=A0=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=99=A8=E5=85=A8=E9=87=8F=E9=A3=8E=E6=A0=BC=E5=8C=96?= =?UTF-8?q?=EF=BC=88t2=E2=96=B2=E8=8A=82=E6=A0=87/=E7=A7=AF=E6=9C=A8?= =?UTF-8?q?=E7=82=B9=E5=88=97=E8=A1=A8/=E5=A2=A8=E5=BA=95=E7=A0=81?= =?UTF-8?q?=E5=9D=97/=E8=99=9A=E7=BA=BF=E5=BC=95=E8=A8=80/=E5=B9=BD?= =?UTF-8?q?=E7=81=B5=E5=B9=B4=E5=8F=B7=E6=A8=AA=E5=B9=85=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E6=97=A5=E6=9C=9F=E5=8F=96=E9=A6=96=E6=8F=90=20git=20=E6=97=A5?= =?UTF-8?q?=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/tutorial/blog-2025-07.html | 12 + apps/tutorial/blog-2026-08.html | 12 + apps/tutorial/blog-gorzil.html | 12 + apps/tutorial/blog-sapling-transformer.html | 12 + apps/tutorial/src/components/BlogPages.tsx | 213 ++++++++++++++++++ apps/tutorial/src/content/blog/gorzil.md | 100 ++++++++ .../src/content/blog/sapling_transformer.md | 43 ++++ apps/tutorial/src/content/blogs.ts | 48 ++++ apps/tutorial/src/lib/ui.tsx | 23 +- apps/tutorial/src/pages/blog-2025-07/main.tsx | 4 + apps/tutorial/src/pages/blog-2026-08/main.tsx | 4 + apps/tutorial/src/pages/blog-gorzil/main.tsx | 4 + .../pages/blog-sapling-transformer/main.tsx | 4 + apps/tutorial/src/vite-env.d.ts | 1 + 14 files changed, 490 insertions(+), 2 deletions(-) create mode 100644 apps/tutorial/blog-2025-07.html create mode 100644 apps/tutorial/blog-2026-08.html create mode 100644 apps/tutorial/blog-gorzil.html create mode 100644 apps/tutorial/blog-sapling-transformer.html create mode 100644 apps/tutorial/src/components/BlogPages.tsx create mode 100644 apps/tutorial/src/content/blog/gorzil.md create mode 100644 apps/tutorial/src/content/blog/sapling_transformer.md create mode 100644 apps/tutorial/src/content/blogs.ts create mode 100644 apps/tutorial/src/pages/blog-2025-07/main.tsx create mode 100644 apps/tutorial/src/pages/blog-2026-08/main.tsx create mode 100644 apps/tutorial/src/pages/blog-gorzil/main.tsx create mode 100644 apps/tutorial/src/pages/blog-sapling-transformer/main.tsx create mode 100644 apps/tutorial/src/vite-env.d.ts diff --git a/apps/tutorial/blog-2025-07.html b/apps/tutorial/blog-2025-07.html new file mode 100644 index 0000000..bcee989 --- /dev/null +++ b/apps/tutorial/blog-2025-07.html @@ -0,0 +1,12 @@ + + + + + + 博客 · 2025-07 + + +
+ + + diff --git a/apps/tutorial/blog-2026-08.html b/apps/tutorial/blog-2026-08.html new file mode 100644 index 0000000..958003c --- /dev/null +++ b/apps/tutorial/blog-2026-08.html @@ -0,0 +1,12 @@ + + + + + + 博客 · 2026-08 + + +
+ + + diff --git a/apps/tutorial/blog-gorzil.html b/apps/tutorial/blog-gorzil.html new file mode 100644 index 0000000..d86584c --- /dev/null +++ b/apps/tutorial/blog-gorzil.html @@ -0,0 +1,12 @@ + + + + + + gorzil:面向多人协作的 AI 原生工具开发自述 + + +
+ + + diff --git a/apps/tutorial/blog-sapling-transformer.html b/apps/tutorial/blog-sapling-transformer.html new file mode 100644 index 0000000..b05c8e2 --- /dev/null +++ b/apps/tutorial/blog-sapling-transformer.html @@ -0,0 +1,12 @@ + + + + + + sapling_transformer 近半月开发自述 + + +
+ + + diff --git a/apps/tutorial/src/components/BlogPages.tsx b/apps/tutorial/src/components/BlogPages.tsx new file mode 100644 index 0000000..8cb54c4 --- /dev/null +++ b/apps/tutorial/src/components/BlogPages.tsx @@ -0,0 +1,213 @@ +/** 博客页——日期页(月内文章卡片预览)+ 文章页(markdown 风格化渲染)。 + * 迷你 md 解析:#/##/###、> 引言、``` 代码块、-/* 列表、**粗体**、`行内码`。 */ +import type { ReactNode } from 'react' +import { Page, Section } from '../lib/ui' +import { MONTHS, POSTS, monthOf } from '../content/blogs' +import { BrickDot, Mark, Tri } from '@stack/design-system' + +/* ── 行内:**粗体** 与 `行内码` ── */ +function inline(text: string): ReactNode[] { + const parts: ReactNode[] = [] + const re = /(\*\*[^*]+\*\*|`[^`]+`)/g + let last = 0 + let m: RegExpExecArray | null + let i = 0 + while ((m = re.exec(text))) { + if (m.index > last) parts.push(text.slice(last, m.index)) + const t = m[0] + if (t.startsWith('**')) parts.push({t.slice(2, -2)}) + else parts.push( + + {t.slice(1, -1)} + , + ) + last = m.index + t.length + } + if (last < text.length) parts.push(text.slice(last)) + return parts +} + +/* ── 块级渲染(对齐字体系统:h2=t2、h3=t3、正文=content、列表=积木点) ── */ +function Markdown({ raw }: { raw: string }) { + const lines = raw.replace(/^---\n[\s\S]*?\n---\n/, '').split('\n') + const out: ReactNode[] = [] + let i = 0 + let key = 0 + while (i < lines.length) { + const ln = lines[i] + if (!ln.trim() || ln.trim() === '{/* truncate */}') { + i++ + continue + } + if (ln.startsWith('```')) { + const buf: string[] = [] + i++ + while (i < lines.length && !lines[i].startsWith('```')) buf.push(lines[i++]) + i++ + out.push( +
+          {buf.join('\n')}
+        
, + ) + continue + } + if (ln.startsWith('> ')) { + out.push( +
+

{inline(ln.slice(2))}

+
, + ) + i++ + continue + } + if (ln.startsWith('# ')) { + out.push( +

+ {inline(ln.slice(2))} +

, + ) + i++ + continue + } + if (ln.startsWith('## ')) { + out.push( +

+ + {inline(ln.slice(3))} +

, + ) + i++ + continue + } + if (ln.startsWith('### ')) { + out.push( +

+ {inline(ln.slice(4))} +

, + ) + i++ + continue + } + if (/^\d+\. |- /.test(ln)) { + const items: string[] = [] + const ordered = /^\d+\. /.test(ln) + while (i < lines.length && (ordered ? /^\d+\. /.test(lines[i]) : /^- /.test(lines[i]))) { + items.push(lines[i].replace(/^(\d+\.|-) /, '')) + i++ + } + out.push( + , + ) + continue + } + // 段落:连续非格式行合并 + const buf: string[] = [ln] + i++ + while ( + i < lines.length && + lines[i].trim() && + !/^(#{1,3} |> |```|\d+\. |- |\/\* |\{\/\*)/.test(lines[i]) + ) { + buf.push(lines[i]) + i++ + } + out.push( +

+ {inline(buf.join(' '))} +

, + ) + } + return
{out}
+} + +/* ── 文章卡片(日期页预览) ── */ +function BlogCard({ post }: { post: (typeof POSTS)[number] }) { + const quote = + post.raw + .split('\n') + .find((l) => l.startsWith('> ') && l.trim() !== '{/* truncate */}') ?? + post.raw.split('\n').find((l) => l.trim() && !l.startsWith('#') && !l.startsWith('---') && !l.startsWith('---')) ?? + '' + return ( + +
+ {post.date} + {post.author} +
+

{post.title}

+

{quote.replace(/^> /, '').slice(0, 90)}…

+

+ 阅读全文 → +

+
+ ) +} + +/* ── 日期页:某月的文章卡片 ── */ +export function BlogDatePage({ dateKey }: { dateKey: string }) { + const month = MONTHS.find((m) => m.key === dateKey)! + return ( + +
+
+ {month.posts.map((p) => ( + + ))} +
+
+
+ ) +} + +/* ── 文章页 ── */ +export function BlogPostPage({ slug }: { slug: string }) { + const post = POSTS.find((p) => p.slug === slug)! + return ( + +
+ + {post.date.slice(2, 4)} + +

{post.title}

+

+ {post.date} + {post.author} + 开发自述 +

+
+
+ +
+
+ + ← 返回 {monthOf(slug)} + + + 返回书架 + +
+
+ ) +} diff --git a/apps/tutorial/src/content/blog/gorzil.md b/apps/tutorial/src/content/blog/gorzil.md new file mode 100644 index 0000000..6082dda --- /dev/null +++ b/apps/tutorial/src/content/blog/gorzil.md @@ -0,0 +1,100 @@ +--- +authors: cqh963852 +--- + +# gorzil:面向多人协作的 AI 原生工具开发自述 + +> 文章结构整理自 glm-5.3 + +## 为什么做 + +最近观察到一个现象:AI 编码工具越来越强。 + +但它们始终是"单人玩具"。Cursor、Claude Code 这类产品默认一个开发者对着一个终端。人与人围绕 AI 工作过程的协作,基本是空白。 + +{/* truncate */} + +我想做的事情可以概括为:多人 + 多 agent 共享同一个工作现场的工具。人发起任务,agent 执行,所有人(包括其他 agent)看到同一份不断演进的状态。 + +## 发生了什么 + +在这个开发过程中,我只负责提 ISSUE、把控架构和技术走向。总结性、文档性的工作交给了 ai,编码工作也让 ai 处理,结果好坏参半,后续也会提到。 + +## 核心设计 + +gorzil 是一个 Rust 多 Agent 协作平台,三个关键词:CRDT 协同、看板驱动、点对点数据同步。 + +### 数据本质是共享状态,不是消息流 + +这是整个项目最重要的一个技术决策。传统 IM(Slack 式)把协作建模为消息流,但 AI 协作的场景里,任务状态是不断被多方修改的:人创建任务、agent 领取、执行中、产出物挂回任务。如果用消息流建模,状态同步就是一场灾难。 + +所以选择了 CRDT,具体是 Loro。看板/房间状态是共享文档,agent 是文档间的翻译者,图片存引用而非二进制。同步难题由 CRDT 自然消解,不需要中心服务器做仲裁。 + +### 三端架构 + +``` +coord(公网):信令 + Logto 认证 + 组织管理 + TURN +daemon(PC):本地 agent 宿主,接受 WebRTC,Loro 同步 +app(浏览器):Dioxus Web 客户端,发起 WebRTC,Loro 同步 +``` + +coord 只做信令和认证,不碰业务数据。数据在 daemon 和 app 之间通过 WebRTC DataChannel 点对点同步,P2P 失败时降级 coturn 中继。这个"云端尽量薄"的形态是有意为之——协作数据不应该经过我的服务器。 + +### 双 AI 审查的安全模型 + +本地 agent 要执行命令、改文件,安全是绕不开的问题。软件把风险暴露给用户做决策,是很常见的偷懒行为——即对风险不做设计,弹个窗让用户选,责任就转移出去了。 + +这是一种经典的工程师做法。静态规则也是同类问题:区分不了 `rm -rf` 清理 target 和清理 home 的区别,于是干脆都问用户。 + +但协作软件要面对的是不同身份的人群。既要减少风险对用户的干扰,也要保留工程师的安全审查能力。这两件事不能靠同一个机制完成。 + +在这里,gorzil 做了如下设计: + +1. 减少干扰上 + 引入 ai 辅助审查:在 tool_calls 提取和执行之间插入一个独立的审查 AI,做意图对齐判断——主 AI 要执行的动作,是否服务于用户的原始意图。审查 AI 的输入是净化过的结构化摘要,不接收主 AI 的原始上下文,从源头切断 prompt injection 的路径。已知危险命令走硬规则,语义模糊的才交给模型。 + + 对于审查 AI 的额外要求是,接入用户足够信任的模型,不做中转站之类的危险做法——安全链路上如果再引入一个不受信任的环节,那就等于白做。 + +2. 审查能力上 + 要处理的是传统 ai 群聊看不到 agent 工作过程的难题。群聊里 agent 只输出结论,过程是黑盒。虽然有些聊天工具可以不断更新当前的消息,但是我想,应该没有人愿意一直盯着 ai 在干什么。那样没有节省多少时间。 + + gorzil 的做法是把工作过程结构化:看板的任务操作记录,将看板任务抽象为 thread 下的引用,操作历史跟着任务走。 + + 如果要看某个 agent 是怎么做的,则通过 agent doc 检查它的历史记录。工程师不再需要盯着消息流,而是直接审计过程本身。 + +### 被否决的路线 + +点对点通信、传输层曾经动摇过,最后还是使用 WebRTC 方案。最初的方案也是 WebRTC —— 自己做信令、自己管 ICE、自己部署 coturn。每一个步骤都是琐碎的活,甚至 ai 编码留下了 bug。 + +看到 iroh 后,优雅的抽象,地址即身份,打洞内置,看起来可以砍掉一大片代码。几乎要迁过去了,甚至开始规划迁移的步骤。 + +但是在让 ai 真正动手时,ai 说“出问题啦:浏览器端的 P2P 做不了”(一些技术上的限制)。 + +而 WebRTC 那套虽然琐碎,但每一件琐碎的事我都已经理解了。迁移到 iroh 并不会省掉我熟悉的麻烦,而且增加了我不熟悉的黑盒。 + +我最终的选择是那个已经踩过坑的方案。 + +### CRDT 增量同步的坑 + +wasm 端的 CRDT 增量同步是最痛苦的一段。由于 dioxus 是跨平台,rtc 有 native、wasm 两个端。最痛苦的是,不知道 bug 长什么样。 + +bug 修复大部分是 ai 在做。因为无法定位 bug,对 ai 的修复过程既不做严格审查,也不纠结代码对不对。协作方式只有两条: + +1. 反复告诉 ai"出错了",让 ai 打日志,再把日志喂回去 +2. 后续让他自己控制浏览器去读日志 + +折腾到快要放弃 ai 修复,准备人手工处理时,glm-5.3 发布了。用对待 ai 的老方式尝试了一下,问题解决了(绷不住了)。 + +为了这个问题,大概燃烧了 2B 的 token。 + +解决后的第一件事,没有让 ai 接着做事,而是抓紧把集成测试用例写起来。 + +让 ai 趁着神志清晰的时候,把集成测试写上。不至于出现相同的错误时,再费劲告诉 ai 应该怎么做。 + +### UI 体系 + +UI 刚开始做,所以我几乎是 1:1 的模仿 Slack。三栏布局、频道、右侧详情面板,样式体系切到 daisyUI,抽了 gorzil-ui crate,配 storybook 和 Playwright 视觉基线。这部分编码工作 ai 参与度较高,因为组件迁移是模式明确的机械劳动。 + +但模仿的过程中一个问题一直悬着:如果我的软件在形式上和 Slack 几乎接近,那 Slack 也可以解决 gorzil 能解决的问题——除了技术核心不同。这个质疑我给不出反驳。CRDT、P2P、双 AI 审查都是看不见的东西,用户看见的就是又一个聊天软件。 + +所以我还在思考:软件的形式本身是否需要变化,才能更好地揭露问题、处理问题。看板驱动是我目前的一个答案——任务状态是一等公民,消息只是状态的注释。但这个答案对不对,要等真实使用之后才知道。形式追随的还是问题本身,而不是 Slack 的截图。 diff --git a/apps/tutorial/src/content/blog/sapling_transformer.md b/apps/tutorial/src/content/blog/sapling_transformer.md new file mode 100644 index 0000000..6236b94 --- /dev/null +++ b/apps/tutorial/src/content/blog/sapling_transformer.md @@ -0,0 +1,43 @@ +--- +authors: cqh963852 +--- + +# sapling_transformer 近半月开发自述 + +## 为什么做这件事情 + +作为一个在前端深耕的工程师,工程效率,代码运行效率一直是我所关注的内容。受 solidjs 与 rust 的影响,我正在考虑是否可以有更`"好"`的方式来 transfrom jsx/tsx 代码。 + +{/* truncate */} + +在这个过程中,对于一些文档性的工作,大部分我交给了ai,也包括这篇文章的 30%。一些编码工作也尝试过让 ai 编写,但是结果通常比较差,所以不做过多描述。 + +## 发生了什么 + +过去半个月,sapling_transformer 项目经历了多次重构和技术路线调整。项目之所以发展到当前状态,主要是因为在实际开发过程中,原有方案暴露出一系列问题,促使不得不做出改变。 + +## 技术路线调整的原因 + +最初采用了 oxc 及相关依赖,目的是快速实现 AST 转换。 + +但 oxc 方案在实际开发中暴露出多方面问题,尤其是在内存管理上。所以不再描述 oxc 方案下的变化。 + +1. 由于 oxc 过度追求性能,放弃了平稳的内存管理库,编写相关代码时异常痛苦,文档匮乏,仅靠 ai 阅读源码,维护难度极高。 +2. 文档缺失,用例缺失。在 oxc 源码历史中,出现过多次 breaking change,甚至是采用不完整的方案,以至于文档老旧。 +3. 库的部分缺失,在使用 oxc 方案的后半段,尝试自行实现 Visitor Traverse,但在当时的架构设计下,部分关键实现缺失,以至于无法实现架构目标。 + +## biome 方案下的变化 + +迁移到 biome 相关依赖后,项目的架构演进经历了四个阶段。 + +最初尝试直接使用 biome 的 transform 能力,但发现 biome 作为 lint/format 工具的定位,导致其转换接口无法满足语义转换的需求,实际功能受限。 + +为此,第二阶段自建了 Traverse 过程,试图将 solidjs transformer 按语义迁移,但由于架构设计不成熟,代码组织混乱,维护和扩展都遇到障碍。 + +第三阶段对 Traverse 过程进行了重构,转向 transform 架构,但实际实现仍存在缺陷,模块划分和接口设计无法支撑复杂转换流程,架构问题依然突出。如父子关系处理,transfrom 复用。 + +在第四阶段(当前版本),针对前三版暴露的各种问题,对第三版进行了部分重构。具体做法包括: + +1. 使用自定义的树遍历。自定义的树遍历,相比于高度抽象的遍历,可以更加自由的修改遍历函数,以此来支持父子关系处理 +2. 增加同类型节点的不同处理方式。针对 ast transfromer,存在单个语句,不同上下文情况下,输出不同结果的场景。 +3. transfrom 函数复用,结构的重新调整 diff --git a/apps/tutorial/src/content/blogs.ts b/apps/tutorial/src/content/blogs.ts new file mode 100644 index 0000000..c04fa49 --- /dev/null +++ b/apps/tutorial/src/content/blogs.ts @@ -0,0 +1,48 @@ +/** 博客内容注册表——markdown 原文随仓,日期取首提日期(git) */ +import gorzil from './blog/gorzil.md?raw' +import sapling from './blog/sapling_transformer.md?raw' + +export interface BlogPost { + slug: string + title: string + /** YYYY-MM-DD(首提日期) */ + date: string + author: string + raw: string +} + +export const POSTS: BlogPost[] = [ + { + slug: 'gorzil', + title: 'gorzil:面向多人协作的 AI 原生工具开发自述', + date: '2026-08-19', + author: 'cqh963852', + raw: gorzil, + }, + { + slug: 'sapling-transformer', + title: 'sapling_transformer 近半月开发自述', + date: '2025-07-17', + author: 'cqh963852', + raw: sapling, + }, +] + +/** 按月分组(navbar 第三行的日期分类) */ +export interface BlogMonth { + key: string // YYYY-MM + posts: BlogPost[] +} + +export const MONTHS: BlogMonth[] = [...POSTS] + .sort((a, b) => (a.date < b.date ? 1 : -1)) + .reduce((acc, p) => { + const key = p.date.slice(0, 7) + const last = acc[acc.length - 1] + if (last && last.key === key) last.posts.push(p) + else acc.push({ key, posts: [p] }) + return acc + }, []) + +/** 文章所属月(用于文章页 navbar 高亮) */ +export const monthOf = (slug: string) => POSTS.find((p) => p.slug === slug)!.date.slice(0, 7) diff --git a/apps/tutorial/src/lib/ui.tsx b/apps/tutorial/src/lib/ui.tsx index 821a1ac..8536efc 100644 --- a/apps/tutorial/src/lib/ui.tsx +++ b/apps/tutorial/src/lib/ui.tsx @@ -5,6 +5,7 @@ import '@stack/design-system' import type { ReactNode } from 'react' import type { ChapterId } from '@stack/design-system' import { CHAPTERS } from './chapters' +import { MONTHS } from '../content/blogs' /** 设计系统四册(自 design-lab 迁入) */ export type DesignKey = 'design-colors' | 'design-typography' | 'design-symbols' | 'design-components' @@ -34,8 +35,8 @@ export function Page({ /** 章节页标题在章头横幅里,可省 */ title?: string desc?: string - /** 站头高亮:章节 id / 书架 / 设计系统册 */ - active?: ChapterId | 'index' | DesignKey + /** 站头高亮:章节 id / 书架 / 设计系统册 / 博客月(blog-YYYY-MM) */ + active?: ChapterId | 'index' | DesignKey | `blog-${string}` children: ReactNode }) { return ( @@ -83,6 +84,24 @@ export function Page({ ) })} + {/* 第三行:博客(按月分类) */} +
+ 博客 + {MONTHS.map((m) => { + const on = active === `blog-${m.key}` + return ( + + {m.key} + + ) + })} +
{(title || desc) && (
diff --git a/apps/tutorial/src/pages/blog-2025-07/main.tsx b/apps/tutorial/src/pages/blog-2025-07/main.tsx new file mode 100644 index 0000000..16a433e --- /dev/null +++ b/apps/tutorial/src/pages/blog-2025-07/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from 'react-dom/client' +import { BlogDatePage } from '../../components/BlogPages' + +createRoot(document.getElementById('root')!).render() diff --git a/apps/tutorial/src/pages/blog-2026-08/main.tsx b/apps/tutorial/src/pages/blog-2026-08/main.tsx new file mode 100644 index 0000000..324cba0 --- /dev/null +++ b/apps/tutorial/src/pages/blog-2026-08/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from 'react-dom/client' +import { BlogDatePage } from '../../components/BlogPages' + +createRoot(document.getElementById('root')!).render() diff --git a/apps/tutorial/src/pages/blog-gorzil/main.tsx b/apps/tutorial/src/pages/blog-gorzil/main.tsx new file mode 100644 index 0000000..09a6f29 --- /dev/null +++ b/apps/tutorial/src/pages/blog-gorzil/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from 'react-dom/client' +import { BlogPostPage } from '../../components/BlogPages' + +createRoot(document.getElementById('root')!).render() diff --git a/apps/tutorial/src/pages/blog-sapling-transformer/main.tsx b/apps/tutorial/src/pages/blog-sapling-transformer/main.tsx new file mode 100644 index 0000000..8241eb5 --- /dev/null +++ b/apps/tutorial/src/pages/blog-sapling-transformer/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from 'react-dom/client' +import { BlogPostPage } from '../../components/BlogPages' + +createRoot(document.getElementById('root')!).render() diff --git a/apps/tutorial/src/vite-env.d.ts b/apps/tutorial/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/tutorial/src/vite-env.d.ts @@ -0,0 +1 @@ +///