@jogak/ui 0.1.0-alpha.14.1 → 0.1.0-alpha.14.3

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to Jogak packages are documented here. The repository follow
5
5
 
6
6
  Version numbers apply to all packages in the workspace (synchronized release).
7
7
 
8
+ ## [0.1.0-alpha.14.3] — 2026-05-15
9
+
10
+ ### Changed
11
+
12
+ - **버전 동기화 (no functional change)**: `@jogak/core@0.1.0-alpha.14.3` 결함 fix(web-components iframe 어댑터 + codegen strict typecheck) publish에 맞춰 동기화. `@jogak/ui` 소스/dist 변경 없음.
13
+
14
+ ## [0.1.0-alpha.14.2] — 2026-05-11
15
+
16
+ ### Fixed
17
+
18
+ - **`src/lib/adapter-for.ts` 누락 publish 결함**: 알파.14.1에서 `package.json` `files` 배열에 `src/lib/`이 빠져 있어, npm 설치 사용자 환경에서 `preview-frame.tsx`의 `import '../lib/adapter-for.js'` resolve가 실패하던 회귀를 수정. `src/lib/adapter-for.ts`를 명시적으로 publish 목록에 추가 (테스트 폴더 `__tests__/`는 의도적으로 제외). 외부 개발자가 `^0.1.0-alpha.14.1`로 설치해 `jogak build` 시 Rollup이 빈 모듈 에러를 던지던 케이스가 해소.
19
+
8
20
  ## [0.1.0-alpha.14.1] — 2026-05-11
9
21
 
10
22
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jogak/ui",
3
- "version": "0.1.0-alpha.14.1",
3
+ "version": "0.1.0-alpha.14.3",
4
4
  "description": "Showcase viewer UI for Jogak — Sidebar / Preview / Controls / Actions and the JogakApp shell.",
5
5
  "keywords": [
6
6
  "jogak",
@@ -46,6 +46,7 @@
46
46
  "src/app",
47
47
  "src/components",
48
48
  "src/hooks",
49
+ "src/lib/adapter-for.ts",
49
50
  "src/styles",
50
51
  "src/index.ts",
51
52
  "src/vite-env.d.ts",
@@ -65,7 +66,7 @@
65
66
  "prism-react-renderer": "^2.4.1",
66
67
  "tailwindcss": "^4.0.0",
67
68
  "@tailwindcss/vite": "^4.0.0",
68
- "@jogak/core": "0.1.0-alpha.14.1"
69
+ "@jogak/core": "0.1.0-alpha.14.3"
69
70
  },
