@jogak/ui 0.1.0-alpha.12 → 0.1.0-alpha.14.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.
- package/CHANGELOG.md +15 -0
- package/dist/index.cjs +6 -6
- package/dist/index.mjs +595 -500
- package/dist/lib/adapter-for.d.ts +17 -0
- package/package.json +2 -2
- package/src/app/preview-frame.tsx +21 -4
- package/src/components/Preview/format-usage.ts +4 -1
- package/src/components/Preview/index.tsx +66 -10
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { JogakAdapter } from '@jogak/core';
|
|
2
|
+
/** RegistryEntryMeta.framework가 받을 수 있는 모든 값 (JogakAdapter.framework와 동일). */
|
|
3
|
+
export type FrameworkKey = 'react' | 'next' | 'web-components' | 'vue' | 'svelte';
|
|
4
|
+
/**
|
|
5
|
+
* framework 이름으로 어댑터를 가져온다. 첫 호출은 dynamic import, 이후는 캐시 반환.
|
|
6
|
+
*
|
|
7
|
+
* 동시에 같은 framework로 호출되는 경우(예: 여러 effect가 동시에 await)에도 단 한 번만
|
|
8
|
+
* import가 일어나도록 inflight Promise를 공유한다.
|
|
9
|
+
*/
|
|
10
|
+
export declare function adapterFor(framework: string): Promise<JogakAdapter>;
|
|
11
|
+
/**
|
|
12
|
+
* @internal test-only. 어댑터 캐시를 초기화한다.
|
|
13
|
+
*
|
|
14
|
+
* cache hit/miss 테스트, framework lookup 격리 테스트에 사용. 프로덕션 코드에서는
|
|
15
|
+
* 호출하지 말 것 — dynamic import의 비용이 발생한다.
|
|
16
|
+
*/
|
|
17
|
+
export declare function __resetAdapterCacheForTesting(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jogak/ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.14.1",
|
|
4
4
|
"description": "Showcase viewer UI for Jogak — Sidebar / Preview / Controls / Actions and the JogakApp shell.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jogak",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"prism-react-renderer": "^2.4.1",
|
|
66
66
|
"tailwindcss": "^4.0.0",
|
|
67
67
|
"@tailwindcss/vite": "^4.0.0",
|
|
68
|
-
"@jogak/core": "0.1.0-alpha.
|
|
68
|
+
"@jogak/core": "0.1.0-alpha.14.1"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^20.14.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 알파.9: standalone-adapter 또는 fallback 시 사용되는 same-origin iframe entry.
|
|
3
|
+
* 알파.14.1: entry.meta.framework 기반 adapter dispatch (react/vue/svelte/wc 지원).
|
|
3
4
|
*
|
|
4
5
|
* `IframeMount`는 알파.9에서 postMessage 프로토콜로 통일됐다 (cross-origin 어댑터와 동일).
|
|
5
6
|
* preview-frame.tsx도 같은 프로토콜을 따라야 한다 — 부모는 `jogak:setProps` 메시지를,
|
|
@@ -8,8 +9,9 @@
|
|
|
8
9
|
* jogak host vite scope에서 동작하므로 `virtual:jogak` (registry metas + entry loader)와
|
|
9
10
|
* `virtual:jogak/global-css` (사용자 globalCss) 가상 모듈을 그대로 사용한다.
|
|
10
11
|
*/
|
|
11
|
-
import {
|
|
12
|
+
import type { JogakAdapter } from '@jogak/core'
|
|
12
13
|
import { defaultRegistry } from '@jogak/core'
|
|
14
|
+
import { adapterFor } from '../lib/adapter-for.js'
|
|
13
15
|
import 'virtual:jogak'
|
|
14
16
|
import 'virtual:jogak/global-css'
|
|
15
17
|
|
|
@@ -17,23 +19,38 @@ const rootEl = document.getElementById('jogak-preview-root')
|
|
|
17
19
|
if (rootEl === null) throw new Error('#jogak-preview-root not found')
|
|
18
20
|
|
|
19
21
|
let currentContainer: HTMLDivElement | null = null
|
|
22
|
+
let currentAdapter: JogakAdapter | null = null
|
|
20
23
|
|
|
21
24
|
async function renderEntry(
|
|
22
25
|
entryId: string,
|
|
23
26
|
args: Readonly<Record<string, unknown>>,
|
|
24
27
|
): Promise<void> {
|
|
25
28
|
const entry = await defaultRegistry.requestEntry(entryId)
|
|
29
|
+
const framework = entry.meta.framework ?? 'react'
|
|
30
|
+
const nextAdapter = await adapterFor(framework)
|
|
31
|
+
|
|
26
32
|
if (currentContainer === null) {
|
|
27
33
|
currentContainer = document.createElement('div')
|
|
28
34
|
rootEl?.replaceChildren(currentContainer)
|
|
29
35
|
}
|
|
30
|
-
|
|
36
|
+
// entry framework가 바뀐 경우 이전 어댑터로 unmount 후 새 어댑터로 mount.
|
|
37
|
+
if (currentAdapter !== null && currentAdapter !== nextAdapter) {
|
|
38
|
+
currentAdapter.unmount(currentContainer)
|
|
39
|
+
// container 자체는 재사용 가능하지만 이전 어댑터가 남긴 root/component 상태가
|
|
40
|
+
// 섞이지 않도록 새 div로 교체.
|
|
41
|
+
const fresh = document.createElement('div')
|
|
42
|
+
rootEl?.replaceChildren(fresh)
|
|
43
|
+
currentContainer = fresh
|
|
44
|
+
}
|
|
45
|
+
currentAdapter = nextAdapter
|
|
46
|
+
await nextAdapter.render(entry, args, currentContainer)
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
function unmount(): void {
|
|
34
|
-
if (currentContainer !== null) {
|
|
35
|
-
|
|
50
|
+
if (currentContainer !== null && currentAdapter !== null) {
|
|
51
|
+
currentAdapter.unmount(currentContainer)
|
|
36
52
|
currentContainer = null
|
|
53
|
+
currentAdapter = null
|
|
37
54
|
}
|
|
38
55
|
}
|
|
39
56
|
|
|
@@ -60,10 +60,13 @@ export function formatUsageCode(
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function resolveComponentName(entry: RegistryEntry): string {
|
|
63
|
+
// 알파.14.1: iframe isolation 모드는 chrome scope에 component를 import하지 않으므로
|
|
64
|
+
// entry.meta.component가 `null`. fallback (title 마지막 segment)으로 직행한다.
|
|
63
65
|
const component = entry.meta.component as
|
|
64
66
|
| { displayName?: unknown; name?: unknown }
|
|
67
|
+
| null
|
|
65
68
|
| undefined
|
|
66
|
-
if (component !== undefined) {
|
|
69
|
+
if (component !== null && component !== undefined) {
|
|
67
70
|
if (typeof component.displayName === 'string' && component.displayName.length > 0) {
|
|
68
71
|
return component.displayName
|
|
69
72
|
}
|
|
@@ -3,9 +3,10 @@ import type { ReactElement, CSSProperties } from 'react'
|
|
|
3
3
|
import clsx from 'clsx'
|
|
4
4
|
import { Highlight, themes } from 'prism-react-renderer'
|
|
5
5
|
import type { PrismTheme } from 'prism-react-renderer'
|
|
6
|
-
import {
|
|
6
|
+
import { useEntry } from '@jogak/core/renderers/react'
|
|
7
7
|
import type { UseEntryState } from '@jogak/core/renderers/react'
|
|
8
|
-
import type { RegistryEntry, RegistryEntryMeta, ArgType } from '@jogak/core'
|
|
8
|
+
import type { JogakAdapter, RegistryEntry, RegistryEntryMeta, ArgType } from '@jogak/core'
|
|
9
|
+
import { adapterFor } from '../../lib/adapter-for.js'
|
|
9
10
|
import { Controls } from '../Controls/index.js'
|
|
10
11
|
import { Actions } from '../Actions/index.js'
|
|
11
12
|
import { ShadowMount } from './ShadowMount.js'
|
|
@@ -134,7 +135,11 @@ export function Preview({
|
|
|
134
135
|
userPreviewUrl = '',
|
|
135
136
|
previewEntryPath = '/__jogak_preview__/index.html',
|
|
136
137
|
}: PreviewProps): ReactElement {
|
|
137
|
-
|
|
138
|
+
// 알파.14.1: iframe isolation 모드에서는 chrome 측에 component 모듈을 import하지 않는다
|
|
139
|
+
// (chrome vite scope에 .vue/.svelte가 들어오면 plugin-vue/svelte 부재로 transform 실패).
|
|
140
|
+
// skipHydrate=true → useEntry가 synthetic entry(component=null)로 ready를 노출하고,
|
|
141
|
+
// 실제 마운트는 IframeMount가 사용자 vite scope의 iframe entry에 위임한다.
|
|
142
|
+
const state = useEntry(entryId, { skipHydrate: previewIsolation === 'iframe' })
|
|
138
143
|
const [viewport, setViewport] = useState<ViewportKey>('desktop')
|
|
139
144
|
const [bgMode, setBgMode] = useState<BgMode>('white')
|
|
140
145
|
const [bottomTab, setBottomTab] = useState<'controls' | 'actions'>('controls')
|
|
@@ -636,23 +641,50 @@ function PreviewMount({ entry, args, previewIsolation, userPreviewUrl, previewEn
|
|
|
636
641
|
|
|
637
642
|
function NoneAdapterContent({ entry, args }: { entry: RegistryEntry; args: Readonly<Record<string, unknown>> }): ReactElement {
|
|
638
643
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
644
|
+
const adapterRef = useRef<JogakAdapter | null>(null)
|
|
639
645
|
|
|
646
|
+
// 알파.14.1: entry.meta.framework로 dispatch. async 적응을 위해 effect 내에서
|
|
647
|
+
// await adapterFor → 캡처된 adapter로 render. unmount는 같은 adapter ref 사용.
|
|
640
648
|
useEffect(() => {
|
|
641
649
|
const container = containerRef.current
|
|
642
650
|
if (container === null) return
|
|
643
|
-
|
|
651
|
+
let cancelled = false
|
|
652
|
+
|
|
653
|
+
const framework = entry.meta.framework ?? 'react'
|
|
654
|
+
void adapterFor(framework).then((adapter) => {
|
|
655
|
+
if (cancelled) return
|
|
656
|
+
adapterRef.current = adapter
|
|
657
|
+
void adapter.render(entry, args, container)
|
|
658
|
+
})
|
|
659
|
+
|
|
644
660
|
return () => {
|
|
661
|
+
cancelled = true
|
|
662
|
+
const adapter = adapterRef.current
|
|
663
|
+
if (adapter === null) return
|
|
645
664
|
// 알파.7.1: React 18 concurrent unmount race(`Attempted to synchronously unmount...`)
|
|
646
665
|
// 회피 — fiber commit 끝난 직후로 defer.
|
|
647
|
-
queueMicrotask(() => {
|
|
666
|
+
queueMicrotask(() => { adapter.unmount(container) })
|
|
648
667
|
}
|
|
649
668
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
650
669
|
}, [entry])
|
|
651
670
|
|
|
671
|
+
// args 갱신용 effect — adapter가 이미 캐시돼 있으면 동기 분기를 탄다.
|
|
652
672
|
useEffect(() => {
|
|
653
673
|
const container = containerRef.current
|
|
654
674
|
if (container === null) return
|
|
655
|
-
|
|
675
|
+
let cancelled = false
|
|
676
|
+
const adapter = adapterRef.current
|
|
677
|
+
if (adapter !== null) {
|
|
678
|
+
void adapter.render(entry, args, container)
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
const framework = entry.meta.framework ?? 'react'
|
|
682
|
+
void adapterFor(framework).then((resolved) => {
|
|
683
|
+
if (cancelled) return
|
|
684
|
+
adapterRef.current = resolved
|
|
685
|
+
void resolved.render(entry, args, container)
|
|
686
|
+
})
|
|
687
|
+
return () => { cancelled = true }
|
|
656
688
|
}, [entry, args])
|
|
657
689
|
|
|
658
690
|
return (
|
|
@@ -665,19 +697,31 @@ function NoneAdapterContent({ entry, args }: { entry: RegistryEntry; args: Reado
|
|
|
665
697
|
}
|
|
666
698
|
|
|
667
699
|
/**
|
|
668
|
-
* Shadow 모드 — ShadowMount의 ShadowRoot 안에서
|
|
700
|
+
* Shadow 모드 — ShadowMount의 ShadowRoot 안에서 adapter.render를 호출하는
|
|
669
701
|
* 작은 wrapper. ShadowMount 안 portal 내부에 위치하므로 useRef는 ShadowRoot scope.
|
|
670
702
|
*/
|
|
671
703
|
function ShadowAdapterContent({ entry, args }: { entry: RegistryEntry; args: Readonly<Record<string, unknown>> }): ReactElement {
|
|
672
704
|
const ref = useRef<HTMLDivElement>(null)
|
|
705
|
+
const adapterRef = useRef<JogakAdapter | null>(null)
|
|
673
706
|
|
|
674
707
|
useEffect(() => {
|
|
675
708
|
const c = ref.current
|
|
676
709
|
if (c === null) return
|
|
677
|
-
|
|
710
|
+
let cancelled = false
|
|
711
|
+
|
|
712
|
+
const framework = entry.meta.framework ?? 'react'
|
|
713
|
+
void adapterFor(framework).then((adapter) => {
|
|
714
|
+
if (cancelled) return
|
|
715
|
+
adapterRef.current = adapter
|
|
716
|
+
void adapter.render(entry, args, c)
|
|
717
|
+
})
|
|
718
|
+
|
|
678
719
|
return () => {
|
|
720
|
+
cancelled = true
|
|
721
|
+
const adapter = adapterRef.current
|
|
722
|
+
if (adapter === null) return
|
|
679
723
|
// 알파.7.1: unmount race 회피
|
|
680
|
-
queueMicrotask(() => {
|
|
724
|
+
queueMicrotask(() => { adapter.unmount(c) })
|
|
681
725
|
}
|
|
682
726
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
683
727
|
}, [entry])
|
|
@@ -685,7 +729,19 @@ function ShadowAdapterContent({ entry, args }: { entry: RegistryEntry; args: Rea
|
|
|
685
729
|
useEffect(() => {
|
|
686
730
|
const c = ref.current
|
|
687
731
|
if (c === null) return
|
|
688
|
-
|
|
732
|
+
let cancelled = false
|
|
733
|
+
const adapter = adapterRef.current
|
|
734
|
+
if (adapter !== null) {
|
|
735
|
+
void adapter.render(entry, args, c)
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
const framework = entry.meta.framework ?? 'react'
|
|
739
|
+
void adapterFor(framework).then((resolved) => {
|
|
740
|
+
if (cancelled) return
|
|
741
|
+
adapterRef.current = resolved
|
|
742
|
+
void resolved.render(entry, args, c)
|
|
743
|
+
})
|
|
744
|
+
return () => { cancelled = true }
|
|
689
745
|
}, [entry, args])
|
|
690
746
|
|
|
691
747
|
return <div ref={ref} data-testid="preview-content-shadow" />
|