fix(playground): lazy-load runtime for inert sources

This commit is contained in:
Kinneyzhang 2026-09-01 05:57:29 +08:00
parent d237b9bc67
commit 64ca9b5931
5 changed files with 135 additions and 6 deletions

View File

@ -19,6 +19,9 @@ into the right-hand preview. Saving a source file also refreshes by default.
`etaf-playground-register-example` is available when a companion needs a
non-conventional root or feature name.
ETAF Playground 0.2.1 declares ETAF 0.1.1, ETAF UI 0.1.0, and ETAF SQLite
0.1.0 so the bundled Research Shelf companion has a complete install closure.
Preview placement uses the standard Emacs `display-buffer` action stored in
`etaf-playground-display-action`. The default is a right side window using
half the frame:
@ -56,6 +59,11 @@ examples/research-shelf.ecss # inert style source
examples/research-shelf.el # DATA / THEME / STATE / VIEW / ROOT sections
```
Opening an inert `.etaf` or `.ecss` source loads only its lightweight editing
mode. The ETAF/Ebox/TP runtime is loaded when a preview is refreshed or
mounted. Runtime use requires TP 1.0.1 or newer so `tp-transaction.el` and the
Host final-accept contract are present.
The companion registers `:reload-on-refresh t`, so saving the `.el`, `.etaf`, or
`.ecss` source and refreshing reloads the complete consumer before the next
mount.

View File

@ -18,6 +18,9 @@ ETAF Playground 是通用的应用构建工作区:左侧编辑同一个应用
也会刷新。若 root 或 feature 不遵循命名约定,可在 companion 中调用
`etaf-playground-register-example` 注册覆盖。
ETAF Playground 0.2.1 会声明 ETAF 0.1.1、ETAF UI 0.1.0 与 ETAF SQLite
0.1.0,保证内置 Research Shelf companion 的安装依赖闭包完整。
预览位置使用标准 Emacs `display-buffer` action
`etaf-playground-display-action` 配置。默认在右侧使用一半 frame
@ -55,6 +58,10 @@ examples/research-shelf.ecss # inert 样式 source
examples/research-shelf.el # DATA / THEME / STATE / VIEW / ROOT 分区
```
直接打开 inert `.etaf``.ecss` source 时只加载轻量编辑 mode刷新或 mount
preview 时才加载 ETAF/Ebox/TP runtime。运行 preview 需要 TP 1.0.1 或更高版本,
以保证 `tp-transaction.el` 与 Host final-accept contract 已安装。
该 companion 注册了 `:reload-on-refresh t`;保存 `.el`、`.etaf` 或 `.ecss` 后刷新
source会在下一次 mount 前重新加载完整 consumer。

View File

