diff --git a/Agents.md b/Agents.md index 7d8d689..684dea5 100644 --- a/Agents.md +++ b/Agents.md @@ -16,7 +16,8 @@ - 仓库根目录:就是 Koka 源码根目录,编译时直接把 repo root 当成模块搜索根。 - `app.kk`:浏览器入口,只暴露 boot、事件桥接和 runtime snapshot 导入导出。 -- `explore/react/core.kk`、`action.kk`、`state.kk`:component authoring surface;业务 view 只从这里获取 elements、actions、stores、keyed lifecycle 与 effects。 +- `explore/react.kk`:普通组件唯一的 author import,只 re-export `core/action/state`,不包含 runtime/inspection/renderer。 +- `explore/react/core.kk`、`action.kk`、`state.kk`:component authoring 实现模块;advanced host/tests 可以按边界显式导入。 - `explore/react/runtime.kk`:browser/app host 使用的 registered callback、scheduled effect 与 snapshot transport。 - `explore/react/inspection.kk`:tests/devtools 使用的 VDOM、event registry 与 runtime entry 只读查询。 - `explore/react/renderer.kk`:host/tests 使用的 render、diff 与 patch;不要导入业务 view。 @@ -152,7 +153,7 @@ chrome-devtools take_screenshot --fullPage --filePath .tmp-devtools-full.png 组件交互状态以 **typed store + serializable actions** 为默认方案。它保留 React reducer 的简单心智模型,同时满足 Koka 严格类型、HMR 和 snapshot 恢复需求。 -- 普通业务 view 只导入 `explore/react/core`、`explore/react/action`、`explore/react/state` 中实际需要的模块;不得导入 `explore/react/runtime`、`explore/react/inspection` 或 `explore/react/renderer`。 +- 普通业务 view 统一 `import explore/react`;不得导入 `explore/react/runtime`、`explore/react/inspection` 或 `explore/react/renderer`。`core/action/state` 保持实现分层,但不再要求组件作者理解三条 import。 - 业务组件用 `(state, dispatch) = use_store(spec, initial = ...)` 读取 reducer pair,不额外暴露 binding record。 - UI 事件优先用 `on_store_click(name, action = ..., dispatch = ...)` / `on_store_input(...)` 发送 typed action,不直接操作 state tree。 diff --git a/PLAN.md b/PLAN.md index 61ba614..42eab84 100644 --- a/PLAN.md +++ b/PLAN.md @@ -104,6 +104,8 @@ ### 业务组件优先使用 ```koka +import explore/react + div(children, class = ..., key = ...) button(text, class = ..., click = ...) input_text(value, input = ..., enter = ..., placeholder = ...) @@ -155,31 +157,28 @@ feature_dom_marker(group = ..., key = ..., name = ...) 按用户风险与依赖顺序推进,不并行扩张公共 API: -### 1. 版本化 runtime snapshot(#17) +### 1. 单一 component-author import(#23) -- 当前实现批次:完整 snapshot 已增加 `respo/runtime-snapshot|1` header,并兼容读取现有无 envelope 格式; -- unknown/malformed 顶层版本回退空树,malformed entry 与 entry 非法编码只丢弃自身,不阻断 app boot; -- entry 级 schema/version 校验保持不变,业务 view 不增加 snapshot 参数或 runtime import; -- Koka compatibility tests、旧 snapshot 浏览器启动、整页 reload 与真实 Vite HMR replacement 恢复均已完成验收。 +- 当前实现批次:新增 `import explore/react`,只 re-export `core/action/state`; +- 真实 demo business modules 迁移到单一入口,advanced host/tests 继续显式依赖 runtime/inspection/renderer; +- `check:author-surface` 固定 re-export allowlist,并禁止业务模块退回三条内部 imports; +- quick start、component author API、README 与开发指南同步单一入口。 -### 2. 定义 effect cleanup 生命周期(#18) +### 2. 可运行 first-component 与错误路径(#24、#25) -- 当前实现批次:`state_effect(...)` 保持轻量 trailing-lambda,`state_resource(...)` 显式声明 setup + cleanup; -- deps change 先 cleanup 后 setup,ordinary child unmount 与 feature reset 释放对应 resource; -- HMR 固定为 snapshot flush、旧 runtime dispose、新 runtime boot,cleanup closure 不进入 snapshot; -- cleanup failure 记录 host error,但不阻断下一次 setup 或新 runtime boot; -- 用一个真实 demo capability、deterministic tests 与浏览器 HMR 回归验证。 +- #24 提供 `yarn example:first-component`,让 typed store/action 的首次成功路径可以直接运行; +- #25 将常见 Koka 编译错误整理为 symptom → cause → minimal fix cookbook; +- 文档示例尽量引用持续编译的源码,减少 snippet drift。 -### 3. 发布 agent-safe store/action surface(#19) +### 3. 安全 tooling surface(#19、#26) -- 暴露可序列化的只读 catalog,不暴露 raw state entry、closure 或 tree write; -- action dispatch 必须通过已注册 codec 校验,并复用现有 typed reducer/observation 链路; -- 明确区分 domain action、ephemeral component action 与默认不可投递 capability; +- #19 先定义可序列化 catalog 与 validated dispatch,不暴露 raw tree/closure; +- #26 再基于 catalog 提供 dev-only read-only action/store inspector; - 自动 replay、完整权限 UI 和远程身份认证继续保持非目标。 ### 持续约束 -- component authoring surface 保持在 `core/action/state`,不增加只做转发的 facade; +- component authoring 实现保持在 `core/action/state`;顶层 `explore/react` 只做 `pub import` allowlist,不新增转发函数; - controlled component 继续使用“主 domain value 位置参数 + labelled callback/config props”; - 显式 key/path/store inspection 只有在能移动真实实现且不产生单行 wrapper 时再拆; - typed listener helper 只在出现第三个真实重复形态时扩展。 @@ -194,8 +193,12 @@ issue、PR 及影响结论的进度更新统一使用中英双语:标题采用 - [#12 简化组件事件中的 domain 与 local-store transition / Simplify domain and local-store transitions in component events](https://github.com/Respo/explore-react.koka/issues/12):已由 PR #15 合并;四个真实调用点共享 event-independent transition,并覆盖 action 顺序与浏览器回归; - [#13 发布渐进式组件作者 API / Publish a progressive-disclosure component authoring surface](https://github.com/Respo/explore-react.koka/issues/13):已由 PR #16 合并;四概念 quick start、单页 author API 与 advanced module import 边界已落地; - [#17 版本化 runtime snapshot 并定义兼容迁移 / Version runtime snapshots and define compatible migration](https://github.com/Respo/explore-react.koka/issues/17):已由 PR #21 合并;snapshot envelope、legacy compatibility 与安全回退已落地; -- [#18 定义组件 effect cleanup 生命周期 / Define the component effect cleanup lifecycle](https://github.com/Respo/explore-react.koka/issues/18):当前实现批次;effect/resource author API、host registry、HMR dispose、测试与文档已完成,等待 PR review; -- [#19 发布 agent-safe store catalog 与校验 action dispatch / Publish an agent-safe store catalog and validated action dispatch](https://github.com/Respo/explore-react.koka/issues/19):后续探索批次;在不暴露 raw runtime tree 的前提下服务 devtools/agents。 +- [#18 定义组件 effect cleanup 生命周期 / Define the component effect cleanup lifecycle](https://github.com/Respo/explore-react.koka/issues/18):已由 PR #22 合并;effect/resource author API、host registry、HMR dispose、测试与文档已落地; +- [#23 提供单一组件作者导入入口 / Provide a single component-author import](https://github.com/Respo/explore-react.koka/issues/23):当前开发批次,减少第一次组件所需的框架结构知识; +- [#24 交付可运行的首个组件示例 / Ship a runnable first-component example](https://github.com/Respo/explore-react.koka/issues/24):下一批次,提供可执行且持续编译的首次成功路径; +- [#25 发布组件编译错误 cookbook / Publish a component compile-error cookbook](https://github.com/Respo/explore-react.koka/issues/25):下一批次,按用户看到的错误组织诊断; +- [#19 发布 agent-safe store catalog 与校验 action dispatch / Publish an agent-safe store catalog and validated action dispatch](https://github.com/Respo/explore-react.koka/issues/19):后续探索批次;先稳定人类用户 author surface,再服务 tools/agents; +- [#26 提供只读 action/store 调试面板 / Provide a read-only action/store inspector](https://github.com/Respo/explore-react.koka/issues/26):依赖 #19 的安全 catalog,作为后续调试体验。 ## 验证标准 diff --git a/README.md b/README.md index 3c2245d..1f88d68 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,16 @@ state, and event dispatch. ## Start with component authoring +Ordinary components need one framework import: + +```koka +import explore/react +``` + +This entry exposes elements, typed actions/stores, keyed lifecycle, and effects; +advanced runtime, inspection, and renderer modules remain explicit host/test +imports. + Learn the normal path as four tasks; runtime ownership and snapshot transport are advanced integration topics, not prerequisites: diff --git a/demo/dialogs.kk b/demo/dialogs.kk index 881934d..7891e9a 100644 --- a/demo/dialogs.kk +++ b/demo/dialogs.kk @@ -3,8 +3,7 @@ module demo/dialogs import library/dialog import demo/model import runtime/dom -import explore/react/core -import explore/react/state +import explore/react fun apply_dialog_update(owner : model, next_update : dialog_update) :
model owner(dialog = next_update.dialog, notice = next_update.notice) diff --git a/demo/effectspanel.kk b/demo/effectspanel.kk index 8e55723..8eeb2bd 100644 --- a/demo/effectspanel.kk +++ b/demo/effectspanel.kk @@ -5,8 +5,7 @@ import demo/model import demo/tests/support import demo/shared import runtime/dom -import explore/react/core -import explore/react/state +import explore/react fun tiny_task_list(key : string, tasks : list) :
vnode if is-empty(tasks) then p("None", class = "empty") else ul(map(tasks, fn(item) li_text(item.title, key = "tiny-" ++ task/id(item).show)), key = key, class = "tiny-list") diff --git a/demo/lab/events.kk b/demo/lab/events.kk index af918b3..edab1e3 100644 --- a/demo/lab/events.kk +++ b/demo/lab/events.kk @@ -6,7 +6,7 @@ import demo/lab/workflow import library/bridge import library/maybe import library/search -import explore/react/action +import explore/react pub type lab_action Load_board diff --git a/demo/lab/state.kk b/demo/lab/state.kk index f95c49f..6c21c72 100644 --- a/demo/lab/state.kk +++ b/demo/lab/state.kk @@ -1,7 +1,6 @@ module demo/lab/state -import explore/react/action -import explore/react/state +import explore/react pub struct incident_local_state(expanded : bool, draft : string) pub type incident_local_action diff --git a/demo/lab/view.kk b/demo/lab/view.kk index 813dbd8..45c163a 100644 --- a/demo/lab/view.kk +++ b/demo/lab/view.kk @@ -7,8 +7,7 @@ import demo/shared import demo/lab/state import library/bridge import library/search -import explore/react/core -import explore/react/state +import explore/react fun severity_class(item : incident) : string match item.severity diff --git a/demo/layout.kk b/demo/layout.kk index 44dffd6..dd7626f 100644 --- a/demo/layout.kk +++ b/demo/layout.kk @@ -9,7 +9,7 @@ import demo/lab/view import demo/effectspanel import demo/testspanel import library/route -import explore/react/core +import explore/react fun intro_panel(item : model) :
vnode header([ diff --git a/demo/model.kk b/demo/model.kk index 15d5a9f..9b8c332 100644 --- a/demo/model.kk +++ b/demo/model.kk @@ -4,8 +4,7 @@ import library/bridge import library/dialog import library/route import library/search -import explore/react/action -import explore/react/state +import explore/react pub type incident_severity Severity_p1 diff --git a/demo/routebar.kk b/demo/routebar.kk index 4b0e686..ec8313e 100644 --- a/demo/routebar.kk +++ b/demo/routebar.kk @@ -2,8 +2,7 @@ module demo/routebar import demo/model import library/route -import explore/react/core -import explore/react/state +import explore/react fun route_button(label : string, route_name : string, active : bool) : app_view vnode val class_name = if active then "button route-tab route-tab-active" else "button route-tab" diff --git a/demo/shared.kk b/demo/shared.kk index 22aa592..9c5ea38 100644 --- a/demo/shared.kk +++ b/demo/shared.kk @@ -1,6 +1,6 @@ module demo/shared -import explore/react/core +import explore/react pub fun panel(children : list, title : string, note : string, key : string, class_name : string) : vnode section([ diff --git a/demo/testspanel.kk b/demo/testspanel.kk index 6c4e165..cb2c2c6 100644 --- a/demo/testspanel.kk +++ b/demo/testspanel.kk @@ -2,7 +2,7 @@ module demo/testspanel import demo/tests/support import demo/shared -import explore/react/core +import explore/react fun test_item(item : test_result) : vnode val name = test_result/name(item) diff --git a/demo/todo/events.kk b/demo/todo/events.kk index c284ed0..d9945a2 100644 --- a/demo/todo/events.kk +++ b/demo/todo/events.kk @@ -2,7 +2,7 @@ module demo/todo/events import demo/model import library/maybe -import explore/react/action +import explore/react pub type todo_action Add_task diff --git a/demo/todo/state.kk b/demo/todo/state.kk index 6c8b684..07c0037 100644 --- a/demo/todo/state.kk +++ b/demo/todo/state.kk @@ -1,7 +1,6 @@ module demo/todo/state -import explore/react/action -import explore/react/state +import explore/react pub struct task_editor_state(editing : bool, draft : string) pub type task_editor_action diff --git a/demo/todo/view.kk b/demo/todo/view.kk index c1b98bf..4cf68a3 100644 --- a/demo/todo/view.kk +++ b/demo/todo/view.kk @@ -6,8 +6,7 @@ import demo/todo/events import demo/todo/state import demo/todo/viewmodel import runtime/dom -import explore/react/core -import explore/react/state +import explore/react fun task_item_view(item : task) : app_view vnode val tid = task/id(item) diff --git a/demo/view.kk b/demo/view.kk index b28f74d..8252688 100644 --- a/demo/view.kk +++ b/demo/view.kk @@ -5,8 +5,7 @@ import demo/model import demo/tests/support import demo/layout import library/dialog -import explore/react/core -import explore/react/state +import explore/react fun append_overlay(shell_node : vnode, overlay_node : vnode) :
vnode shell_node(children = append(vnode/children(shell_node), [overlay_node])) diff --git a/docs/component-authoring.md b/docs/component-authoring.md index 9e9ca3c..dcd5052 100644 --- a/docs/component-authoring.md +++ b/docs/component-authoring.md @@ -2,15 +2,13 @@ # 中文 -普通业务组件只导入三个模块: +普通业务组件只导入一个稳定入口: ```koka -import explore/react/core -import explore/react/action -import explore/react/state +import explore/react ``` -这三个模块分别承载 elements/VDOM、serializable action protocol,以及 component lifecycle/store/effect authoring。不要在业务 view 中导入 `explore/react/runtime`、`explore/react/inspection` 或 `explore/react/renderer`。 +该入口 re-export elements/VDOM、serializable action protocol,以及 component lifecycle/store/effect authoring。实现仍拆分在 `core`、`action`、`state`,但这不是组件作者的前置知识。不要在业务 view 中导入 `explore/react/runtime`、`explore/react/inspection` 或 `explore/react/renderer`。 ## 1. Elements @@ -72,15 +70,13 @@ effect name 在同一 component boundary 内保持稳定。`state_resource(...)` # English -Ordinary business components import only three modules: +Ordinary business components import one stable entry: ```koka -import explore/react/core -import explore/react/action -import explore/react/state +import explore/react ``` -They provide elements/VDOM, the serializable action protocol, and component lifecycle/store/effect authoring. Business views do not import `explore/react/runtime`, `explore/react/inspection`, or `explore/react/renderer`. +It re-exports elements/VDOM, the serializable action protocol, and component lifecycle/store/effect authoring. Implementation remains split across `core`, `action`, and `state`, but component authors do not need that knowledge up front. Business views do not import `explore/react/runtime`, `explore/react/inspection`, or `explore/react/renderer`. ## 1. Elements diff --git a/docs/quick-start.md b/docs/quick-start.md index 868c524..70e763b 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -2,14 +2,20 @@ # 中文 -Respo 的日常组件代码只需要按顺序理解四件事:elements、keyed components、typed stores/actions 和 effects。下面用一个 FAQ feature 把它们串在一起。 +Respo 的日常组件代码从一个入口开始: + +```koka +import explore/react +``` + +它只公开 component authoring surface,不包含 runtime、inspection 或 renderer。之后按顺序理解四件事:elements、keyed components、typed stores/actions 和 effects。下面用一个 FAQ feature 把它们串在一起。 ## 1. 先写普通 view function element 的主要内容保持第一个位置参数,样式、key 和事件使用 labelled arguments: ```koka -import explore/react/core +import explore/react fun faq_answer(text : string) : vnode p(text, class = "faq-answer") @@ -22,8 +28,7 @@ fun faq_answer(text : string) : vnode store 把 state、action、纯 reducer 和恢复方式定义在一起。这个 disclosure 只有一个 toggle action,所以使用内置 bool codec 的 snapshot store: ```koka -import explore/react/action -import explore/react/state +import explore/react type disclosure_action Toggle_disclosure @@ -97,14 +102,20 @@ fun faq_panel(items : list, key : string = "panel") # English -Everyday Respo component code can be learned as four concepts in order: elements, keyed components, typed stores/actions, and effects. The following FAQ feature combines all four. +Everyday Respo component code starts from one entry: + +```koka +import explore/react +``` + +It exposes only the component-authoring surface, not runtime, inspection, or renderer. From there, learn four concepts in order: elements, keyed components, typed stores/actions, and effects. The following FAQ feature combines all four. ## 1. Start with an ordinary view function Keep primary element content positional and use labelled arguments for styling, keys, and events: ```koka -import explore/react/core +import explore/react fun faq_answer(text : string) : vnode p(text, class = "faq-answer") @@ -117,8 +128,7 @@ An ordinary function call only extracts rendering. A helper without local state A store groups its state, action, pure reducer, and recovery choice. This disclosure has one toggle action, so it uses a snapshot store with the built-in bool codec: ```koka -import explore/react/action -import explore/react/state +import explore/react type disclosure_action Toggle_disclosure diff --git a/explore/react.kk b/explore/react.kk new file mode 100644 index 0000000..4959eea --- /dev/null +++ b/explore/react.kk @@ -0,0 +1,7 @@ +module explore/react + +// Ordinary component authors need one stable entry point. Runtime execution, +// inspection, and rendering stay in their explicit advanced modules. +pub import explore/react/core +pub import explore/react/action +pub import explore/react/state diff --git a/library/bridge.kk b/library/bridge.kk index 8a91366..981077e 100644 --- a/library/bridge.kk +++ b/library/bridge.kk @@ -1,6 +1,6 @@ module library/bridge -import explore/react/core +import explore/react pub effect fun request_bridge_resource(payload : string, delay : int) : () diff --git a/library/dialog.kk b/library/dialog.kk index e7d7562..0f32573 100644 --- a/library/dialog.kk +++ b/library/dialog.kk @@ -1,6 +1,6 @@ module library/dialog -import explore/react/core +import explore/react pub type dialog_kind Dialog_none diff --git a/library/search.kk b/library/search.kk index 6d5b57b..ac03711 100644 --- a/library/search.kk +++ b/library/search.kk @@ -1,6 +1,6 @@ module library/search -import explore/react/core +import explore/react pub struct search_case query : string diff --git a/package.json b/package.json index 1520514..796b0f0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build:koka": "node ./scripts/build-koka.mjs", "dev": "yarn build:koka && vite", "build": "yarn build:koka && vite build", - "check": "yarn build:koka && vite build", + "check:author-surface": "node ./scripts/check-author-surface.mjs", + "check": "yarn check:author-surface && yarn build:koka && vite build", "test:koka": "node ./scripts/test-koka.mjs", "preview": "vite preview" }, diff --git a/scripts/check-author-surface.mjs b/scripts/check-author-surface.mjs new file mode 100644 index 0000000..287f361 --- /dev/null +++ b/scripts/check-author-surface.mjs @@ -0,0 +1,68 @@ +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { relative, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = resolve(fileURLToPath(new URL("..", import.meta.url))); +const authorEntryPath = resolve(rootDir, "explore/react.kk"); +const allowedExports = [ + "pub import explore/react/core", + "pub import explore/react/action", + "pub import explore/react/state", +]; +const forbiddenAuthorImport = + /^\s*(?:pub\s+)?import\s+(?:(?:[A-Za-z_][A-Za-z0-9_]*)\s*=\s*)?explore\/react\/(?:core|action|state)\s*(?:\/\/.*)?$/m; + +/** Collect Koka source files recursively from a known project directory. */ +function kokaFiles(directory) { + return readdirSync(directory).flatMap((name) => { + const path = resolve(directory, name); + if (statSync(path).isDirectory()) { + return kokaFiles(path); + } + return name.endsWith(".kk") ? [path] : []; + }); +} + +/** Keep host-owned test and runtime modules out of the component-author check. */ +function isAdvancedDemoModule(path) { + const [topLevel, module] = relative(rootDir, path).split(sep); + return ( + topLevel === "demo" && + (module === "tests" || module === "tests.kk" || module === "runtimeframe.kk") + ); +} + +const authorEntry = readFileSync(authorEntryPath, "utf8"); +const exportedImports = authorEntry + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.startsWith("pub import ")); + +if ( + exportedImports.length !== allowedExports.length || + allowedExports.some((line) => !exportedImports.includes(line)) +) { + throw new Error( + "explore/react.kk must re-export exactly core, action, and state.", + ); +} + +const businessFiles = [ + ...kokaFiles(resolve(rootDir, "library")), + ...kokaFiles(resolve(rootDir, "demo")), +].filter((path) => !isAdvancedDemoModule(path)); + +const violations = businessFiles.filter((path) => { + const source = readFileSync(path, "utf8"); + return forbiddenAuthorImport.test(source); +}); + +if (violations.length > 0) { + throw new Error( + `Business modules must use import explore/react:\n${violations.join("\n")}`, + ); +} + +console.log( + `Author surface OK: one entry, three re-exports, ${businessFiles.length} business modules checked.`, +);