@jogak/ui 0.1.0-alpha.7 → 0.1.0-alpha.8
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 +53 -0
- package/README.md +47 -24
- package/dist/app/App.d.ts +10 -5
- package/dist/components/Preview/IframeMount.d.ts +17 -24
- package/dist/components/Preview/ShadowMount.d.ts +13 -14
- package/dist/components/Preview/index.d.ts +10 -5
- package/dist/host/index.d.ts +12 -4
- package/dist/host/index.js +1 -1
- package/dist/host/index.mjs +30 -30
- package/dist/index.js +1 -2
- package/dist/index.mjs +328 -324
- package/package.json +3 -3
- package/src/app/App.tsx +12 -5
- package/src/app/main.tsx +15 -7
- package/src/components/Preview/IframeMount.tsx +55 -42
- package/src/components/Preview/ShadowMount.tsx +15 -59
- package/src/components/Preview/index.tsx +35 -14
- package/src/vite-env.d.ts +7 -2
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.8",
|
|
4
4
|
"description": "Showcase viewer UI for Jogak — Sidebar / Preview / Controls / Actions and the JogakApp shell.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jogak",
|
|
@@ -65,8 +65,8 @@
|
|
|
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.
|
|
69
|
-
"@jogak/react": "0.1.0-alpha.
|
|
68
|
+
"@jogak/core": "0.1.0-alpha.8",
|
|
69
|
+
"@jogak/react": "0.1.0-alpha.8"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^20.14.0",
|
package/src/app/App.tsx
CHANGED
|
@@ -21,15 +21,20 @@ export interface JogakAppProps {
|
|
|
21
21
|
readonly metas?: readonly RegistryEntryMeta[]
|
|
22
22
|
readonly codeTheme?: string
|
|
23
23
|
/**
|
|
24
|
-
* 알파.
|
|
24
|
+
* 알파.8: Preview 영역 격리 모드. default `'iframe'`.
|
|
25
25
|
*
|
|
26
|
-
* - `'
|
|
27
|
-
* - `'shadow'` — ShadowRoot 안에 마운트. 사용자
|
|
28
|
-
* - `'
|
|
26
|
+
* - `'iframe'` (default) — 사용자 vite 정상 client(iframe)에 마운트. 사용자 utility 정상 컴파일.
|
|
27
|
+
* - `'shadow'` (deprecated) — ShadowRoot 안에 마운트. 사용자 utility 미적용.
|
|
28
|
+
* - `'none'` (deprecated) — chrome 같은 document에 렌더. 알파.6까지의 동작.
|
|
29
29
|
*
|
|
30
30
|
* 자세한 트레이드오프는 `@jogak/ui` README의 "previewIsolation 사용 가이드" 참조.
|
|
31
31
|
*/
|
|
32
32
|
readonly previewIsolation?: 'none' | 'shadow' | 'iframe'
|
|
33
|
+
/**
|
|
34
|
+
* 알파.8: 사용자 vite spawn URL. iframe `src` base로 사용.
|
|
35
|
+
* 빈 문자열 시 fallback (jogak SPA Vite scope의 preview-frame.tsx).
|
|
36
|
+
*/
|
|
37
|
+
readonly userViteUrl?: string
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function readUrlParams(): { entryId: string; jogakName: string | null } | null {
|
|
@@ -52,7 +57,8 @@ export function JogakApp({
|
|
|
52
57
|
entries,
|
|
53
58
|
metas,
|
|
54
59
|
codeTheme = 'vsDark',
|
|
55
|
-
previewIsolation = '
|
|
60
|
+
previewIsolation = 'iframe',
|
|
61
|
+
userViteUrl = '',
|
|
56
62
|
}: JogakAppProps = {}): ReactElement {
|
|
57
63
|
// ── 4가지 모드 결정 (계약 §5.2) ─────────────────────────────────────
|
|
58
64
|
// 1) entries가 주어지면: 새 ComponentRegistry에 register (eager, 기존 동작)
|
|
@@ -156,6 +162,7 @@ export function JogakApp({
|
|
|
156
162
|
codeTheme={codeTheme}
|
|
157
163
|
onResolveJogak={handleResolveJogak}
|
|
158
164
|
previewIsolation={previewIsolation}
|
|
165
|
+
userViteUrl={userViteUrl}
|
|
159
166
|
/>
|
|
160
167
|
) : (
|
|
161
168
|
<div className="jogak:flex jogak:items-center jogak:justify-center jogak:h-full jogak:text-[var(--jogak-color-fg-subtle)]">
|
package/src/app/main.tsx
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { StrictMode } from 'react'
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
3
|
import 'virtual:jogak'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
_jogakCodeTheme,
|
|
6
|
+
_jogakPreviewIsolation,
|
|
7
|
+
_jogakUserViteUrl,
|
|
8
|
+
} from 'virtual:jogak'
|
|
5
9
|
import '../styles/jogak.css'
|
|
6
|
-
// 알파.6: 사용자 globalCss opt-in.
|
|
7
|
-
// JogakPluginOptions.globalCss=false (default) → 빈 모듈 (no-op, SPA 번들 영향 zero).
|
|
8
|
-
// true / string / string[] → plugin이 사용자 css를 import한다.
|
|
9
|
-
// jogak.css 뒤에 둬서 사용자가 jogak chrome 기본값을 명시적으로 override 가능 —
|
|
10
|
-
// 단, jogak utility는 prefix=jogak로 격리되어 사용자 utility와 충돌하지 않는다.
|
|
11
|
-
import 'virtual:jogak/global-css'
|
|
12
10
|
import { JogakApp } from './App.js'
|
|
13
11
|
|
|
12
|
+
// 알파.8: 사용자 globalCss는 사용자 vite scope(iframe entry)에서 처리되므로
|
|
13
|
+
// jogak SPA outer document에는 import하지 않는다 — chrome 격리 보존.
|
|
14
|
+
//
|
|
15
|
+
// 'none' 모드(deprecated): 알파.7.1 동작 유지가 필요한 사용자만 명시 사용.
|
|
16
|
+
// 이 경우만 outer document에 사용자 globalCss inject.
|
|
17
|
+
if (_jogakPreviewIsolation === 'none') {
|
|
18
|
+
await import('virtual:jogak/global-css')
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
const rootEl = document.getElementById('root')
|
|
15
22
|
if (rootEl === null) throw new Error('#root element not found')
|
|
16
23
|
|
|
@@ -19,6 +26,7 @@ createRoot(rootEl).render(
|
|
|
19
26
|
<JogakApp
|
|
20
27
|
codeTheme={_jogakCodeTheme}
|
|
21
28
|
previewIsolation={_jogakPreviewIsolation}
|
|
29
|
+
userViteUrl={_jogakUserViteUrl}
|
|
22
30
|
/>
|
|
23
31
|
</StrictMode>,
|
|
24
32
|
)
|
|
@@ -1,79 +1,92 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react'
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import type { ReactElement } from 'react'
|
|
3
3
|
import type { RegistryEntry } from '@jogak/core'
|
|
4
4
|
|
|
5
5
|
export interface IframeMountProps {
|
|
6
6
|
readonly entry: RegistryEntry
|
|
7
7
|
readonly args: Readonly<Record<string, unknown>>
|
|
8
|
+
/**
|
|
9
|
+
* 알파.8: 사용자 vite spawn URL (예: `http://localhost:5174`).
|
|
10
|
+
* 빈 문자열 시 fallback (jogak SPA Vite scope의 `/preview-frame.html`).
|
|
11
|
+
*/
|
|
12
|
+
readonly userViteUrl: string
|
|
8
13
|
readonly className?: string
|
|
9
14
|
readonly 'data-testid'?: string
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
interface SetPropsArgs {
|
|
13
|
-
readonly entry: RegistryEntry
|
|
14
|
-
readonly args: Readonly<Record<string, unknown>>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
declare global {
|
|
18
|
-
interface Window {
|
|
19
|
-
__jogak_setProps__?: (args: SetPropsArgs) => void
|
|
20
|
-
__jogak_unmount__?: () => void
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
17
|
/**
|
|
25
|
-
* 알파.
|
|
18
|
+
* 알파.8: previewIsolation='iframe' 모드의 mount 컴포넌트.
|
|
26
19
|
*
|
|
27
|
-
*
|
|
28
|
-
* -
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* - entry/args 변경 시 setProps 재호출 (load 완료 이후).
|
|
20
|
+
* 통신:
|
|
21
|
+
* - 사용자 vite spawn URL이 주어지면(`userViteUrl !== ''`) iframe src를
|
|
22
|
+
* `${userViteUrl}/__jogak_preview__/index.html` (cross-origin)로 설정.
|
|
23
|
+
* - 동일 origin fallback 시 `/preview-frame.html` (jogak SPA Vite scope).
|
|
32
24
|
*
|
|
33
|
-
*
|
|
34
|
-
* - iframe
|
|
35
|
-
*
|
|
36
|
-
* - previewIsolation 모드 자체 변경은 가상 모듈 invalidate → full reload.
|
|
25
|
+
* 양쪽 모두 postMessage로 통신:
|
|
26
|
+
* - 부모 → iframe: `{ type: 'jogak:setProps', entryId, args }` | `{ type: 'jogak:unmount' }`
|
|
27
|
+
* - iframe → 부모: `{ type: 'jogak:ready' }` | `{ type: 'jogak:rendered', entryId }`
|
|
37
28
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
29
|
+
* `entry`는 객체가 아닌 **id만 전달** — iframe 안에서 `defaultRegistry.requestEntry(id)`로
|
|
30
|
+
* dynamic import. 사용자 vite scope의 entry 가상 모듈이 사용자 컴포넌트를 fetch하므로
|
|
31
|
+
* 사용자 plugins(@tailwindcss/vite, custom alias 등)이 정상 작동.
|
|
40
32
|
*/
|
|
41
33
|
export function IframeMount({
|
|
42
34
|
entry,
|
|
43
35
|
args,
|
|
36
|
+
userViteUrl,
|
|
44
37
|
className,
|
|
45
38
|
'data-testid': dataTestId,
|
|
46
39
|
}: IframeMountProps): ReactElement {
|
|
47
40
|
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
|
48
|
-
const
|
|
41
|
+
const [ready, setReady] = useState(false)
|
|
42
|
+
|
|
43
|
+
const src =
|
|
44
|
+
userViteUrl !== ''
|
|
45
|
+
? `${userViteUrl}/__jogak_preview__/index.html`
|
|
46
|
+
: '/preview-frame.html'
|
|
49
47
|
|
|
50
|
-
// iframe
|
|
48
|
+
// postMessage 리스너 — iframe contentWindow 일치성 검증 후 처리.
|
|
51
49
|
useEffect(() => {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
const handler = (event: MessageEvent): void => {
|
|
51
|
+
const iframe = iframeRef.current
|
|
52
|
+
if (iframe === null) return
|
|
53
|
+
if (event.source !== iframe.contentWindow) return
|
|
54
|
+
const data = event.data
|
|
55
|
+
if (data == null || typeof data !== 'object') return
|
|
56
|
+
if (data.type === 'jogak:ready') setReady(true)
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
window.addEventListener('message', handler)
|
|
59
59
|
return () => {
|
|
60
|
-
|
|
61
|
-
// unmount 시 iframe 안 react root도 정리 (best-effort)
|
|
62
|
-
iframe.contentWindow?.__jogak_unmount__?.()
|
|
60
|
+
window.removeEventListener('message', handler)
|
|
63
61
|
}
|
|
64
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
62
|
}, [])
|
|
66
63
|
|
|
67
|
-
// entry/args 변경 시 setProps
|
|
64
|
+
// iframe ready 또는 entry/args 변경 시 setProps.
|
|
68
65
|
useEffect(() => {
|
|
69
|
-
if (!
|
|
70
|
-
iframeRef.current
|
|
71
|
-
|
|
66
|
+
if (!ready) return
|
|
67
|
+
const iframe = iframeRef.current
|
|
68
|
+
if (iframe === null) return
|
|
69
|
+
iframe.contentWindow?.postMessage(
|
|
70
|
+
{ type: 'jogak:setProps', entryId: entry.id, args },
|
|
71
|
+
'*',
|
|
72
|
+
)
|
|
73
|
+
}, [ready, entry, args])
|
|
74
|
+
|
|
75
|
+
// unmount 시 unmount 메시지 (race 회피 microtask defer).
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const iframe = iframeRef.current
|
|
78
|
+
return () => {
|
|
79
|
+
if (iframe === null) return
|
|
80
|
+
queueMicrotask(() => {
|
|
81
|
+
iframe.contentWindow?.postMessage({ type: 'jogak:unmount' }, '*')
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}, [])
|
|
72
85
|
|
|
73
86
|
return (
|
|
74
87
|
<iframe
|
|
75
88
|
ref={iframeRef}
|
|
76
|
-
src=
|
|
89
|
+
src={src}
|
|
77
90
|
title="Preview"
|
|
78
91
|
className={className}
|
|
79
92
|
data-testid={dataTestId}
|
|
@@ -6,26 +6,25 @@ export interface ShadowMountProps {
|
|
|
6
6
|
readonly children: ReactNode
|
|
7
7
|
readonly className?: string
|
|
8
8
|
readonly style?: CSSProperties
|
|
9
|
-
/** 외부 테스트 hook (호스트 div에 부여). */
|
|
10
9
|
readonly 'data-testid'?: string
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
* 알파.7: previewIsolation='shadow' 모드의 mount 컴포넌트.
|
|
13
|
+
* 알파.7.1: previewIsolation='shadow' 모드의 mount 컴포넌트.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
* `adoptedStyleSheets`로 ShadowRoot에 share (jogak.css + virtual:jogak/global-css
|
|
21
|
-
* 둘 다 자동 포함).
|
|
22
|
-
* - Vite dev에서 `<style>` HMR 시 MutationObserver로 ShadowRoot도 갱신.
|
|
15
|
+
* 책임: 양방향 격리만 제공 (Preview ↔ outer document 양방향 cascade 차단).
|
|
16
|
+
* - 사용자 globalCss는 main.tsx 가드로 outer document에 inject되지 않음.
|
|
17
|
+
* - shadow root 안에는 jogak chrome css도 사용자 css도 없음 (둘 다 외부에서 격리).
|
|
18
|
+
* - 사용자 컴포넌트의 utility class 컴파일은 결함 B (알파.8 사이클).
|
|
23
19
|
*
|
|
24
|
-
*
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
20
|
+
* 알파.7 결함 정정:
|
|
21
|
+
* - `syncStyleSheets`/`MutationObserver`/`adoptedStyleSheets` 흡수 로직 제거.
|
|
22
|
+
* 알파.7은 outer document에 사용자 css가 있는 한 chrome을 침범했고, shadow
|
|
23
|
+
* 안의 흡수 로직은 의미가 없었음. 알파.7.1: outer에 사용자 css 자체가 없음.
|
|
24
|
+
*
|
|
25
|
+
* Radix portal 한계 (사용자 인지 필요, README 명시):
|
|
26
|
+
* - default Portal target = document.body (shadow 외부). 사용자가 명시적으로
|
|
27
|
+
* `<Portal container={shadowRootEl}>`을 전달해야 portal 내용도 격리됨.
|
|
29
28
|
*/
|
|
30
29
|
export function ShadowMount({
|
|
31
30
|
children,
|
|
@@ -39,16 +38,9 @@ export function ShadowMount({
|
|
|
39
38
|
useEffect(() => {
|
|
40
39
|
const host = hostRef.current
|
|
41
40
|
if (host === null) return
|
|
42
|
-
|
|
43
|
-
if (host.shadowRoot !== null) {
|
|
44
|
-
sr = host.shadowRoot
|
|
45
|
-
} else {
|
|
46
|
-
sr = host.attachShadow({ mode: 'open' })
|
|
47
|
-
}
|
|
41
|
+
const sr = host.shadowRoot ?? host.attachShadow({ mode: 'open' })
|
|
48
42
|
setShadowRoot(sr)
|
|
49
|
-
|
|
50
|
-
const observer = observeDocumentStyles(sr)
|
|
51
|
-
return () => { observer.disconnect() }
|
|
43
|
+
// shadow root는 host element와 함께 GC — 명시 detach 불필요.
|
|
52
44
|
}, [])
|
|
53
45
|
|
|
54
46
|
return (
|
|
@@ -63,39 +55,3 @@ export function ShadowMount({
|
|
|
63
55
|
</div>
|
|
64
56
|
)
|
|
65
57
|
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 외부 document의 모든 stylesheet를 ShadowRoot에 share한다.
|
|
69
|
-
*
|
|
70
|
-
* - `adoptedStyleSheets` API를 사용 (Chromium/Safari/Firefox 모던 브라우저 지원).
|
|
71
|
-
* - cross-origin sheet는 `cssRules` 접근 시 SecurityError → catch 후 skip.
|
|
72
|
-
* - Vite dev의 `<style>` HMR이 자주 발생할 수 있어 `replaceSync`를 통한 새
|
|
73
|
-
* `CSSStyleSheet` 인스턴스 생성으로 처리.
|
|
74
|
-
*/
|
|
75
|
-
function syncStyleSheets(shadowRoot: ShadowRoot): void {
|
|
76
|
-
const sheets: CSSStyleSheet[] = []
|
|
77
|
-
for (const sheet of Array.from(document.styleSheets)) {
|
|
78
|
-
try {
|
|
79
|
-
const rules = sheet.cssRules
|
|
80
|
-
const cs = new CSSStyleSheet()
|
|
81
|
-
const cssText = Array.from(rules).map((r) => r.cssText).join('\n')
|
|
82
|
-
cs.replaceSync(cssText)
|
|
83
|
-
sheets.push(cs)
|
|
84
|
-
} catch {
|
|
85
|
-
// cross-origin sheet — skip
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
shadowRoot.adoptedStyleSheets = sheets
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* document.head의 변화(`<style>` HMR add/remove)를 관찰해 ShadowRoot의
|
|
93
|
-
* adoptedStyleSheets를 다시 sync.
|
|
94
|
-
*/
|
|
95
|
-
function observeDocumentStyles(shadowRoot: ShadowRoot): MutationObserver {
|
|
96
|
-
const observer = new MutationObserver(() => {
|
|
97
|
-
syncStyleSheets(shadowRoot)
|
|
98
|
-
})
|
|
99
|
-
observer.observe(document.head, { childList: true, subtree: true })
|
|
100
|
-
return observer
|
|
101
|
-
}
|
|
@@ -24,13 +24,18 @@ export interface PreviewProps {
|
|
|
24
24
|
*/
|
|
25
25
|
readonly onResolveJogak?: (entryId: string, jogakName: string) => void
|
|
26
26
|
/**
|
|
27
|
-
* 알파.
|
|
27
|
+
* 알파.8: Preview 영역 격리 모드. default `'iframe'`.
|
|
28
28
|
*
|
|
29
|
-
* - `'
|
|
30
|
-
* - `'shadow'` — ShadowRoot에 마운트. 사용자
|
|
31
|
-
* - `'
|
|
29
|
+
* - `'iframe'` (default) — 사용자 vite scope에 마운트. 사용자 utility 정상 컴파일.
|
|
30
|
+
* - `'shadow'` (deprecated) — ShadowRoot에 마운트. 사용자 utility 미적용.
|
|
31
|
+
* - `'none'` (deprecated) — chrome과 같은 document에 렌더.
|
|
32
32
|
*/
|
|
33
33
|
readonly previewIsolation?: 'none' | 'shadow' | 'iframe'
|
|
34
|
+
/**
|
|
35
|
+
* 알파.8: 사용자 vite spawn URL. iframe `src` base.
|
|
36
|
+
* 빈 문자열 시 fallback (jogak SPA Vite scope의 `/preview-frame.html`).
|
|
37
|
+
*/
|
|
38
|
+
readonly userViteUrl?: string
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
type ViewportKey = 'mobile' | 'tablet' | 'desktop'
|
|
@@ -120,7 +125,8 @@ export function Preview({
|
|
|
120
125
|
onReset,
|
|
121
126
|
codeTheme,
|
|
122
127
|
onResolveJogak,
|
|
123
|
-
previewIsolation = '
|
|
128
|
+
previewIsolation = 'iframe',
|
|
129
|
+
userViteUrl = '',
|
|
124
130
|
}: PreviewProps): ReactElement {
|
|
125
131
|
const state = useEntry(entryId)
|
|
126
132
|
const [viewport, setViewport] = useState<ViewportKey>('desktop')
|
|
@@ -187,6 +193,7 @@ export function Preview({
|
|
|
187
193
|
onBottomTabChange={setBottomTab}
|
|
188
194
|
prismTheme={prismTheme}
|
|
189
195
|
previewIsolation={previewIsolation}
|
|
196
|
+
userViteUrl={userViteUrl}
|
|
190
197
|
/>
|
|
191
198
|
)
|
|
192
199
|
}
|
|
@@ -275,6 +282,7 @@ interface ReadyFrameProps {
|
|
|
275
282
|
readonly onBottomTabChange: (tab: 'controls' | 'actions') => void
|
|
276
283
|
readonly prismTheme: PrismTheme
|
|
277
284
|
readonly previewIsolation: 'none' | 'shadow' | 'iframe'
|
|
285
|
+
readonly userViteUrl: string
|
|
278
286
|
}
|
|
279
287
|
|
|
280
288
|
function ReadyFrame({
|
|
@@ -292,6 +300,7 @@ function ReadyFrame({
|
|
|
292
300
|
onBottomTabChange,
|
|
293
301
|
prismTheme,
|
|
294
302
|
previewIsolation,
|
|
303
|
+
userViteUrl,
|
|
295
304
|
}: ReadyFrameProps): ReactElement {
|
|
296
305
|
// jogakName이 비어있으면 (deep link `?entry=...&jogak` 누락) 첫 jogak로 보정.
|
|
297
306
|
const resolvedJogakName = jogakName ?? entry.jogaks[0]?.name ?? null
|
|
@@ -364,6 +373,7 @@ function ReadyFrame({
|
|
|
364
373
|
source={entry.source}
|
|
365
374
|
theme={prismTheme}
|
|
366
375
|
previewIsolation={previewIsolation}
|
|
376
|
+
userViteUrl={userViteUrl}
|
|
367
377
|
/>
|
|
368
378
|
</div>
|
|
369
379
|
</div>
|
|
@@ -514,18 +524,19 @@ interface JogakRendererProps {
|
|
|
514
524
|
readonly source: string | undefined
|
|
515
525
|
readonly theme: PrismTheme
|
|
516
526
|
readonly previewIsolation: 'none' | 'shadow' | 'iframe'
|
|
527
|
+
readonly userViteUrl: string
|
|
517
528
|
}
|
|
518
529
|
|
|
519
530
|
/**
|
|
520
|
-
* 알파.
|
|
531
|
+
* 알파.8: previewIsolation 모드별로 사용자 콘텐츠 마운트 방식을 분기한다.
|
|
521
532
|
*
|
|
522
|
-
* - `'
|
|
523
|
-
* - `'shadow'` — `<ShadowMount>` 안에
|
|
524
|
-
* - `'
|
|
533
|
+
* - `'iframe'` (default) — 사용자 vite scope의 `<IframeMount>`로 별도 document.
|
|
534
|
+
* - `'shadow'` (deprecated) — `<ShadowMount>` 안에 마운트.
|
|
535
|
+
* - `'none'` (deprecated) — 같은 document에 직접 마운트.
|
|
525
536
|
*
|
|
526
537
|
* Show source 토글, 코드 패널 등 chrome 부분은 모드 무관하게 외부에 둔다.
|
|
527
538
|
*/
|
|
528
|
-
function JogakRenderer({ entry, args, source, theme, previewIsolation }: JogakRendererProps): ReactElement {
|
|
539
|
+
function JogakRenderer({ entry, args, source, theme, previewIsolation, userViteUrl }: JogakRendererProps): ReactElement {
|
|
529
540
|
const [showCode, setShowCode] = useState(false)
|
|
530
541
|
|
|
531
542
|
const previewBody = (
|
|
@@ -534,6 +545,7 @@ function JogakRenderer({ entry, args, source, theme, previewIsolation }: JogakRe
|
|
|
534
545
|
entry={entry}
|
|
535
546
|
args={args}
|
|
536
547
|
previewIsolation={previewIsolation}
|
|
548
|
+
userViteUrl={userViteUrl}
|
|
537
549
|
/>
|
|
538
550
|
<button
|
|
539
551
|
type="button"
|
|
@@ -575,13 +587,14 @@ interface PreviewMountProps {
|
|
|
575
587
|
readonly entry: RegistryEntry
|
|
576
588
|
readonly args: Readonly<Record<string, unknown>>
|
|
577
589
|
readonly previewIsolation: 'none' | 'shadow' | 'iframe'
|
|
590
|
+
readonly userViteUrl: string
|
|
578
591
|
}
|
|
579
592
|
|
|
580
593
|
const PREVIEW_HOST_CLASS =
|
|
581
594
|
'jogak:border jogak:border-dashed jogak:border-[var(--jogak-color-border)] ' +
|
|
582
595
|
'jogak:rounded-[var(--jogak-radius-xl)] jogak:p-4 jogak:pb-9'
|
|
583
596
|
|
|
584
|
-
function PreviewMount({ entry, args, previewIsolation }: PreviewMountProps): ReactElement {
|
|
597
|
+
function PreviewMount({ entry, args, previewIsolation, userViteUrl }: PreviewMountProps): ReactElement {
|
|
585
598
|
if (previewIsolation === 'shadow') {
|
|
586
599
|
return (
|
|
587
600
|
<ShadowMount
|
|
@@ -598,13 +611,14 @@ function PreviewMount({ entry, args, previewIsolation }: PreviewMountProps): Rea
|
|
|
598
611
|
<IframeMount
|
|
599
612
|
entry={entry}
|
|
600
613
|
args={args}
|
|
614
|
+
userViteUrl={userViteUrl}
|
|
601
615
|
data-testid="preview-content"
|
|
602
616
|
className={`${PREVIEW_HOST_CLASS} jogak:block jogak:w-full jogak:bg-transparent jogak:min-h-[256px]`}
|
|
603
617
|
/>
|
|
604
618
|
)
|
|
605
619
|
}
|
|
606
620
|
|
|
607
|
-
// 'none' —
|
|
621
|
+
// 'none' — deprecated 경로 (알파.7.1 동등 동작 보존, back-compat)
|
|
608
622
|
return <NoneAdapterContent entry={entry} args={args} />
|
|
609
623
|
}
|
|
610
624
|
|
|
@@ -615,7 +629,11 @@ function NoneAdapterContent({ entry, args }: { entry: RegistryEntry; args: Reado
|
|
|
615
629
|
const container = containerRef.current
|
|
616
630
|
if (container === null) return
|
|
617
631
|
reactAdapter.render(entry, args, container)
|
|
618
|
-
return () => {
|
|
632
|
+
return () => {
|
|
633
|
+
// 알파.7.1: React 18 concurrent unmount race(`Attempted to synchronously unmount...`)
|
|
634
|
+
// 회피 — fiber commit 끝난 직후로 defer.
|
|
635
|
+
queueMicrotask(() => { reactAdapter.unmount(container) })
|
|
636
|
+
}
|
|
619
637
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
620
638
|
}, [entry])
|
|
621
639
|
|
|
@@ -645,7 +663,10 @@ function ShadowAdapterContent({ entry, args }: { entry: RegistryEntry; args: Rea
|
|
|
645
663
|
const c = ref.current
|
|
646
664
|
if (c === null) return
|
|
647
665
|
reactAdapter.render(entry, args, c)
|
|
648
|
-
return () => {
|
|
666
|
+
return () => {
|
|
667
|
+
// 알파.7.1: unmount race 회피
|
|
668
|
+
queueMicrotask(() => { reactAdapter.unmount(c) })
|
|
669
|
+
}
|
|
649
670
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
650
671
|
}, [entry])
|
|
651
672
|
|
package/src/vite-env.d.ts
CHANGED
|
@@ -4,10 +4,15 @@ declare module 'virtual:jogak' {
|
|
|
4
4
|
/** 플러그인 설정에서 지정한 prism-react-renderer 테마 이름 */
|
|
5
5
|
export const _jogakCodeTheme: string
|
|
6
6
|
/**
|
|
7
|
-
* 알파.
|
|
8
|
-
* `JogakPluginOptions.previewIsolation` (default '
|
|
7
|
+
* 알파.8: Preview 영역 격리 모드 ('none' | 'shadow' | 'iframe').
|
|
8
|
+
* `JogakPluginOptions.previewIsolation` (default 'iframe')의 literal emit.
|
|
9
9
|
*/
|
|
10
10
|
export const _jogakPreviewIsolation: 'none' | 'shadow' | 'iframe'
|
|
11
|
+
/**
|
|
12
|
+
* 알파.8: 사용자 vite spawn URL. iframe `src` base로 사용 (예: `http://localhost:5174`).
|
|
13
|
+
* 빈 문자열 시 fallback (jogak SPA Vite scope의 preview-frame.tsx).
|
|
14
|
+
*/
|
|
15
|
+
export const _jogakUserViteUrl: string
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
declare module 'virtual:jogak/global-css' {
|