@jogak/ui 0.1.0-alpha.14.1 → 0.1.0-alpha.14.2
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 +6 -0
- package/package.json +3 -2
- package/src/lib/adapter-for.ts +173 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ 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.2] — 2026-05-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`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이 빈 모듈 에러를 던지던 케이스가 해소.
|
|
13
|
+
|
|
8
14
|
## [0.1.0-alpha.14.1] — 2026-05-11
|
|
9
15
|
|
|
10
16
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jogak/ui",
|
|
3
|
-
"version": "0.1.0-alpha.14.
|
|
3
|
+
"version": "0.1.0-alpha.14.2",
|
|
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.
|
|
69
|
+
"@jogak/core": "0.1.0-alpha.14.2"
|
|
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
|
+
}
|