@context-action/lit-ui 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ # 웹 컴포넌트 인터페이스 설계 및 상태 관리 표준 규약
2
+ (Web Component Interface Design & Baseline State Management Standards)
3
+
4
+ 이 문서는 Context-Action 프레임워크 생태계(Lit 및 Preact 기반)에서 웹 컴포넌트(Custom Elements)를 제작할 때 준수해야 하는 **공식 인터페이스 설계 규약**과 **기본 상태 관리 원칙**을 정의합니다.
5
+
6
+ > **핵심 철학: 자율성 보장과 기본 원칙의 균형 (Autonomy with Invariants)**
7
+ > 컴포넌트의 내부 렌더링 방식(Lit Template vs Preact Signals), 스타일링, 마크업 구조는 개발자에게 완전한 자유도를 부여하되, **외부로 노출되는 인터페이스(Interface Exposure)**와 **도메인 상태 연동(State Management)**에 대해서는 명확한 계약(Contract)을 확립하여 예측 가능하고 메모리 누수가 없는 아키텍처를 유지합니다.
8
+
9
+ ---
10
+
11
+ ## 1. 비타협적 4대 기본 원칙 (Non-Negotiable Invariants)
12
+
13
+ ```text
14
+ ┌─────────────────────────────────────────────────────────────────────────────┐
15
+ │ 4대 대원칙 (Core Invariants) │
16
+ ├─────────────────────────────────────────────────────────────────────────────┤
17
+ │ 1. SSOT (단일 진실 원천) : 도메인 상태를 독자 복제하지 않고 Store/Signal을 구독 │
18
+ │ 2. 속성/프로퍼티 이분법 : Primitive만 Attribute로, Complex/Store는 Property로 │
19
+ │ 3. 무누수 라이프사이클 : Disconnect 시 동기적 완전 해제, Reconnect 시 재동기화 │
20
+ │ 4. 웹 표준 프로토콜 준수 : composed 커스텀 이벤트 및 ElementInternals(FACE) 준수 │
21
+ └─────────────────────────────────────────────────────────────────────────────┘
22
+ ```
23
+
24
+ 1. **SSOT (Single Source of Truth) 준수**: 도메인 상태(Domain State)를 엘리먼트 내부에 독자 복제하여 원천 상태와 어긋나는 상태 불일치를 유발하지 않습니다. 도메인 데이터는 Context-Action Store나 공유 Signal이 유일한 원천이며, 컴포넌트는 이를 투영(Projection)하여 읽기만 합니다.
25
+ 2. **속성과 프로퍼티의 엄격한 분리 (Attribute vs Property)**: 프리미티브 값만 HTML 속성으로 리플렉션하고, Store, ActionRegister, Model 객체, 함수 등 복합 객체는 **절대로** HTML 속성으로 취급하지 않고 DOM 프로퍼티(`attribute: false`)로만 수용합니다.
26
+ 3. **결정론적 수명주기 및 메모리 누수 0 (Zero Memory Leak)**: 엘리먼트가 DOM에서 분리(`disconnectedCallback`)되면 등록된 모든 구독, 리스너, 진행 중인 비동기 요청(`AbortSignal`)이 동기적으로 완전 해제되어야 합니다. DOM에 재부착(`connectedCallback`)될 때는 최신 스냅샷으로 깨끗하게 재동기화되어야 합니다.
27
+ 4. **웹 표준 프로토콜 준수 (Composed Events & FACE)**: 외부로 알리는 이벤트는 `composed: true`로 Shadow DOM을 통과할 수 있어야 하며, 폼 입력 성격의 컴포넌트는 `ElementInternals` 기반의 Form-Associated Custom Element (FACE) 표준을 준수해야 합니다.
28
+
29
+ ---
30
+
31
+ ## 2. 인터페이스 작성 규약 (Interface Authoring Standards)
32
+
33
+ ### 2.1 속성(Attribute) vs 프로퍼티(Property) 분리
34
+
35
+ | 데이터 유형 | 전달 방식 | Lit 데코레이터 예시 | Preact 팩토리 예시 | 네이밍 규칙 |
36
+ |---|---|---|---|---|
37
+ | **문자열, 숫자, 불리언** | Attribute + Property 양방향 리플렉션 | `@property({ type: Number })` | `observedAttributes: ['min', 'max']` | Attribute: `kebab-case`<br>Property: `camelCase` |
38
+ | **불리언 플래그** | Presence / Absence (참일 때 속성 존재, 거짓일 때 속성 제거) | `@property({ type: Boolean, reflect: true })` | `observedAttributes: ['disabled']` | `disabled`, `readonly`, `hide-zero` |
39
+ | **Store, ActionRegister** | **순수 DOM 프로퍼티 전용 (속성 금지)** | `@property({ attribute: false })` | 인스턴스 프로퍼티 또는 `setup` 파라미터 | `store`, `register` |
40
+ | **객체, 배열, 콜백 함수** | **순수 DOM 프로퍼티 전용 (속성 금지)** | `@property({ attribute: false })` | 인스턴스 프로퍼티 | `items`, `selector`, `equalityFn` |
41
+
42
+ #### ⚠️ 안티패턴 vs 모범사례
43
+ ```ts
44
+ // ❌ 안티패턴: 복합 객체를 속성으로 선언 (HTML 마크업에서 "[object Object]" 문자열화 버그 발생)
45
+ @property({ type: Object })
46
+ cartStore?: ReadableStore<CartState>;
47
+
48
+ // ✅ 올바른 표준: 복합 객체는 반드시 attribute: false로 지정
49
+ @property({ attribute: false })
50
+ cartStore?: ReadableStore<CartState>;
51
+ ```
52
+
53
+ ---
54
+
55
+ ### 2.2 이벤트(CustomEvent) 노출 규약
56
+
57
+ 1. **이벤트 명칭**:
58
+ - 네이티브 시맨틱 호환 이벤트: `input`, `change`, `focus`, `blur`
59
+ - 도메인 전용 커스텀 이벤트: `kebab-case` 명사-동사/상태 조합 (예: `quantity-change`, `cart-badge-click`, `order-submit`)
60
+ 2. **이벤트 디테일 타입 명시 (Strictly Typed Payload)**:
61
+ - 모든 커스텀 이벤트는 소비자가 TypeScript로 안전하게 타입을 추론할 수 있도록 `export interface XxxEventDetail` 규격을 함께 export합니다.
62
+ 3. **버블링 및 섀도우 DOM 통과 (Composed & Bubbles)**:
63
+ - 외부 섀도우 루트 및 호스트 문서 조상 엘리먼트가 이벤트를 수신할 수 있도록 `{ bubbles: true, composed: true }`를 필수 지정합니다.
64
+
65
+ ```ts
66
+ // 1. 이벤트 페이로드 인터페이스 수출
67
+ export interface QuantityChangeEventDetail {
68
+ value: number;
69
+ previousValue?: number;
70
+ }
71
+
72
+ // 2. 표준 CustomEvent 디스패치
73
+ private emitQuantityChange(nextVal: number): void {
74
+ this.dispatchEvent(
75
+ new CustomEvent<QuantityChangeEventDetail>('quantity-change', {
76
+ bubbles: true,
77
+ composed: true,
78
+ detail: { value: nextVal, previousValue: this.value }
79
+ })
80
+ );
81
+ }
82
+ ```
83
+
84
+ ---
85
+
86
+ ### 2.3 명령형 인스턴스 메서드 (Imperative APIs)
87
+
88
+ 컴포넌트 외부에서 스크립트나 ref를 통해 호출할 수 있는 공용 메서드는 다음 표준을 따릅니다:
89
+
90
+ 1. **명시적 가시성 (Public Exposure)**:
91
+ - 클래스 기반(Lit)의 경우 `public` 메서드로 선언합니다.
92
+ - Preact(`@context-action/preact-ui`)의 경우 `definePreactElement<Input, Expose>`의 `setup()` 반환 객체에서 `expose: { ... }` 프로퍼티 디스크립터로 선언적으로 노출합니다 (`Object.defineProperties` 수동 호출이나 `as unknown as` 캐스팅 불필요, 예약된 `HTMLElement`/FACE 키 충돌 자동 차단).
93
+ 2. **부수 효과 제어 옵션 (Options Pattern)**:
94
+ - 프로그래밍 방식으로 값을 변경할 때 이벤트 발생 여부를 호출자가 제어할 수 있도록 옵션 파라미터를 제공합니다.
95
+ 3. **표준 폼 유효성 메서드**:
96
+ - `checkValidity()`, `reportValidity()`, `setCustomValidity(msg)`를 표준 구현하여 브라우저 내장 폼 검증과 완벽히 일치시킵니다.
97
+
98
+ ```ts
99
+ // 명령형 메서드 작성 표준
100
+ public setValue(nextValue: number, options?: { dispatchEvents?: boolean }): void {
101
+ this.value = nextValue;
102
+ this.syncFormState();
103
+ this.requestUpdate();
104
+
105
+ if (options?.dispatchEvents) {
106
+ this.emitQuantityChange(nextValue);
107
+ }
108
+ }
109
+ ```
110
+
111
+ ---
112
+
113
+ ### 2.4 React 18/19 호스트 연동 브리지 (`createLitElementBridge` / `createCustomElementBridge`)
114
+
115
+ React 호스트 환경에서 웹 컴포넌트를 소비할 때는 리액트의 속성 직렬화 한계(`[object Object]`)를 우회하는 브리지를 제공해야 합니다. 특히 부모 컴포넌트가 `ref`를 사용할 때 원시 `HTMLElement`나 `shadowRoot`가 노출되지 않도록 `createHandle` / `createElementFacade`를 통해 OOP 도메인 핸들만 캡슐화하여 노출할 수 있습니다.
116
+
117
+ ```tsx
118
+ // 컴포넌트 브리지 정의 표준
119
+ export const ReactQuantityStepper = createLitElementBridge<StepperProps, LitQuantityStepper>({
120
+ tagName: 'lit-quantity-stepper',
121
+ // 1. DOM 프로퍼티로 직할 할당할 복합 속성 목록
122
+ properties: ['store', 'register', 'selector'],
123
+ // 2. React onXxx 핸들러 ↔ DOM CustomEvent 이름 매핑
124
+ events: {
125
+ onQuantityChange: 'quantity-change',
126
+ },
127
+ });
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 3. 기본 상태 관리 표준 규칙 (State Management Standards)
133
+
134
+ 상태의 성격과 범위에 따라 **3가지 티어(Tier)**로 엄격히 구분하여 관리합니다.
135
+
136
+ ```text
137
+ ┌────────────────────────────────────────────────────────────────────────┐
138
+ │ 3-Tier State Hierarchy (상태 3계층 모델) │
139
+ ├────────────────────────────────────────────────────────────────────────┤
140
+ │ [Tier 1] Local UI Transient State : 포커스, 팝오버 오픈, 애니메이션 플래그│
141
+ │ → Lit @state(), Preact local signal (엘리먼트 내부 소멸 가능) │
142
+ ├────────────────────────────────────────────────────────────────────────┤
143
+ │ [Tier 2] Component Controlled Value : input value, disabled, validity │
144
+ │ → @property() + ElementInternals.setFormValue() (폼 리셋 복원)│
145
+ ├────────────────────────────────────────────────────────────────────────┤
146
+ │ [Tier 3] Domain Business State : 장바구니, 유저 정보, 주문 상태, 테마 │
147
+ │ → Context-Action ReadableStore / Global Signal (SSOT 원천) │
148
+ │ → StoreController / connectSourceSignal 로 선택적 프로젝션 구독 │
149
+ └────────────────────────────────────────────────────────────────────────┘
150
+ ```
151
+
152
+ ### 3.1 Tier 1: 로컬 UI 일시 상태 (Local UI Transient State)
153
+ * **정의**: 컴포넌트 내부 렌더링에만 관여하며, 컴포넌트가 사라지면 버려져도 무방한 상태 (예: `focused`, `isOpen`, `hovered`).
154
+ * **구현**: Lit의 `@state()` 또는 Preact의 `signal()`.
155
+ * **원칙**: 이 상태를 상위 Store나 전역 상태로 끌어올리지(lifting) 않습니다.
156
+
157
+ ### 3.2 Tier 2: 컴포넌트 제어 값 (Component Controlled Value)
158
+ * **정의**: 컴포넌트가 UI 컨트롤로서 보유하는 고유 값 (예: `value`, `min`, `max`, `defaultValue`).
159
+ * **구현**: `@property()` 및 `ElementInternals.setFormValue()`.
160
+ * **원칙**:
161
+ - `connectedCallback` 시 `defaultValue` 또는 초기 `value`를 캡처해 둡니다.
162
+ - 부모 `<form>`이 리셋될 때 `onFormReset()` 콜백에서 초기값으로 정확히 복원합니다.
163
+
164
+ ### 3.3 Tier 3: 도메인/비즈니스 상태 (Domain Business State)
165
+ * **정의**: 여러 컴포넌트가 공유하거나 백엔드/스토어와 동기화되는 핵심 비즈니스 데이터.
166
+ * **구현**: **Context-Action `ReadableStore`** 또는 **공유 `ReadonlySignal`**.
167
+ * **원칙**:
168
+ 1. **절대 복제 금지**: 컴포넌트 내부에 별도의 로컬 state를 만들어 스토어 데이터를 복제하지 않습니다.
169
+ 2. **선택적 프로젝션 (Selective Projection)**: 스토어 전체를 구독하지 않고, 컴포넌트에 필요한 슬라이스만 `selector` 함수로 추출합니다.
170
+ 3. **Equality Guard**: 프로젝션 결과가 이전과 동일하다면 `equalityFn`을 통해 `requestUpdate()`를 억제하여 고주파(120fps) 상태 변경에서도 렌더링 오버헤드를 0으로 유지합니다.
171
+ 4. **Coalesced Pending**: `ActionController`를 통해 액션 진행 상태(`isPending`)를 추적하되, 리렌더링 요청은 **0 ↔ >0 전환 시점**에만 병합 실행합니다.
172
+
173
+ ---
174
+
175
+ ## 4. 자율성 보장 영역 vs 제한 영역 가이드
176
+
177
+ 개발자가 창의성을 발휘할 수 있는 자율 영역과 반드시 지켜야 하는 제한 영역의 경계는 다음과 같습니다:
178
+
179
+ ```text
180
+ 자유도 보장 영역 (Developer Freedom)
181
+ ─────────────────────────────────────────────────────────────────────────────
182
+ ✔ 렌더링 라이브러리 선택 (Lit vs Preact vs Vanilla)
183
+ ✔ 컴포넌트 내부 스타일링 (CSS Custom Properties, Shadow CSS, ::part)
184
+ ✔ 슬롯(Slot)을 활용한 자식 노드 컴포지션 구조
185
+ ✔ 내부 계산용 유틸리티 및 순수 비즈니스 함수 구성
186
+ ✔ 컴포넌트 내부 프라이빗 헬퍼 메서드 및 상태 모델링
187
+
188
+ 필수 제한 영역 (Non-Negotiable Contracts)
189
+ ─────────────────────────────────────────────────────────────────────────────
190
+ ✘ 복합 객체(Store/Register)를 HTML Attribute로 선언하는 행위 금지
191
+ ✘ disconnectedCallback 시 구독/리스너를 해제하지 않아 메모리 누수를 내는 행위 금지
192
+ ✘ 외부 알림 이벤트를 composed: false로 발송하여 Shadow DOM에 가두는 행위 금지
193
+ ✘ 도메인 스토어의 값을 컴포넌트 로컬 상태로 복제하여 SSOT를 깨뜨리는 행위 금지
194
+ ✘ Form-associated 컴포넌트가 Form Reset 수명주기를 무시하는 행위 금지
195
+ ```
196
+
197
+ ---
198
+
199
+ ## 5. 자체 점검 체크리스트 (Self-Audit Checklist)
200
+
201
+ 새로운 웹 컴포넌트를 제작하거나 PR을 검토할 때 아래 8개 항목을 확인하세요:
202
+
203
+ - [ ] **Attr/Prop 분리**: 프리미티브만 속성으로 연결되어 있고, Store/객체는 `attribute: false`로 정의되었는가?
204
+ - [ ] **SSOT 유지**: 도메인 데이터를 컴포넌트 내부에 중복 저장하지 않고 `StoreController` / `Signal`로 직접 프로젝션하는가?
205
+ - [ ] **프로젝션 최적화**: 필요한 데이터만 `selector`로 추출하고 불필요한 리렌더를 `equalityFn`으로 방어하고 있는가?
206
+ - [ ] **메모리 누수 차단**: `disconnectedCallback` 시 스토어 구독, DOM 이벤트, 비동기 작업이 완전하게 클린업되는가?
207
+ - [ ] **재연결 안전성**: DOM에서 뗐다가 다시 붙였을 때(`connectedCallback`) 최신 상태로 정상 동기화되는가?
208
+ - [ ] **이벤트 규격**: 커스텀 이벤트가 `{ bubbles: true, composed: true }`이며 전용 `detail` 타입을 함께 export하는가?
209
+ - [ ] **FACE 표준**: 폼 컨트롤 엘리먼트인 경우 `ElementInternals`에 폼 값과 유효성을 반영하고 `onFormReset`을 지원하는가?
210
+ - [ ] **React 브리지 제공**: React 호스트 소비자를 위한 `createLitElementBridge` / `createCustomElementBridge`를 제공하는가?
@@ -0,0 +1,161 @@
1
+ # 06. SSR과 Hydration Entry
2
+
3
+ 이 문서는 `@context-action/lit-ui/ssr`과 `@context-action/lit-ui/hydrate`의 공개 계약을 정의합니다.
4
+ Lit의 declarative shadow DOM(DSD), `part`/`data-*` 계약, 반사된 reactive property, light DOM slot projection을
5
+ 서버와 클라이언트에서 동일하게 유지하는 것이 목표입니다.
6
+
7
+ ## 1. Entry 분리
8
+
9
+ | Entry | 역할 | 서버 renderer 포함 |
10
+ | --- | --- | --- |
11
+ | `@context-action/lit-ui/ssr` | SSR render + `hydrateLit` re-export | 예 |
12
+ | `@context-action/lit-ui/hydrate` | client hydration 전용 | 아니오 |
13
+
14
+ 클라이언트 번들 크기를 줄이려면 hydration만 필요한 코드는 반드시 `./hydrate`를 사용합니다.
15
+
16
+ ```ts
17
+ // Server
18
+ import { renderLitElement, renderLitTemplate } from '@context-action/lit-ui/ssr';
19
+
20
+ // Client — Lit element 정의보다 먼저 import
21
+ import { hydrateLit } from '@context-action/lit-ui/hydrate';
22
+ ```
23
+
24
+ `./hydrate`는 `@lit-labs/ssr-client/lit-element-hydrate-support.js`를 자동으로 로드합니다.
25
+ Lit hydration support는 **LitElement 정의 전에 로드되어야** 하므로, 클라이언트 entry의 첫 import로 두는 것을 권장합니다.
26
+
27
+ ## 2. SSR API
28
+
29
+ ```ts
30
+ renderLitElement(element: LitElement, options?: RenderLitElementOptions): string
31
+ renderLitTemplate(template: TemplateResult): string
32
+ renderLitElementAsync(element: LitElement, options?: RenderLitElementOptions): Promise<string>
33
+ renderLitTemplateAsync(template: TemplateResult): Promise<string>
34
+ ```
35
+
36
+ `RenderLitElementOptions`:
37
+
38
+ | 옵션 | 기본값 | 설명 |
39
+ | --- | --- | --- |
40
+ | `properties` | `static elementProperties` | SSR에 property binding으로 전달할 reactive property 목록 |
41
+ | `includeAttributes` | `true` | element의 현재 HTML attribute를 host markup에 포함 |
42
+ | `includeLightDom` | `true` | light DOM children을 host 내부에 직렬화해 `<slot>` projection 유지 |
43
+
44
+ ### 2-1. Element SSR
45
+
46
+ `renderLitElement`는 element instance를 그대로 Lit SSR에 넘기지 않습니다.
47
+ Lit은 raw instance를 문자열화하므로, reactive property를 `.property=${value}` binding으로 재구성한
48
+ TemplateResult를 Lit ElementRenderer에 전달합니다.
49
+
50
+ - `part`, `data-*`, adopted styles, declarative shadow root가 유지됩니다.
51
+ - 복합 객체는 attribute로 직렬화되지 않습니다.
52
+ - `reflect: true` property는 host attribute로 합성됩니다.
53
+ - Boolean `true` → `disabled=""`
54
+ - Boolean `false` → attribute 생략
55
+ - Number/String → `String(value)`
56
+ - custom `converter.toAttribute` 지원
57
+ - element는 `customElements.define()`으로 등록되어 있어야 합니다.
58
+
59
+ ### 2-2. Light DOM과 Slot
60
+
61
+ ```ts
62
+ const element = new TimelineElement();
63
+ element.innerHTML = '<span slot="suffix">Server suffix</span>';
64
+
65
+ const markup = renderLitElement(element);
66
+ ```
67
+
68
+ `includeLightDom: false`로 명시적으로 제외할 수 있습니다.
69
+ light DOM은 서버 신뢰 입력으로 취급하며, nested custom element의 복합 property는 별도 SSR 경로가 필요합니다.
70
+
71
+ ## 3. Hydration API
72
+
73
+ ```ts
74
+ hydrateLit(root: Element | ShadowRoot, element: LitElement): void
75
+ ```
76
+
77
+ 지원 모드:
78
+
79
+ 1. `root`에 `renderLitTemplate`의 lit-part marker가 있는 경우
80
+ - `hydrate(element.render(), root)`로 기존 DOM을 채택합니다.
81
+ - 같은 root를 두 번 hydrate하면 live part를 감지해 `requestUpdate()`만 수행합니다.
82
+ 2. `root` 안에 같은 tag의 server-rendered host가 있는 경우
83
+ - Lit hydration support가 있으면 첫 update에서 DSD를 채택합니다.
84
+ - support가 없으면 수동으로 기존 shadow root를 hydrate합니다.
85
+ - 기존 `_$litPart$`가 있으면 재hydrate하지 않습니다.
86
+ 3. element 자신의 shadow root를 직접 넘기는 경우
87
+ - shadow root 전체에서 `lit-part` comment marker를 찾습니다. `static styles`로
88
+ 첫 자식이 `<style>`이어도 그 뒤/하위의 marker를 인식합니다.
89
+ - 기존 server tree를 manual hydrate하고 hydration support flag를 해제해
90
+ double hydration을 막습니다.
91
+ - 첫 client update가 이미 별도 tree를 append한 경우 server root part만
92
+ 분리하고 client tree를 제거한 뒤 server tree를 다시 채택합니다.
93
+ 4. `root`가 비어 있는 경우
94
+ - element를 mount하고 일반 client render를 수행합니다.
95
+
96
+ ### Static styles와 marker 탐색
97
+
98
+ `hasLitPartMarker`는 direct sibling만 보지 않고 `TreeWalker`로 root 아래의
99
+ comment node를 순회합니다. 따라서 다음 markup에서도 hydration 경로를 탑니다.
100
+
101
+ ```html
102
+ <template shadowrootmode="open">
103
+ <style>...</style>
104
+ <!--lit-part ...-->
105
+ <div class="tl-item">...</div>
106
+ <!--/lit-part-->
107
+ </template>
108
+ ```
109
+
110
+ ### 이미 중복된 client tree 복구
111
+
112
+ `hydrateLit` 호출 전에 LitElement의 첫 update가 실행되어 server tree 옆에
113
+ client tree가 append된 경우, `_$litPart$`가 server root part인지 확인합니다.
114
+ 별도 client render라면 server root marker 범위만 남기고 client anchor/children을
115
+ 제거한 뒤 `hydrate()`로 다시 채택합니다. 서버 tree를 분리할 수 없으면 shadow
116
+ root를 비우고 client render로 fallback합니다.
117
+
118
+ ### Import 순서
119
+
120
+ ```ts
121
+ import { hydrateLit } from '@context-action/lit-ui/hydrate';
122
+ import { TimelineElement } from './timeline-element.js';
123
+
124
+ const host = document.getElementById('timeline-host')!;
125
+ hydrateLit(host, new TimelineElement());
126
+ ```
127
+
128
+ LitElement가 이미 로드된 뒤 `hydrate` entry를 import하면 hydration support가 자동 적용되지 않을 수 있습니다.
129
+ 이 경우 `hydrateLit`은 manual hydration fallback을 사용하지만, 최상의 DSD 채택을 위해 entry를 먼저 import합니다.
130
+
131
+ ## 4. 계약과 검증
132
+
133
+ 보장하는 계약:
134
+
135
+ - server markup과 client hydration 사이에 `part`, `data-*`가 유지됩니다.
136
+ - DSD shadow root를 재사용하며 중복 shadow root를 만들지 않습니다.
137
+ - hydration 후 이벤트와 ReactiveController가 client에서 연결됩니다.
138
+ - reflected host attribute로 초기 상태가 유지됩니다.
139
+ - light DOM slot projection이 유지됩니다.
140
+ - SSR entry는 `window`/`document` 없이 Node/Edge/Worker에서 evaluate됩니다.
141
+ - ESM/CJS 양쪽에서 import됩니다.
142
+
143
+ 검증 명령:
144
+
145
+ ```sh
146
+ pnpm --filter @context-action/lit-ui test
147
+ pnpm --filter @context-action/lit-ui test:edge
148
+ pnpm --filter @context-action/lit-ui test:browser
149
+ pnpm verify:package-exports
150
+ pnpm verify:package-tarballs
151
+ pnpm verify:lit-packed-consumer
152
+ ```
153
+
154
+ `test:browser`는 Chromium에서 실제 DSD markup을 hydrate하고 controller/event/slot/part/data 계약을 확인합니다.
155
+
156
+ ## 5. 제약
157
+
158
+ - `renderLitTemplate`의 동기 버전은 Promise가 포함된 renderer chunk를 지원하지 않습니다. 이 경우 async variant를 사용합니다.
159
+ - `renderLitElement`는 Lit 내부 TemplateResult 표현을 사용합니다. Lit major 업그레이드 시 SSR unit/browser contract가 fail-fast 안전장치입니다.
160
+ - streaming SSR은 현재 제공하지 않습니다.
161
+ - light DOM의 nested custom element 복합 property는 자동 직렬화되지 않습니다.
@@ -0,0 +1,168 @@
1
+ # 07. 공개 릴리스와 검증
2
+
3
+ 이 문서는 `@context-action/lit@0.1.0`, `@context-action/lit-ui@0.1.0` 공개 preview의
4
+ 검증 계약과 보호 배포 경로를 설명합니다. 릴리스 계획의 source of truth는
5
+ [`releases/LIT_PUBLIC_0.1.0.md`](../../../releases/LIT_PUBLIC_0.1.0.md)와
6
+ [`releases/lit-public-2026-10.json`](../../../releases/lit-public-2026-10.json)입니다.
7
+
8
+ ## 0. 게시 상태
9
+
10
+ 두 패키지는 2026-10-23 npm에 게시되었습니다.
11
+
12
+ | 패키지 | 버전 | latest | next |
13
+ | --- | --- | --- | --- |
14
+ | `@context-action/lit` | `0.1.0` | `0.1.0` | `0.1.0` |
15
+ | `@context-action/lit-ui` | `0.1.0` | `0.1.0` | `0.1.0` |
16
+
17
+ - Candidate: `publish-lit-public-candidate.yml` — 성공
18
+ - Promotion: `promote-lit-public.yml` — 성공
19
+ - Registry consumer smoke — 25 assertions 통과
20
+
21
+ ### 0.1.1 patch candidate
22
+
23
+ `@context-action/lit-ui@0.1.1`은 static styles DSD hydration과 중복 client tree
24
+ 복구 patch입니다. `@context-action/lit@0.1.0`은 재사용하며, candidate/promotion
25
+ workflow는 기존 lit artifact의 attested commit과 새 lit-ui commit을 함께
26
+ 검증합니다. 배포 완료 후 이 절의 상태를 갱신합니다.
27
+
28
+ ## 1. 패키지 경계
29
+
30
+ ```text
31
+ @context-action/lit@0.1.0
32
+ └─ @context-action/core@^1.2.6
33
+
34
+ @context-action/lit-ui@0.1.0
35
+ ├─ @context-action/core@^1.2.6
36
+ └─ @context-action/lit@^0.1.0
37
+ ```
38
+
39
+ 선택적 peer:
40
+
41
+ - `lit@^3.2.0`
42
+ - `@lit/context@^1.1.0`
43
+ - `react@^18 || ^19` (bridge 전용)
44
+ - `@lit-labs/ssr@^4.1.0` (`./ssr`)
45
+ - `@lit-labs/ssr-client@^1.1.8` (`./ssr`, `./hydrate`)
46
+
47
+ 공개 패키지는 `private: false`, `publishConfig.access=public`, Apache-2.0 LICENSE,
48
+ CHANGELOG, repository metadata를 포함합니다.
49
+
50
+ ## 2. 로컬 검증
51
+
52
+ ```sh
53
+ pnpm install --frozen-lockfile
54
+ pnpm verify:lit-public-release -- --require-current-source
55
+ pnpm verify:lit-public-workflows
56
+ pnpm check:lit
57
+ pnpm verify:lit-react-compatibility
58
+ pnpm --filter @context-action/lit-ui test:browser
59
+ pnpm verify:package-exports
60
+ pnpm verify:package-tarballs
61
+ pnpm verify:lit-packed-consumer
62
+ pnpm package-boundary:check
63
+ pnpm release:check
64
+ ```
65
+
66
+ | 명령 | 확인 내용 |
67
+ | --- | --- |
68
+ | `verify:lit-public-release` | plan/manifest/version/dependency/CHANGELOG/Lerna 일치 |
69
+ | `verify:lit-public-workflows` | candidate/promotion guard, input, publish order |
70
+ | `check:lit` | build, type-check, unit, edge runtime |
71
+ | `verify:lit-react-compatibility` | React 18 consumer matrix |
72
+ | `test:browser` | Chromium DSD hydration/event/controller/part/data |
73
+ | `verify:package-exports` | ESM/CJS/types export contract |
74
+ | `verify:package-tarballs` | LICENSE/README/CHANGELOG/archive target |
75
+ | `verify:lit-packed-consumer` | tarball 설치, SSR, hydrate export, 소비자 타입 |
76
+
77
+ ## 3. 보호 배포
78
+
79
+ ### Candidate
80
+
81
+ Workflow: `publish-lit-public-candidate.yml`
82
+
83
+ ```text
84
+ inputs:
85
+ release_commit: 승인된 immutable main SHA
86
+ publish_auth: token | oidc
87
+ environment: npm-stable
88
+ candidate tag: next
89
+ ```
90
+
91
+ Candidate는 다음을 fail-closed로 수행합니다.
92
+
93
+ 1. release commit이 main의 승인된 commit인지 확인
94
+ 2. Lit release plan과 workflow contract 검증
95
+ 3. release gate, Lit build/type/test/edge/browser
96
+ 4. package exports/tarballs/packed consumer/boundary
97
+ 5. publication 직전 source 불변 재검증
98
+ 6. `@context-action/lit` → `@context-action/lit-ui` 순서로 `next` 게시
99
+ 7. provenance, published consumer smoke, registry evidence
100
+
101
+ ### 첫 배포 bootstrap (완료)
102
+
103
+ 0.1.0은 두 패키지가 npm에 없던 상태였으므로 `publish_auth=token`과 `npm-stable`의
104
+ `NPM_TOKEN`으로 bootstrap했습니다.
105
+
106
+ 이제 두 package version이 registry에 존재합니다. npm trusted publisher가 이
107
+ workflow/environment에 구성된 경우 후속 릴리스는 `publish_auth=oidc`를 사용할 수
108
+ 있습니다. Candidate workflow는 OIDC 선택 시 package 존재 여부를 확인하고 없으면
109
+ fail-closed로 중단합니다. `NPM_TOKEN`은 bootstrap/recovery 경로로 유지합니다.
110
+
111
+ ### Promotion
112
+
113
+ Workflow: `promote-lit-public.yml`
114
+
115
+ ```text
116
+ inputs:
117
+ release_commit: candidate와 동일한 SHA
118
+ confirmation: PROMOTE_LIT_PUBLIC
119
+ environment: npm-stable
120
+ promotion tag: latest
121
+ ```
122
+
123
+ Promotion은 다음을 검증합니다.
124
+
125
+ 1. `next` provenance가 release commit과 candidate workflow에 결속
126
+ 2. published consumer matrix
127
+ 3. 이전 `latest` snapshot
128
+ 4. `lit` → `lit-ui` 순서로 `latest` 승격
129
+ 5. `latest` consumer/provenance/evidence 재검증
130
+ 6. 실패 시 candidate가 아직 `latest`일 때만 이전 `latest`로 rollback
131
+
132
+ ## 4. 증거와 rollback
133
+
134
+ Candidate evidence:
135
+
136
+ ```text
137
+ reports/npm-lit-public-candidate-summary.json
138
+ reports/npm-lit-public-candidate-provenance.json
139
+ reports/npm-lit-public-candidate-registry-evidence.json
140
+ ```
141
+
142
+ Promotion evidence:
143
+
144
+ ```text
145
+ reports/npm-lit-public-promotion-preflight-provenance.json
146
+ reports/npm-lit-public-promotion-summary.json
147
+ reports/npm-lit-public-promotion-provenance.json
148
+ reports/npm-lit-public-promotion-registry-evidence.json
149
+ ```
150
+
151
+ Workflow artifact:
152
+
153
+ - `npm-lit-public-candidate-evidence`
154
+ - `npm-lit-public-promotion-evidence`
155
+
156
+ Rollback은 promotion helper가 캡처한 이전 `latest`만 사용하며, 승격 이후 `latest`가 다른 값으로 바뀐 경우
157
+ rollback을 거부합니다.
158
+
159
+ ## 5. 금지 경로
160
+
161
+ - 로컬 `npm publish`
162
+ - 직접 `npm dist-tag`
163
+ - mutable working tree에서 publish
164
+ - candidate 검증 없이 `latest` 변경
165
+ - provenance/consumer evidence 없이 promotion
166
+ - 이미 게시된 버전 덮어쓰기
167
+
168
+ 두 패키지는 0.x public preview입니다. 1.0 안정성 결정 전에는 API와 지원 범위가 바뀔 수 있습니다.
package/package.json CHANGED
@@ -1,6 +1,107 @@
1
1
  {
2
2
  "name": "@context-action/lit-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "description": "Lit Web Component primitives, FACE support, W3C context protocol, React bridge, and SSR/hydration entries",
8
+ "main": "dist/index.cjs",
9
+ "module": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "require": {
18
+ "types": "./dist/index.d.cts",
19
+ "default": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "./ssr": {
23
+ "import": {
24
+ "types": "./dist/ssr.d.ts",
25
+ "default": "./dist/ssr.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/ssr.d.cts",
29
+ "default": "./dist/ssr.cjs"
30
+ }
31
+ },
32
+ "./hydrate": {
33
+ "import": {
34
+ "types": "./dist/hydrate.d.ts",
35
+ "default": "./dist/hydrate.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/hydrate.d.cts",
39
+ "default": "./dist/hydrate.cjs"
40
+ }
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md",
46
+ "docs",
47
+ "CHANGELOG.md",
48
+ "LICENSE"
49
+ ],
50
+ "scripts": {
51
+ "build": "tsdown",
52
+ "prepublishOnly": "pnpm run build",
53
+ "type-check": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "test:edge": "node --test test/edge-runtime.test.mjs",
56
+ "test:browser": "node test/browser-hydration.test.mjs"
57
+ },
58
+ "dependencies": {
59
+ "@context-action/core": "^1.2.6",
60
+ "@context-action/lit": "^0.1.1"
61
+ },
62
+ "peerDependencies": {
63
+ "@lit-labs/ssr": "^4.1.0",
64
+ "@lit-labs/ssr-client": "^1.1.8",
65
+ "@lit/context": "^1.1.0",
66
+ "lit": "^3.2.0",
67
+ "react": "^18.0.0 || ^19.0.0"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "@lit-labs/ssr": {
71
+ "optional": true
72
+ },
73
+ "@lit-labs/ssr-client": {
74
+ "optional": true
75
+ },
76
+ "react": {
77
+ "optional": true
78
+ }
79
+ },
80
+ "devDependencies": {
81
+ "@lit-labs/ssr": "4.1.0",
82
+ "@lit-labs/ssr-client": "1.1.8",
83
+ "@lit/context": "1.1.6",
84
+ "@types/node": "24.19.2",
85
+ "@types/react": "19.2.17",
86
+ "@types/react-dom": "19.2.3",
87
+ "jsdom": "30.1.3",
88
+ "lit": "3.3.3",
89
+ "react": "19.3.1",
90
+ "react-dom": "19.3.1",
91
+ "tsdown": "0.23.1",
92
+ "typescript": "6.0.3",
93
+ "vite": "8.3.5",
94
+ "vitest": "5.0.5"
95
+ },
96
+ "engines": {
97
+ "node": ">=24.11.0"
98
+ },
99
+ "publishConfig": {
100
+ "access": "public"
101
+ },
102
+ "repository": {
103
+ "type": "git",
104
+ "url": "git+https://github.com/mineclover/context-action.git",
105
+ "directory": "packages/lit-ui"
106
+ }
107
+ }