@jogak/ui 0.1.0-alpha.7.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jogak/ui",
3
- "version": "0.1.0-alpha.7.1",
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.7.1",
69
- "@jogak/react": "0.1.0-alpha.7.1"
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
- * 알파.7: Preview 영역 격리 모드. default `'none'`.
24
+ * 알파.8: Preview 영역 격리 모드. default `'iframe'`.
25
25
  *
26
- * - `'none'` — Preview 콘텐츠를 chrome 같은 document렌더 (알파.6까지의 동작).
27
- * - `'shadow'` — ShadowRoot 안에 마운트. 사용자 globalCss/reset이 chrome 침범 차단.
28
- * - `'iframe'` — 별도 document(iframe)마운트. 가장 강한 격리.
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 = 'shadow',
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,19 @@
1
1
  import { StrictMode } from 'react'
2
2
  import { createRoot } from 'react-dom/client'
3
3
  import 'virtual:jogak'
4
- import { _jogakCodeTheme, _jogakPreviewIsolation } from 'virtual:jogak'
4
+ import {
5
+ _jogakCodeTheme,
6
+ _jogakPreviewIsolation,
7
+ _jogakUserViteUrl,
8
+ } from 'virtual:jogak'
5
9
  import '../styles/jogak.css'
6
10
  import { JogakApp } from './App.js'
7
11
 
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 표준 경로로 처리한다.
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.
14
17
  if (_jogakPreviewIsolation === 'none') {
15
18
  await import('virtual:jogak/global-css')
16
19
  }
@@ -23,6 +26,7 @@ createRoot(rootEl).render(
23
26
  <JogakApp
24
27
  codeTheme={_jogakCodeTheme}
25
28
  previewIsolation={_jogakPreviewIsolation}
29
+ userViteUrl={_jogakUserViteUrl}
26
30
  />
27
31
  </StrictMode>,
28
32
  )
@@ -1,81 +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
- * 알파.7: previewIsolation='iframe' 모드의 mount 컴포넌트.
18
+ * 알파.8: previewIsolation='iframe' 모드의 mount 컴포넌트.
26
19
  *
27
- * - `<iframe src="/preview-frame.html">`을 마운트.
28
- * - iframe load `iframe.contentWindow.__jogak_setProps__({ entry, args })`를
29
- * 호출해 entry/args를 주입한다 (postMessage 미사용 — 동일 origin이므로
30
- * contentWindow 직접 접근 가능).
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
- * HMR:
34
- * - iframe document 자체도 Vite dev server module을 import하므로 사용자 컴포넌트
35
- * 파일 변경 fast refresh가 iframe 안에서 작동.
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
- * sandbox 미설정:
39
- * - 사용자 컴포넌트가 fetch/clipboard/storage 자유롭게 사용해야 하므로 sandbox X.
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 readyRef = useRef(false)
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 load첫 setProps
48
+ // postMessage 리스너 — iframe contentWindow 일치성 검증 처리.
51
49
  useEffect(() => {
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
+ }
58
+ window.addEventListener('message', handler)
59
+ return () => {
60
+ window.removeEventListener('message', handler)
61
+ }
62
+ }, [])
63
+
64
+ // iframe ready 또는 entry/args 변경 시 setProps.
65
+ useEffect(() => {
66
+ if (!ready) return
52
67
  const iframe = iframeRef.current
53
68
  if (iframe === null) return
54
- const handleLoad = (): void => {
55
- readyRef.current = true
56
- iframe.contentWindow?.__jogak_setProps__?.({ entry, args })
57
- }
58
- iframe.addEventListener('load', handleLoad)
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
59
78
  return () => {
60
- iframe.removeEventListener('load', handleLoad)
61
- // 알파.7.1: unmount race 회피 — iframe contentWindow 정리도 microtask defer.
79
+ if (iframe === null) return
62
80
  queueMicrotask(() => {
63
- iframe.contentWindow?.__jogak_unmount__?.()
81
+ iframe.contentWindow?.postMessage({ type: 'jogak:unmount' }, '*')
64
82
  })
65
83
  }
66
- // eslint-disable-next-line react-hooks/exhaustive-deps
67
84
  }, [])
68
85
 
69
- // entry/args 변경 시 setProps 재호출 (load 후에만)
70
- useEffect(() => {
71
- if (!readyRef.current) return
72
- iframeRef.current?.contentWindow?.__jogak_setProps__?.({ entry, args })
73
- }, [entry, args])
74
-
75
86
  return (
76
87
  <iframe
77
88
  ref={iframeRef}
78
- src="/preview-frame.html"
89
+ src={src}
79
90
  title="Preview"
80
91
  className={className}
81
92
  data-testid={dataTestId}
@@ -24,13 +24,18 @@ export interface PreviewProps {
24
24
  */
25
25
  readonly onResolveJogak?: (entryId: string, jogakName: string) => void
26
26
  /**
27
- * 알파.7: Preview 영역 격리 모드. default `'none'`.
27
+ * 알파.8: Preview 영역 격리 모드. default `'iframe'`.
28
28
  *
29
- * - `'none'` — 기존 동작 (chrome과 같은 document, 알파.6 chrome 보호 rule 적용).
30
- * - `'shadow'` — ShadowRoot에 마운트. 사용자 globalCss reset이 chrome 침범 차단.
31
- * - `'iframe'` — `/preview-frame.html` iframe마운트. 강한 격리.
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 = 'shadow',
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
- * 알파.7: previewIsolation 모드별로 사용자 콘텐츠 마운트 방식을 분기한다.
531
+ * 알파.8: previewIsolation 모드별로 사용자 콘텐츠 마운트 방식을 분기한다.
521
532
  *
522
- * - `'none'` — 같은 document에 직접 마운트 (알파.6까지의 동작 그대로).
523
- * - `'shadow'` — `<ShadowMount>` 안에 마운트해 ShadowRoot 격리.
524
- * - `'iframe'` — `<IframeMount>`로 별도 document에 마운트.
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
 
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
- * 알파.7: Preview 영역 격리 모드 ('none' | 'shadow' | 'iframe').
8
- * `JogakPluginOptions.previewIsolation` (default 'none')의 literal emit.
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' {