diff --git a/.yarnrc.yml b/.yarnrc.yml index 3186f3f..d8e6c67 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,2 @@ nodeLinker: node-modules +npmMinimalAgeGate: 60 diff --git a/README.md b/README.md index e459de9..bbf4c7e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ # Respo Guidebook -配对正式 Calcit CLI / `@calcit/procs` 0.30.1,以及已发布的 Docs Workflow 0.0.37、 -Respo 0.16.124、Reel 0.6.41、UI 0.8.0 和 JS-FFI 0.2.5。升级后重新安装依赖并完整 -刷新页面,不混用旧 CLI/alpha runtime。UI 0.8 基础样式可能影响外观。源码仅使用 +配对正式 Calcit CLI / `@calcit/procs` 0.31.0,以及已发布的 Docs Workflow 0.0.38、 +Respo 0.17.0-alpha.2、Reel 0.7.0-alpha.2、UI 0.9.0-alpha.2 和 JS-FFI 0.2.5。 +工具链为正式版本,模块图仍有预发布版本;不把两者统称为全图稳定。升级后重新安装依赖并完整 +刷新页面,不混用旧 CLI/alpha runtime。UI 0.9 基础样式可能影响外观。源码仅使用 `calcit.cirru` 与 `deps.cirru`,不生成 compact/package.cirru。 -Guide 说明当前 Ref/watch、Option、widget/app dispatch、名义 Store 和宿主边界。 -旧 Beginner Guide/API 仍有历史片段,尚未全部迁移或独立执行;站点构建通过不代表 +Guide 说明当前 Ref/watch、Option、event handler/app dispatch、名义 Store 和宿主边界。 +旧 Beginner Guide、部分 Guide 实现说明和 API 仍有历史片段,尚未全部迁移或独立执行;站点构建通过不代表 所有旧示例都可用于当前版本。新应用以维护中的模板和已发布接口为准。 ## 开发与验收 -Node 24、Yarn 4.18.0、正式 0.30.1 CLI: +Node 24、Yarn 4.18.0、正式 0.31.0 CLI: ```bash corepack enable @@ -29,6 +30,10 @@ yarn dev 并保存 PR 预览。原项目没有定义附带测试,零个测试不等于业务覆盖;浏览器导航、 搜索与刷新需另行验收。页面 `hud!` 错误展示保留。 +默认 entry 的 dispatch-op 显式绑定原有 renderer 的 Dynamic 宿主入口; +非 Enum 输入仍被拒绝,业务 payload 仍受检后构造 Docs Workflow 的具名 Op。 +Store/Op、全部定义 schema 与处理函数未改,不以关闭证明审计或增加断言绕过 strict。 + ## 部署边界 生产 SPA 引用 `https://cos-sh.tiye.me/Respo/guidebook/` 下的静态 assets,COS 只用 diff --git a/calcit.cirru b/calcit.cirru index 99b3552..3997aac 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -6,7 +6,7 @@ {} (:description |) (:init-fn 'app.main/main!) (:mode :js) (:reload-fn 'app.main/reload!) (:target :browser) :feature-policy $ {} :modules $ [] |respo.calcit/ |respo-ui.calcit/ |respo-markdown.calcit/ |reel.calcit/ |respo-router.calcit/ |docs-workflow/ |js-ffi/ - :type-slots $ {} + :type-slots $ {} $ :dispatch-op 'Dynamic :files $ {} 'app.config $ %{} 'FileEntry :defs $ {} diff --git a/deps.cirru b/deps.cirru index badd552..9a8658d 100644 --- a/deps.cirru +++ b/deps.cirru @@ -1,10 +1,10 @@ -{} (:calcit-version |0.30.1) +{} (:calcit-version |0.31.0) :version |0.0.4 - :dependencies $ {} (|Respo/reel.calcit |0.6.41) - |Respo/respo-markdown.calcit |0.4.59 - |Respo/respo-router.calcit |0.8.37 - |Respo/respo-ui.calcit |0.8.0 - |Respo/respo.calcit |0.16.124 - |calcit-lang/docs-workflow |0.0.37 + :dependencies $ {} (|Respo/reel.calcit |0.7.0-alpha.2) + |Respo/respo-markdown.calcit |0.5.0-alpha.3 + |Respo/respo-router.calcit |0.9.0-alpha.1 + |Respo/respo-ui.calcit |0.9.0-alpha.2 + |Respo/respo.calcit |0.17.0-alpha.2 + |calcit-lang/docs-workflow |0.0.38 |calcit-lang/js-ffi |0.2.5 diff --git a/docs/beginner-guide.md b/docs/beginner-guide.md index 333b0a8..fa16247 100644 --- a/docs/beginner-guide.md +++ b/docs/beginner-guide.md @@ -1,6 +1,6 @@ > Historical tutorial: the old macro expansions, JavaScript access and dispatch > examples below predate current typed APIs. Start with **Guide** in the sidebar -> for formal 0.30.1 contracts. This is not a standalone checked current app. +> for formal 0.31.0 contracts. This is not a standalone checked current app. Want to explore by yourself? diff --git a/docs/guide.md b/docs/guide.md index c5703a9..7d8dbbf 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,7 +1,8 @@ -# Guide: Calcit 0.30.1 +# Guide: Calcit 0.31.0 -This site pairs formal Calcit CLI / `@calcit/procs` **0.30.1**, Respo -**0.16.124**, Reel **0.6.41** and Docs Workflow **0.0.37**. Use `calcit.cirru` +This site pairs formal Calcit CLI / `@calcit/procs` **0.31.0**, Respo +**0.17.0-alpha.2**, Reel **0.7.0-alpha.2** and Docs Workflow **0.0.38**. +The CLI/runtime are formal releases; the module graph still includes prereleases. Use `calcit.cirru` for source and `deps.cirru` for published module versions; do not restore `compact.cirru` or `package.cirru`, or mix this runtime with an alpha CLI. @@ -13,7 +14,7 @@ versions before copying code: upgrading this site does not upgrade other apps. Use `defref` / `ref` for retained mutable references, and `add-watch!` / `remove-watch!` for effectful subscriptions. The historical `defatom`, `atom` and -unmarked watch names are not recommended spellings. Do not hide a missing type +unmarked watch names have been removed from the current language. Do not hide a missing type relationship with `unsafe-coerce` merely to pass a check. A native counter illustrates an explicit effectful function contract: @@ -36,12 +37,14 @@ strings and numbers through published JS-FFI adapters; an unchecked cast does not prove a JavaScript value has a Calcit schema. Keep JavaScript `bottom-tip` `hud!` calls in development reload handlers so errors remain visible on-page. -## Component operations are not application operations +## Event callbacks and application operations -`respo.core/>>` selects a state branch and carries its cursor. Widget handlers -receive Respo's widget dispatcher; an application `render!` dispatcher receives -one operation. Do not copy the two-argument widget dispatcher's type into the -application entry point. +`respo.core/>>` selects a state branch and carries its cursor. A Respo event +handler receives `(event, dispatch!)`; the dispatch function accepts exactly +one value of the entry's `dispatch-op` type and returns Unit. For state changes, +package the cursor and new state in one application operation, then validate +its fields at the app boundary. A two-argument handler or custom +`on-select(path, d!)` callback does not make `d!` a two-argument dispatcher. Define an application Op enum and validate incoming state cursors/payloads before constructing it. With typed Reel, retain `State` in a Ref, @@ -68,8 +71,9 @@ checks, full strict diagnostics, and zero deprecated/dynamic-method findings in CI. Both SPA and mdBook are built. These checks do **not** execute every Markdown fragment or certify browser/WebSocket application behavior. -The Beginner Guide and several API chapters are historical design notes. Their +The Beginner Guide, older implementation discussions under Guide, and several API +chapters retain historical design notes. Their old macro expansions, direct JavaScript access and operation syntax are not -copy-ready 0.30.1 code. Consult the paired published module's `calcit query def` +copy-ready 0.31.0 code. Consult the paired published module's `calcit query def` and `query schema` views and the maintained template. Historical examples need section-by-section migration, not mechanical renaming without contract checks. diff --git a/docs/guide/component-states.md b/docs/guide/component-states.md index cf1a520..e5dcee3 100644 --- a/docs/guide/component-states.md +++ b/docs/guide/component-states.md @@ -1,151 +1,58 @@ ## Component States -Unlike React, states in Respo is maintained manually for stablility during hot code swapping. -At first, states is a HashMap inside the store: +A component state branch is an open Map with a `:cursor` path and optional +`:data`. The state value can differ between widgets. Keep that heterogeneous +boundary explicit; decode the fields a widget consumes before using them as a +business model. A missing Map key returns `Option :none`, not an implicit nil. -```cirru -defatom *store {} - :states $ {} -``` - -By design, if states is added, you would a tree: - -```cirru -{} - :states $ {} - :data $ {} - :todolist $ {} - :data $ {} - :input "|xyz..." - "task-1-id" $ {} - :data $ {} - :draft "|xxx..." - "task-2-id" $ {} - :data $ {} - :draft "|yyy..." - "task-2-id" $ {} - :data $ {} - :draft "|zzz.." -``` - -`:data` is a special field for holding state of each component. -It has to be a tree since Virtual DOM is a tree. -You also notice that its structure is simpler than a DOM tree, it only contains states. - -`respo.core/>>` is the "picking branch" function. It also maintains a `:cursor` field. - -When you call `(>> states :todolist)`, you get new `states` variable for a child component: - -```cirru -{} - ; "generated cursor, nil at top level" - :cursor $ [] :todolist - ; "state at current level" - :data $ {} - :input "|xyz..." - - ; states for children - "task-1-id" $ {} - :data $ {} - :draft "|xxx..." - "task-2-id" $ {} - :data $ {} - :draft "|yyy..." - "task-2-id" $ {} - :data $ {} - :draft "|zzz.." -``` - -Then you call `(>> states "task-1-id")` and you get new `states` for child "task-1": - -```cirru -{} - ; "generated cursor" - :cursor $ [] :todolist "|task-1-id" - - ; "state of task-1" - :data $ {} - :draft "|xxx..." -``` - -For state inside each component, it's `nil` at first. -You want to have an initial state, use `or` to provide one. +`respo.core/>>` selects a branch and extends its cursor. This small example uses +Tag keys, matching this documentation app's checked `List` operation path: ```cirru -defcomp comp-task (states) - let - cursor (:cursor states) - state $ or (:data states) $ {} - :draft "|empty" - (div {})) +let + states $ {} (:cursor $ []) (:task $ {} (:data |Draft)) + branch $ respo.core/>> states :task + assert= (Option :some $ [] :task) (get branch :cursor) + assert= (Option :some |Draft) (get branch :data) ``` -By accessing `(:data states)`, you get `nil`, so `&{} :draft "|empty"` is used. -After there's data in states, you get data that was set. +A missing branch starts with an empty cursor-bearing Map. Do not replace a +present false, zero or empty string state with an arbitrary default. Match the +Option returned by `get`; decode a present value according to the widget's +actual state shape. -Then you want to update component s:tate +A Respo event handler receives two arguments: the event Map and the application +dispatch function. That dispatch function accepts **one** entry `dispatch-op` +value and returns Unit. Two handler arguments do not imply a two-argument +state dispatcher. Wrap the cursor and new state in the app's single operation: ```cirru -defcomp comp-task (states) - let - cursor (:cursor states) - state $ or (:data states) $ {} - :draft "|empty" - div $ {} - :on-click $ fn (e dispatch!) - dispatch! cursor (assoc state :draft "|New state") +fn (event d!) + d! $ :: :states cursor new-state + , &unit ``` -So `(dispatch! cursor state)` sends the new state. +This is a callback fragment in a context that already provides `cursor` and +`new-state`, not a standalone program. This site's application boundary checks +the raw Enum and payload before constructing the published nominal Op. +A custom callback such as `on-select(path, d!)` can also have two arguments; +that does not change the arity of the `d!` value passed to it. -The last step to to update global states with `respo.cursor/update-states`. -Internally `(dispatch! cursor op-data)` will be transformed to `(dispatch! :states ([] cursor op-data))`. -And then in `updater` you add: +For the tree update itself, use the current three-argument helper: ```cirru -case-default op - ; other actions - do store - - ; "where op-data is [cursor new-state]" - :states (update-states store op-data) +let + updated $ respo.cursor/update-state-tree ({}) ([] :task) |Edited + assert= ({} $ :task $ {} (:data |Edited)) updated ``` ---- - -Let's wrap it. First we have empty states inside store: - -```cirru -{} - :states $ {} -``` - -And it is passed to `(comp-todolist (>> states :todolist) data)`, -and then passed to `(comp-task (>> states (:id task)) task)`. - -In `comp-todolist`, `(:data states)` provides component state, `(:cursor states)` provides its cursor. -Call `(dispatch! cursor {:input "|New draft"})` and global store will become: - -```cirru -{} - :states $ {} - :todolist $ {} - :data $ {} - :input "|New draft" - -``` - -In `comp-task` of "task-1", you also get `state` and `cursor`, so call `(dispatch! cursor {:draft "New text"})` you will get: - -```cirru -{} - :states $ {} - :todolist $ {} - :data $ {} - :input "|New draft" - "task-1-id" $ {} - :data $ {} - :draft "|New text" -``` +`update-state-tree` and `respo.cursor/update-states` expose open boundaries; +`update-states` also takes three arguments: store, cursor, new state. Their +results are not automatically proof of a nominal Store. This app's published +Docs Workflow updater checks the updated state Map before assigning it into +Store. Its app dispatcher validates cursors and hydrated stores before building +`docs-workflow.schema/Op`, then typed Reel records that operation. -And that's how Respo states is maintained. +Use the actual checked `app.main/dispatch!` and `docs-workflow.updater/updater` +definitions as integration references. The removed `case-default` example and +the old two-argument `update-states` call are not current alternatives. diff --git a/docs/guide/hot-swapping.md b/docs/guide/hot-swapping.md index e4afa98..b6110bf 100644 --- a/docs/guide/hot-swapping.md +++ b/docs/guide/hot-swapping.md @@ -1,27 +1,43 @@ ## Hot swapping -Hot swapping is done by the compiler. What you need to do is to call `respo.core/clear-cache!` before re-rendering happens: +Use retained `defref` values and explicit watch effects. The current app keeps +its typed Reel in `app.main/*reel`; its value is immutable, while the reference +is mutable. `defatom`, `atom`, `add-watch` and `remove-watch` are retired names. +Do not add another reference around the initial store. + +This is the initialization expression from this site's checked source: ```cirru -defn reload! () - ; "clear component caches" - clear-cache! - ; "rerender DOM tree, I mean, a diff/patch loop" - render-app! - println "|Code update." +defref *reel $ typed/new-reel schema/store ``` -If you don't, in the next rendering phase old element tree would be used if no argument changes found, which means Respo would still use render functions defined previously. +It belongs with the published `reel.typed/State` schema attached to +`app.main/*reel`. An isolated excerpt does not carry that schema or its imports; +inspect the actual definition with `calcit query def 'app.main/*reel'` before reuse. -In Respo, you are asked to define `*store` explicitly. They the global states of data. As an Atom, the value inside is immutable, but the reference is mutable. During hot swapping, variables defined with `defatom` will be retained. As a result, component states are persistent even code is swapped: +The successful branch of this app's `reload!` uses the following sequence: ```cirru -defatom *store $ atom - or - let - raw $ or - js/localStorage.getItem "|respo" - , "|{:data [], :states {}}" - read-string raw - , schema/store +remove-watch! *reel :changes +clear-cache! +add-watch! *reel :changes $ fn (reel prev) + render-app! +reset! *reel $ typed/refresh updater @*reel schema/store ``` + +Clear the component cache before refreshing Reel. Register the new callback +before that refresh so its state change renders the new code. Watch callbacks +receive both the new and previous values; they are not the application dispatch +callback. Replacing the named watch avoids retaining an obsolete callback after +reload. + +Keep the original error branch and JavaScript `bottom-tip` `hud!` calls: build +errors must remain visible on the page. A failed build must not refresh the +running state as though it were valid. `hud!` is a host UI function, not a +replacement for `raise` or a language primitive. + +Archive loading is a separate boundary. This app currently writes an archive +but does not restore it at startup; this migration does not enable restoration. +If adding restoration, decode the wire value into the local Store before +registering automatic saves. A parse failure must not overwrite the original +archive with a default store. diff --git a/docs/guide/server-rendering.md b/docs/guide/server-rendering.md index e7401ed..0f831d3 100644 --- a/docs/guide/server-rendering.md +++ b/docs/guide/server-rendering.md @@ -1,80 +1,43 @@ -For more details please read - -### Rendering assumptions - -Before talking about **S**erver **S**ide **R**endering(SSR), you should know about how Respo mounts and rerenders. There's a Atom called `*global-element` which represents the virtual DOM of currently rendered HTML content on the page: - -```cirru -defatom *global-element nil -``` - -And every time you call `render!`, it checks if old virtual DOM exists. If exists, it will do patching with `rerender-app!` rather than mounting: - +## Server rendering + +The current public HTML output API is `respo.render.html/make-string`. It +purifies a component tree and serializes HTML without live event handlers. +`respo.core/realize-ssr!` adopts existing server-rendered DOM on the client before +the first ordinary `render!` call. These APIs are not a second application store. + +For an SSR integration: + +1. Build the server component tree from a checked Store and render it with + `respo.render.html/make-string`. Include the matching stylesheet resources. +2. On the client, recover and validate the same initial data before building + the matching component tree. Resolve an actual `respo.dom/DomElement` mount + target; a missing selector result is not a valid SSR target. +3. Pass that mount target, the **Component** and the application's single-op + Unit-returning dispatcher to `respo.core/realize-ssr!`. It compares existing + DOM, adopts the tree, attaches handlers and mounts effects. +4. Continue through the normal `respo.core/render!` path for later updates. + `render!` checks whether a previous component exists and mounts or patches + accordingly. Do not call `realize-ssr!` repeatedly as a refresh operation. + +Do not copy the historical `render-element`, mutable `*global-element`, nil +Atom, direct `js/document.querySelector`, or unmarked `add-watch` implementation +recipes. The retained renderer state is owned by Respo. Current callers use +published DOM contracts and `defref` / explicit watch effects for their own +state; they do not reset Respo's internal Ref to impersonate an adopted tree. + +This guidebook remains a client-rendered SPA plus a separate static mdBook. It +does not enable or runtime-test an SSR application in this migration. SSR +requires a server/client data and CSS pipeline; producing an HTML string alone +does not establish correct hydration or event delivery. Inspect the paired +release's actual contracts before integrating: + +```sh +calcit .calcit/modules/respo.calcit/calcit.cirru query def respo.render.html/make-string +calcit .calcit/modules/respo.calcit/calcit.cirru query schema respo.core/realize-ssr! +calcit .calcit/modules/respo.calcit/calcit.cirru query schema respo.core/render! ``` -defn render! (target markup dispatch!) - if (some? @*global-element) - rerender-app! target markup dispatch! - mount-app! target markup dispatch! -``` - -### What is SSR in Respo? - -So SSR is there's already HTML in `
{"some HTML existed"}
` and Respo need to patch the DOM in the first screen. And in order to generate the patches, we must prepare an old virtual DOM so that we can call diff function. - -And note that the HTML transferred over the network does not bind events, and we need to bind them on client side. Internally there's `mute-element` function to remove events from virtual DOM. - -### Server rendering - -Virtual DOM can be rendered on a server, use it like in JavaScript. - -[`make-string`](../apis/make-string.md) is the function to render HTML. [`realize-ssr!`](../apis/realize-ssr_.md) is also useful to make first screen look smoother, make sure it's called before `render!`. - -Notice that when rendering on server, events are not bound, -internally we use [`mute-element`](../apis/mute-element.md) to remove events before rendering. -Without [`realize-ssr!`](../apis/realize-ssr_.md), [`render!`](../apis/render_.md) function will remove existing DOM and mount the whole tree. - -### `realize-ssr!` solution - -How to prepare that virtual DOM? You have to render that by yourself. Since Respo components are like functions, it's not hard. Read code below: - -```cirru -defatom *store $ {} - -def mount-target (js/document.querySelector "|.app") - -defn -main () - if server-rendered? - realize-ssr! target - render-element (comp-container @*store) - , dispatch! - render-app! - add-watch *store :changes render-app! -``` - -It can be divided into several steps: - -- call `(comp-container store)` to create component -- call `(render-element component)` to render component to virtual DOM -- call `(realize-ssr! target element dispatch!)` to reset `*global-element` we mentioned above -- then call `render!` with `(render-app!)` - -In `realize-ssr!` we also setup the event listener, and all listeners are finished registering after `render!` is called, i.e. DOM patching finished. - -### Extracting CSS defined in Calcit - -Respo introduced `defstyle` macro for generating `