@ -3,8 +3,8 @@
;; SPDX-License-Identifier: GPL-3.0-or-later
;; Author: ETAF contributors
;; Version: 0.2.0
;; Package-Requires: ((emacs "29.1") (etaf "0.1.0"))
;; Version: 0.2.1
;; Package-Requires: ((emacs "29.1") (etaf "0.1.1") (etaf-sqlite "0.1.0") (etaf-ui "0.1.0"))
;; Keywords: tools, convenience, ui
;;; Commentary:
@ -32,7 +32,21 @@
(require 'cl-lib)
(require 'subr-x)
(require 'button)
(require 'etaf)
(define-error 'etaf-playground-dependency-error
"ETAF Playground requires TP 1.0.1 or newer with tp-transaction")
(defun etaf-playground--ensure-runtime ()
"Load ETAF runtime dependencies or report an actionable TP mismatch."
(condition-case condition
(require 'etaf)
(file-missing
(if (member "tp-transaction" condition)
(signal 'etaf-playground-dependency-error
(list :required-package '(tp "1.0.1")
:missing-feature 'tp-transaction
:original-condition condition))
(signal (car condition) (cdr condition))))))
(defgroup etaf-playground nil
"Authoring workspaces for ETAF applications."
@ -86,6 +100,11 @@ frame."
(declare-function ebox-call-with-render-burst
"ebox-buffer-backend" (function &rest arguments))
(declare-function etaf-action-redefine-run "etaf-actions" (function))
(declare-function etaf-component-redefine-run "etaf-component" (function))
(declare-function etaf-mount "etaf-runtime" (buffer view &optional options))
(declare-function etaf-runtime-for-buffer "etaf-runtime" (buffer))
(declare-function etaf-unmount "etaf-runtime" (runtime))
(cl-defstruct (etaf-playground-session
(:constructor etaf-playground--session-create))
@ -510,6 +529,7 @@ source editors and side-by-side workspace around it. MOUNT-OPTIONS is
forwarded to `etaf-mount', including an optional initial viewport. Source
loading, root construction, ETAF publication, and Ebox rendering share one
public framework render burst so GC cannot split an interactive mount."
(etaf-playground--ensure-runtime)
(ebox-call-with-render-burst
#'etaf-playground--mount-example-now
buffer-name name session mount-options))
@ -534,6 +554,7 @@ public framework render burst so GC cannot split an interactive mount."
(let ((preview (etaf-playground-session-preview-buffer session)))
(condition-case err
(progn
(etaf-playground--ensure-runtime)
(when-let* ((runtime (etaf-runtime-for-buffer preview)))
(with-current-buffer preview
(etaf-unmount runtime)))
@ -620,10 +641,12 @@ the preview instead of destroying the source workspace."
(remove-hook 'after-change-functions
#'etaf-playground--after-source-change t)))
;;;###autoload
(define-derived-mode etaf-playground-etaf-mode emacs-lisp-mode "ETAF"
"Major mode for inert ETAF structural source files."
(etaf-playground-source-mode 1))
;;;###autoload
(define-derived-mode etaf-playground-ecss-mode emacs-lisp-mode "ECSS"
"Major mode for inert ETAF ECSS style source files."
(etaf-playground-source-mode 1))
@ -634,7 +657,9 @@ the preview instead of destroying the source workspace."
(setq-local buffer-read-only nil)
(setq-local mode-line-process nil))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.etaf\\'" . etaf-playground-etaf-mode))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.ecss\\'" . etaf-playground-ecss-mode))
(defun etaf-playground--source-header ()
@ -984,8 +1009,10 @@ default example there."
(window-frame preview-window)))
(configuration
(etaf-playground-session-previous-window-configuration session)))
(when-let* ((runtime (and (buffer-live-p preview)
(etaf-runtime-for-buffer preview))))
(when-let* ((runtime
(and (fboundp 'etaf-runtime-for-buffer)
(buffer-live-p preview)
(etaf-runtime-for-buffer preview))))
(with-current-buffer preview
(etaf-unmount runtime)))
(remhash (etaf-playground-session-name session)
@ -1013,7 +1040,8 @@ default example there."
((and (stringp target) (get-buffer target))
(get-buffer target))
((null target) (current-buffer))))
(runtime (and (buffer-live-p buffer)
(runtime (and (fboundp 'etaf-runtime-for-buffer)
(buffer-live-p buffer)
(etaf-runtime-for-buffer buffer))))
(when runtime
(with-current-buffer buffer

View File

@ -9,6 +9,7 @@
;;; Code:
(require 'cl-lib)
(require 'seq)
(require 'etaf)
(require 'etaf-playground)
(require 'etaf-performance)
(require 'ebox-native-reflow)

View File

@ -8,15 +8,100 @@
;;; Code:
(require 'cl-lib)
(require 'ert)
(require 'package)
(require 'etaf-playground)
(require 'etaf-performance)
(defconst etaf-playground-test--root
(file-name-directory
(directory-file-name
(file-name-directory (or load-file-name buffer-file-name))))
"Absolute ETAF Playground repository path used by subprocess gates.")
(defun etaf-playground-test--package-description (file)
"Return the package description parsed from FILE."
(with-temp-buffer
(insert-file-contents file)
(package-buffer-info)))
(defun etaf-playground-test--ensure-app-loaded ()
"Load the same-basename Research Shelf companion for test setup."
(unless (featurep 'etaf-research-shelf)
(load-file (expand-file-name "examples/research-shelf.el"
default-directory))))
(ert-deftest etaf-playground-etaf-source-mode-clean-loads-without-runtime ()
"Opening research-shelf.etaf must not eagerly load ETAF or TP."
(let ((output (generate-new-buffer " *etaf-source-mode-clean-load*"))
(program (expand-file-name invocation-name invocation-directory))
(package-file
(expand-file-name "etaf-playground.el" etaf-playground-test--root))
(source-file
(expand-file-name "examples/research-shelf.etaf"
etaf-playground-test--root)))
(unwind-protect
(let ((status
(call-process
program nil output nil "-Q" "--batch"
"-L" etaf-playground-test--root
"-l" package-file
"--eval"
(format
"(progn (find-file %S) (unless (eq major-mode 'etaf-playground-etaf-mode) (kill-emacs 11)) (when (featurep 'etaf) (kill-emacs 12)) (princ \"ETAF source mode clean-load OK\\n\"))"
source-file))))
(unless (zerop status)
(ert-fail
(with-current-buffer output (buffer-string))))
(with-current-buffer output
(should (string-match-p "ETAF source mode clean-load OK"
(buffer-string)))))
(when (buffer-live-p output) (kill-buffer output)))))
(ert-deftest etaf-playground-package-closure-requires-transaction-tp ()
"Package metadata must reject TP snapshots without tp-transaction.el."
(let* ((workspace (file-name-directory
(directory-file-name etaf-playground-test--root)))
(tp (etaf-playground-test--package-description
(expand-file-name "tp/tp.el" workspace)))
(ebox (etaf-playground-test--package-description
(expand-file-name "ebox/ebox.el" workspace)))
(etaf (etaf-playground-test--package-description
(expand-file-name "etaf/etaf.el" workspace)))
(playground (etaf-playground-test--package-description
(expand-file-name
"etaf-playground/etaf-playground.el" workspace))))
(should (equal (package-desc-version tp) '(1 0 1)))
(should (equal (package-desc-version ebox) '(2 0 1)))
(should (member '(tp (1 0 1)) (package-desc-reqs ebox)))
(should (equal (package-desc-version etaf) '(0 1 1)))
(should (member '(ebox (2 0 1)) (package-desc-reqs etaf)))
(should (member '(tp (1 0 1)) (package-desc-reqs etaf)))
(should (equal (package-desc-version playground) '(0 2 1)))
(should (member '(etaf (0 1 1)) (package-desc-reqs playground)))
(should (member '(etaf-ui (0 1 0))
(package-desc-reqs playground)))
(should (member '(etaf-sqlite (0 1 0))
(package-desc-reqs playground)))))
(ert-deftest etaf-playground-stale-tp-error-is-actionable ()
"A stale TP install reports its required package instead of file-missing."
(let ((original-require (symbol-function 'require)))
(cl-letf (((symbol-function 'require)
(lambda (feature &optional filename noerror)
(if (eq feature 'etaf)
(signal 'file-missing
'("Cannot open load file"
"No such file or directory"
"tp-transaction"))
(funcall original-require feature filename noerror)))))
(let ((condition
(should-error (etaf-playground--ensure-runtime)
:type 'etaf-playground-dependency-error)))
(should (equal (plist-get (cdr condition) :required-package)
'(tp "1.0.1")))
(should (eq (plist-get (cdr condition) :missing-feature)
'tp-transaction))))))
(defun etaf-playground-test--text (buffer)
"Return BUFFER's plain rendered text."
(with-current-buffer buffer