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 `etaf-playground-register-example` is available when a companion needs a
non-conventional root or feature name. 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 Preview placement uses the standard Emacs `display-buffer` action stored in
`etaf-playground-display-action`. The default is a right side window using `etaf-playground-display-action`. The default is a right side window using
half the frame: half the frame:
@ -56,6 +59,11 @@ examples/research-shelf.ecss # inert style source
examples/research-shelf.el # DATA / THEME / STATE / VIEW / ROOT sections 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 The companion registers `:reload-on-refresh t`, so saving the `.el`, `.etaf`, or
`.ecss` source and refreshing reloads the complete consumer before the next `.ecss` source and refreshing reloads the complete consumer before the next
mount. mount.

View File

@ -18,6 +18,9 @@ ETAF Playground 是通用的应用构建工作区:左侧编辑同一个应用
也会刷新。若 root 或 feature 不遵循命名约定,可在 companion 中调用 也会刷新。若 root 或 feature 不遵循命名约定,可在 companion 中调用
`etaf-playground-register-example` 注册覆盖。 `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 预览位置使用标准 Emacs `display-buffer` action
`etaf-playground-display-action` 配置。默认在右侧使用一半 frame `etaf-playground-display-action` 配置。默认在右侧使用一半 frame
@ -55,6 +58,10 @@ examples/research-shelf.ecss # inert 样式 source
examples/research-shelf.el # DATA / THEME / STATE / VIEW / ROOT 分区 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` 后刷新 该 companion 注册了 `:reload-on-refresh t`;保存 `.el`、`.etaf` 或 `.ecss` 后刷新
source会在下一次 mount 前重新加载完整 consumer。 source会在下一次 mount 前重新加载完整 consumer。

View File

@ -3,8 +3,8 @@
;; SPDX-License-Identifier: GPL-3.0-or-later ;; SPDX-License-Identifier: GPL-3.0-or-later
;; Author: ETAF contributors ;; Author: ETAF contributors
;; Version: 0.2.0 ;; Version: 0.2.1
;; Package-Requires: ((emacs "29.1") (etaf "0.1.0")) ;; Package-Requires: ((emacs "29.1") (etaf "0.1.1") (etaf-sqlite "0.1.0") (etaf-ui "0.1.0"))
;; Keywords: tools, convenience, ui ;; Keywords: tools, convenience, ui
;;; Commentary: ;;; Commentary:
@ -32,7 +32,21 @@
(require 'cl-lib) (require 'cl-lib)
(require 'subr-x) (require 'subr-x)
(require 'button) (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 (defgroup etaf-playground nil
"Authoring workspaces for ETAF applications." "Authoring workspaces for ETAF applications."
@ -86,6 +100,11 @@ frame."
(declare-function ebox-call-with-render-burst (declare-function ebox-call-with-render-burst
"ebox-buffer-backend" (function &rest arguments)) "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 (cl-defstruct (etaf-playground-session
(:constructor etaf-playground--session-create)) (: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 forwarded to `etaf-mount', including an optional initial viewport. Source
loading, root construction, ETAF publication, and Ebox rendering share one loading, root construction, ETAF publication, and Ebox rendering share one
public framework render burst so GC cannot split an interactive mount." public framework render burst so GC cannot split an interactive mount."
(etaf-playground--ensure-runtime)
(ebox-call-with-render-burst (ebox-call-with-render-burst
#'etaf-playground--mount-example-now #'etaf-playground--mount-example-now
buffer-name name session mount-options)) 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))) (let ((preview (etaf-playground-session-preview-buffer session)))
(condition-case err (condition-case err
(progn (progn
(etaf-playground--ensure-runtime)
(when-let* ((runtime (etaf-runtime-for-buffer preview))) (when-let* ((runtime (etaf-runtime-for-buffer preview)))
(with-current-buffer preview (with-current-buffer preview
(etaf-unmount runtime))) (etaf-unmount runtime)))
@ -620,10 +641,12 @@ the preview instead of destroying the source workspace."
(remove-hook 'after-change-functions (remove-hook 'after-change-functions
#'etaf-playground--after-source-change t))) #'etaf-playground--after-source-change t)))
;;;###autoload
(define-derived-mode etaf-playground-etaf-mode emacs-lisp-mode "ETAF" (define-derived-mode etaf-playground-etaf-mode emacs-lisp-mode "ETAF"
"Major mode for inert ETAF structural source files." "Major mode for inert ETAF structural source files."
(etaf-playground-source-mode 1)) (etaf-playground-source-mode 1))
;;;###autoload
(define-derived-mode etaf-playground-ecss-mode emacs-lisp-mode "ECSS" (define-derived-mode etaf-playground-ecss-mode emacs-lisp-mode "ECSS"
"Major mode for inert ETAF ECSS style source files." "Major mode for inert ETAF ECSS style source files."
(etaf-playground-source-mode 1)) (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 buffer-read-only nil)
(setq-local mode-line-process nil)) (setq-local mode-line-process nil))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.etaf\\'" . etaf-playground-etaf-mode)) (add-to-list 'auto-mode-alist '("\\.etaf\\'" . etaf-playground-etaf-mode))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.ecss\\'" . etaf-playground-ecss-mode)) (add-to-list 'auto-mode-alist '("\\.ecss\\'" . etaf-playground-ecss-mode))
(defun etaf-playground--source-header () (defun etaf-playground--source-header ()
@ -984,7 +1009,9 @@ default example there."
(window-frame preview-window))) (window-frame preview-window)))
(configuration (configuration
(etaf-playground-session-previous-window-configuration session))) (etaf-playground-session-previous-window-configuration session)))
(when-let* ((runtime (and (buffer-live-p preview) (when-let* ((runtime
(and (fboundp 'etaf-runtime-for-buffer)
(buffer-live-p preview)
(etaf-runtime-for-buffer preview)))) (etaf-runtime-for-buffer preview))))
(with-current-buffer preview (with-current-buffer preview
(etaf-unmount runtime))) (etaf-unmount runtime)))
@ -1013,7 +1040,8 @@ default example there."
((and (stringp target) (get-buffer target)) ((and (stringp target) (get-buffer target))
(get-buffer target)) (get-buffer target))
((null target) (current-buffer)))) ((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)))) (etaf-runtime-for-buffer buffer))))
(when runtime (when runtime
(with-current-buffer buffer (with-current-buffer buffer

View File

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

View File

@ -8,15 +8,100 @@
;;; Code: ;;; Code:
(require 'cl-lib) (require 'cl-lib)
(require 'ert) (require 'ert)
(require 'package)
(require 'etaf-playground) (require 'etaf-playground)
(require 'etaf-performance) (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 () (defun etaf-playground-test--ensure-app-loaded ()
"Load the same-basename Research Shelf companion for test setup." "Load the same-basename Research Shelf companion for test setup."
(unless (featurep 'etaf-research-shelf) (unless (featurep 'etaf-research-shelf)
(load-file (expand-file-name "examples/research-shelf.el" (load-file (expand-file-name "examples/research-shelf.el"
default-directory)))) 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) (defun etaf-playground-test--text (buffer)
"Return BUFFER's plain rendered text." "Return BUFFER's plain rendered text."
(with-current-buffer buffer (with-current-buffer buffer