@jogak/ui 0.1.0-alpha.7 → 0.1.0-alpha.7.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 +30 -0
- package/README.md +28 -23
- package/dist/components/Preview/ShadowMount.d.ts +13 -14
- package/dist/host/index.d.ts +2 -2
- package/dist/index.js +1 -2
- package/dist/index.mjs +207 -222
- package/package.json +3 -3
- package/src/app/App.tsx +1 -1
- package/src/app/main.tsx +10 -6
- package/src/components/Preview/IframeMount.tsx +4 -2
- package/src/components/Preview/ShadowMount.tsx +15 -59
- package/src/components/Preview/index.tsx +10 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jogak/ui",
|
|
3
|
-
"version": "0.1.0-alpha.7",
|
|
3
|
+
"version": "0.1.0-alpha.7.1",
|
|
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.7",
|
|
69
|
-
"@jogak/react": "0.1.0-alpha.7"
|
|
68
|
+
"@jogak/core": "0.1.0-alpha.7.1",
|
|
69
|
+
"@jogak/react": "0.1.0-alpha.7.1"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^20.14.0",
|
package/src/app/App.tsx
CHANGED
|
@@ -52,7 +52,7 @@ export function JogakApp({
|
|
|
52
52
|
entries,
|
|
53
53
|
metas,
|
|
54
54
|
codeTheme = 'vsDark',
|
|
55
|
-
previewIsolation = '
|
|
55
|
+
previewIsolation = 'shadow',
|
|
56
56
|
}: JogakAppProps = {}): ReactElement {
|
|
57
57
|
// ── 4가지 모드 결정 (계약 §5.2) ─────────────────────────────────────
|
|
58
58
|
// 1) entries가 주어지면: 새 ComponentRegistry에 register (eager, 기존 동작)
|
package/src/app/main.tsx
CHANGED
|
@@ -3,14 +3,18 @@ import { createRoot } from 'react-dom/client'
|
|
|
3
3
|
import 'virtual:jogak'
|
|
4
4
|
import { _jogakCodeTheme, _jogakPreviewIsolation } from 'virtual:jogak'
|
|
5
5
|
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
6
|
import { JogakApp } from './App.js'
|
|
13
7
|
|
|
8
|
+
// 알파.7.1: 사용자 globalCss는 isolation === 'none'일 때만 outer document에 inject.
|
|
9
|
+
// - 'shadow'/'iframe' 모드에서는 ShadowMount/preview-frame.tsx가 자기 scope에서
|
|
10
|
+
// 사용자 css를 자체 import하므로 outer document inject가 불필요하고, 오히려
|
|
11
|
+
// chrome을 침범한다 (알파.7 결함).
|
|
12
|
+
// - top-level await로 가드 — Vite는 string literal specifier의 dynamic import를
|
|
13
|
+
// 정적 분석하여 별도 chunk + css HMR 표준 경로로 처리한다.
|
|
14
|
+
if (_jogakPreviewIsolation === 'none') {
|
|
15
|
+
await import('virtual:jogak/global-css')
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
const rootEl = document.getElementById('root')
|
|
15
19
|
if (rootEl === null) throw new Error('#root element not found')
|
|
16
20
|
|
|
@@ -58,8 +58,10 @@ export function IframeMount({
|
|
|
58
58
|
iframe.addEventListener('load', handleLoad)
|
|
59
59
|
return () => {
|
|
60
60
|
iframe.removeEventListener('load', handleLoad)
|
|
61
|
-
// unmount
|
|
62
|
-
|
|
61
|
+
// 알파.7.1: unmount race 회피 — iframe contentWindow 정리도 microtask defer.
|
|
62
|
+
queueMicrotask(() => {
|
|
63
|
+
iframe.contentWindow?.__jogak_unmount__?.()
|
|
64
|
+
})
|
|
63
65
|
}
|
|
64
66
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
67
|
}, [])
|
|
@@ -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
|
-
}
|
|
@@ -120,7 +120,7 @@ export function Preview({
|
|
|
120
120
|
onReset,
|
|
121
121
|
codeTheme,
|
|
122
122
|
onResolveJogak,
|
|
123
|
-
previewIsolation = '
|
|
123
|
+
previewIsolation = 'shadow',
|
|
124
124
|
}: PreviewProps): ReactElement {
|
|
125
125
|
const state = useEntry(entryId)
|
|
126
126
|
const [viewport, setViewport] = useState<ViewportKey>('desktop')
|
|
@@ -615,7 +615,11 @@ function NoneAdapterContent({ entry, args }: { entry: RegistryEntry; args: Reado
|
|
|
615
615
|
const container = containerRef.current
|
|
616
616
|
if (container === null) return
|
|
617
617
|
reactAdapter.render(entry, args, container)
|
|
618
|
-
return () => {
|
|
618
|
+
return () => {
|
|
619
|
+
// 알파.7.1: React 18 concurrent unmount race(`Attempted to synchronously unmount...`)
|
|
620
|
+
// 회피 — fiber commit 끝난 직후로 defer.
|
|
621
|
+
queueMicrotask(() => { reactAdapter.unmount(container) })
|
|
622
|
+
}
|
|
619
623
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
620
624
|
}, [entry])
|
|
621
625
|
|
|
@@ -645,7 +649,10 @@ function ShadowAdapterContent({ entry, args }: { entry: RegistryEntry; args: Rea
|
|
|
645
649
|
const c = ref.current
|
|
646
650
|
if (c === null) return
|
|
647
651
|
reactAdapter.render(entry, args, c)
|
|
648
|
-
return () => {
|
|
652
|
+
return () => {
|
|
653
|
+
// 알파.7.1: unmount race 회피
|
|
654
|
+
queueMicrotask(() => { reactAdapter.unmount(c) })
|
|
655
|
+
}
|
|
649
656
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
650
657
|
}, [entry])
|
|
651
658
|
|