70
71
  "devDependencies": {
71
72
  "@types/node": "^20.14.0",
@@ -0,0 +1,173 @@
1
+ /**
2
+ * 알파.14.1: framework별 renderer adapter dispatch router.
3
+ *
4
+ * `RegistryEntryMeta.framework` 필드를 보고 적절한 어댑터를 dynamic import한다.
5
+ * React-only 사용자가 Vue/Svelte 모듈을 로딩 받지 않도록 dynamic import + 모듈 캐시.
6
+ *
7
+ * 지원 framework:
8
+ * - 'react' / 'next' → `@jogak/core/renderers/react#reactAdapter`
9
+ * - 'vue' → `@jogak/core/renderers/vue#vueAdapter`
10
+ * - 'svelte' → `@jogak/core/renderers/svelte#svelteAdapter`
11
+ * - 'web-components' → 내부 wrapper (defineJogakElement 기반)
12
+ *
13
+ * 알 수 없는 framework는 명시적 에러 메시지로 throw한다.
14
+ */
15
+
16
+ import type { JogakAdapter, RegistryEntry } from '@jogak/core'
17
+
18
+ /** RegistryEntryMeta.framework가 받을 수 있는 모든 값 (JogakAdapter.framework와 동일). */
19
+ export type FrameworkKey =
20
+ | 'react'
21
+ | 'next'
22
+ | 'web-components'
23
+ | 'vue'
24
+ | 'svelte'
25
+
26
+ const cache = new Map<FrameworkKey, JogakAdapter>()
27
+ const inflight = new Map<FrameworkKey, Promise<JogakAdapter>>()
28
+
29
+ /**
30
+ * framework 이름으로 어댑터를 가져온다. 첫 호출은 dynamic import, 이후는 캐시 반환.
31
+ *
32
+ * 동시에 같은 framework로 호출되는 경우(예: 여러 effect가 동시에 await)에도 단 한 번만
33
+ * import가 일어나도록 inflight Promise를 공유한다.
34
+ */
35
+ export async function adapterFor(framework: string): Promise<JogakAdapter> {
36
+ const key = framework as FrameworkKey
37
+ const cached = cache.get(key)
38
+ if (cached !== undefined) return cached
39
+
40
+ const pending = inflight.get(key)
41
+ if (pending !== undefined) return pending
42
+
43
+ const loader = loadAdapter(key)
44
+ inflight.set(key, loader)
45
+ try {
46
+ const adapter = await loader
47
+ cache.set(key, adapter)
48
+ return adapter
49
+ } finally {
50
+ inflight.delete(key)
51
+ }
52
+ }
53
+
54
+ async function loadAdapter(framework: FrameworkKey): Promise<JogakAdapter> {
55
+ switch (framework) {
56
+ case 'react':
57
+ case 'next': {
58
+ // Next.js의 client-side 렌더링도 React 18+ root API와 동일하므로 reactAdapter 재사용.
59
+ const mod = await import('@jogak/core/renderers/react')
60
+ return mod.reactAdapter
61
+ }
62
+ case 'vue': {
63
+ const mod = await import('@jogak/core/renderers/vue')
64
+ return mod.vueAdapter
65
+ }
66
+ case 'svelte': {
67
+ const mod = await import('@jogak/core/renderers/svelte')
68
+ return mod.svelteAdapter
69
+ }
70
+ case 'web-components': {
71
+ const mod = await import('@jogak/core/renderers/web-components')
72
+ return createWebComponentsAdapter(mod.defineJogakElement)
73
+ }
74
+ default: {
75
+ throw new Error(
76
+ `[jogak/ui] Unknown framework: '${framework as string}'. ` +
77
+ `Expected one of: 'react' | 'next' | 'vue' | 'svelte' | 'web-components'.`,
78
+ )
79
+ }
80
+ }
81
+ }
82
+
83
+ // ── web-components wrapper ────────────────────────────────
84
+ //
85
+ // web-components renderer는 `defineJogakElement(tagName, entry)`로 custom element를
86
+ // 등록하는 형태로, `JogakAdapter` ABI(render/unmount(container))를 직접 제공하지 않는다.
87
+ // UI 측 dispatch 통일을 위해 thin wrapper로 감싼다.
88
+
89
+ type CustomElementHost = HTMLElement & {
90
+ setAttribute(name: string, value: string): void
91
+ }
92
+
93
+ type WCContainer = HTMLElement & {
94
+ _jogakWCElement?: HTMLElement
95
+ _jogakWCTagName?: string
96
+ }
97
+
98
+ function entryToTagName(entryId: string): string {
99
+ // entryId는 `Category/Subcategory/Name` 형태일 수 있다. custom element는 dash를
100
+ // 포함한 lowercase여야 하므로 안전한 문자만 남기고 'jogak-' prefix를 부여한다.
101
+ const safe = entryId
102
+ .toLowerCase()
103
+ .replace(/[^a-z0-9]+/gu, '-')
104
+ .replace(/^-+|-+$/gu, '')
105
+ return `jogak-${safe || 'entry'}`
106
+ }
107
+
108
+ function serializeAttribute(value: unknown): string | null {
109
+ if (value === undefined || value === null) return null
110
+ if (typeof value === 'string') return value
111
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
112
+ // 함수/객체는 attribute로 표현 불가 — null로 skip(어댑터가 injectActions 처리).
113
+ if (typeof value === 'function' || typeof value === 'object') return null
114
+ return String(value)
115
+ }
116
+
117
+ function createWebComponentsAdapter(
118
+ defineJogakElement: (tagName: string, entry: RegistryEntry) => void,
119
+ ): JogakAdapter {
120
+ return {
121
+ framework: 'web-components',
122
+ render(
123
+ entry: RegistryEntry,
124
+ args: Readonly<Record<string, unknown>>,
125
+ container: HTMLElement,
126
+ ): void {
127
+ const state = container as WCContainer
128
+ const tagName = entryToTagName(entry.id)
129
+ defineJogakElement(tagName, entry)
130
+
131
+ let el = state._jogakWCElement
132
+ if (el === undefined || state._jogakWCTagName !== tagName) {
133
+ // entry가 바뀐 경우(다른 tagName) 기존 element 제거 후 재생성.
134
+ if (el !== undefined) el.remove()
135
+ el = document.createElement(tagName) as CustomElementHost
136
+ container.replaceChildren(el)
137
+ state._jogakWCElement = el
138
+ state._jogakWCTagName = tagName
139
+ }
140
+
141
+ for (const [key, value] of Object.entries(args)) {
142
+ const serialized = serializeAttribute(value)
143
+ if (serialized === null) {
144
+ el.removeAttribute(key)
145
+ } else {
146
+ el.setAttribute(key, serialized)
147
+ }
148
+ }
149
+ },
150
+ unmount(container: HTMLElement): void {
151
+ const state = container as WCContainer
152
+ const el = state._jogakWCElement
153
+ if (el !== undefined) {
154
+ el.remove()
155
+ }
156
+ delete state._jogakWCElement
157
+ delete state._jogakWCTagName
158
+ },
159
+ }
160
+ }
161
+
162
+ // ── test-only helpers ─────────────────────────────────────
163
+
164
+ /**
165
+ * @internal test-only. 어댑터 캐시를 초기화한다.
166
+ *
167
+ * cache hit/miss 테스트, framework lookup 격리 테스트에 사용. 프로덕션 코드에서는
168
+ * 호출하지 말 것 — dynamic import의 비용이 발생한다.
169
+ */
170
+ export function __resetAdapterCacheForTesting(): void {
171
+ cache.clear()
172
+ inflight.clear()
173
+ }