fix: make component catalog package reload-safe

This commit is contained in:
Kinneyzhang 2026-09-01 17:07:28 +08:00
parent 4d0d63bbb3
commit 6a1d816292
6 changed files with 55 additions and 20 deletions

View File

@ -52,8 +52,7 @@
:color (plist-get theme color-key)
:font-weight (when (memq variant '(strong heading)) 'bold))))
;;;###autoload
(etaf-define-component etaf-label (&key text variant)
(etaf-ui--define-component etaf-label (&key text variant)
"Render TEXT as a semantic Label."
:view
(text
@ -99,8 +98,7 @@
(funcall callback)))))
state))
;;;###autoload
(etaf-define-component etaf-button
(etaf-ui--define-component etaf-button
(&key label on-press disabled ref class color bgcolor border padding
font-weight tab-index aria-label use variant)
"Render a standard pressable Button with retained callback identity.
@ -154,8 +152,7 @@ across parent Component rerenders."
:bgcolor (plist-get theme bg)
:border (etaf-ui--style-border (plist-get theme border-key)))))
;;;###autoload
(etaf-define-component etaf-checkbox
(etaf-ui--define-component etaf-checkbox
(&key checked label on-change disabled)
"Render a controlled Checkbox whose next value is sent to ON-CHANGE."
:view
@ -187,8 +184,7 @@ across parent Component rerenders."
("&.enabled" :padding (0 1))
(".etaf-checkbox-mark" :font-weight bold :width 1)))
;;;###autoload
(etaf-define-component etaf-panel (&key title variant)
(etaf-ui--define-component etaf-panel (&key title variant)
"Render a titled Panel with named header and default slots."
:render
(let ((theme (etaf-ui--style-tokens
@ -216,8 +212,7 @@ across parent Component rerenders."
("&" :padding (1 2))
(".etaf-panel-title" :font-weight bold)))
;;;###autoload
(etaf-define-component etaf-number-input
(etaf-ui--define-component etaf-number-input
(&key value label on-change disabled min max)
"Render a controlled minibuffer-backed NumberInput using Button."
:render

View File

@ -240,7 +240,7 @@ LOADING-LABEL, ERROR-LABEL, and EMPTY-LABEL override the state text."
(when (eq kind 'error) "etaf-data-grid-error")
(and theme (plist-get theme :ui-data-grid-error-fg)))))
(etaf-define-component etaf-ui--data-grid-body-item
(etaf-ui--define-component etaf-ui--data-grid-body-item
(&key controller entry columns row-ref on-row-press row-selected-p
grid-state)
"Render one retained keyed DataGrid ENTRY with a cached row action."
@ -252,8 +252,7 @@ LOADING-LABEL, ERROR-LABEL, and EMPTY-LABEL override the state text."
(etaf-ui--style-tokens
:ui-table-border :ui-table-selected-fg :ui-table-selected-bg))))
;;;###autoload
(etaf-define-component etaf-data-grid
(etaf-ui--define-component etaf-data-grid
(&key controller columns row-key on-row-press row-ref row-selected-p
loading-label error-label empty-label)
"Render DATA CONTROLLER state through the public Component DSL.
@ -297,8 +296,7 @@ actions; failed candidates cannot mutate committed handler inputs."
:grid-state (etaf-state)))
(slot :name 'footer)))
;;;###autoload
(etaf-define-component etaf-pagination
(etaf-ui--define-component etaf-pagination
(&key controller previous-ref next-ref class color bgcolor border padding
aria-label)
"Render a controlled pager for DATA CONTROLLER with retained controls."

View File

@ -12,6 +12,29 @@
(require 'cl-lib)
(require 'etaf)
(defvar etaf-ui--component-definition-signatures
(make-hash-table :test #'eq)
"Exact catalog definitions already installed in the current process.")
(defmacro etaf-ui--define-component (name arguments &rest clauses)
"Define catalog Component NAME once for exact ARGUMENTS and CLAUSES.
`package.el' may reload dependency files after byte compilation. Identical
catalog definitions are idempotent across that reload, while a changed
definition still reaches `etaf-define-component' and therefore requires the
public `etaf-component-redefine-run' boundary."
(declare (indent 2) (debug defun))
(let ((signature
(secure-hash 'sha256
(prin1-to-string (list name arguments clauses)))))
`(unless (equal
(gethash ',name etaf-ui--component-definition-signatures)
,signature)
(prog1
(etaf-define-component ,name ,arguments ,@clauses)
(puthash ',name ,signature
etaf-ui--component-definition-signatures)))))
(defconst etaf-ui--style-palette
'(:ui-fg "#252A2E"
:ui-bg "#FFFDF8"

View File

@ -202,7 +202,7 @@ ROW-REF, ON-ROW-PRESS, and ROW-SELECTED-P define optional interaction."
(lambda () (funcall callback row-value))))
(etaf-ui--table-row-children row columns))))
(etaf-define-component etaf-ui--table-header (&key columns)
(etaf-ui--define-component etaf-ui--table-header (&key columns)
"Render one retained Table header."
:render (etaf-ui--table-header columns)
:styles
@ -210,7 +210,7 @@ ROW-REF, ON-ROW-PRESS, and ROW-SELECTED-P define optional interaction."
(".etaf-table-header" :font-weight bold :padding (0 1))
(".etaf-table-header-cell" :font-weight bold)))
(etaf-define-component etaf-ui--table-row
(etaf-ui--define-component etaf-ui--table-row
(&key row identity columns row-ref on-row-press row-selected-p)
"Render one retained Table ROW with stable IDENTITY."
:render
@ -220,8 +220,7 @@ ROW-REF, ON-ROW-PRESS, and ROW-SELECTED-P define optional interaction."
(styles
(".etaf-table-row" :padding (0 1))))
;;;###autoload
(etaf-define-component etaf-table
(etaf-ui--define-component etaf-table
(&key columns rows row-key row-ref on-row-press row-selected-p)
"Render ordinary ROWS as a presentational Table.

View File

@ -106,7 +106,9 @@ it is not a claim that every attribute is a declared business prop.")
"\\`etaf-ui-.*\\.el\\'"))
(dolist (entry (etaf-ui-m0a--read-top-level-forms file))
(let ((form (plist-get entry :form)))
(when (and (eq (car-safe form) 'etaf-define-component)
(when (and (memq (car-safe form)
'(etaf-define-component
etaf-ui--define-component))
(symbolp (cadr form))
(not (string-prefix-p "etaf-ui--"
(symbol-name (cadr form)))))

View File

@ -30,6 +30,24 @@ Use Chinese punctuation when CHINESE-P is non-nil."
(plist-get component :observed-business-props)
(if chinese-p "" ", "))))
(ert-deftest etaf-ui-package-autoloads-never-register-components ()
"Keep package activation from executing Component registration forms."
(dolist (file (directory-files default-directory t "\\.el\\'"))
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(while (re-search-forward "^;;;###autoload[[:space:]]*$" nil t)
(forward-comment (point-max))
(should-not (eq (car-safe (read (current-buffer)))
'etaf-define-component))))))
(ert-deftest etaf-ui-package-reload-keeps-identical-catalog-definitions ()
"Allow package.el to reload exact catalog files without duplicate owners."
(let ((before (etaf-ui-m0a-component-inventory)))
(dolist (file '("etaf-ui-basic.el" "etaf-ui-table.el" "etaf-ui-data.el"))
(load-file (expand-file-name file default-directory)))
(should (equal before (etaf-ui-m0a-component-inventory)))))
(ert-deftest etaf-ui-m0b1-readmes-match-runtime-component-manifest ()
"Keep the README contract aligned with the runtime-derived inventory."
(let* ((components (etaf-ui-m0a-component-inventory))