11<script setup lang="ts">
2- import { File , Repl } from ' @vue/repl'
2+ import { Repl } from ' @vue/repl'
33import Monaco from ' @vue/repl/monaco-editor'
44import { nextTick , onMounted , ref , watch , watchEffect } from ' vue'
55import Header from ' ./Header.vue'
66import { generateImportMap , generateStore , getDefaultFiles , getVersions } from ' ./utils'
77
8- declare global {
9- const __TINY_ROBOT_VERSION__: string
10- }
11-
12- const tinyRobotVersion = ref (__TINY_ROBOT_VERSION__ || ' latest' )
8+ const tinyRobotVersion = ref (' latest' )
139
1410const tinyRobotVersions = ref <string []>([tinyRobotVersion .value ])
11+ const tinyRobotLatestVersion = ref <string | undefined >(undefined )
1512
1613const { store, builtinImportMap } = generateStore ({
1714 tinyRobotVersion: tinyRobotVersion .value ,
1815 files: location .hash ? [] : getDefaultFiles ({ tinyRobotVersion: tinyRobotVersion .value }),
1916})
2017
18+ // Extract TinyRobot version from import-map in store (e.g. from hash) and sync to tinyRobotVersion
19+ function syncTinyRobotVersionFromImportMap() {
20+ type ImportMapShape = { imports? : Record <string , string > }
21+ let importMap: ImportMapShape | null = null
22+
23+ const importMapFile = store .files [' import-map.json' ]
24+ if (importMapFile ?.code ) {
25+ try {
26+ importMap = JSON .parse (importMapFile .code ) as ImportMapShape
27+ } catch {
28+ // ignore
29+ }
30+ }
31+ const tinyRobotUrl = importMap ?.imports ?.[' @opentiny/tiny-robot' ]
32+ if (! tinyRobotUrl ) return
33+
34+ // Match version in URL like .../tiny-robot@0.3.2/... or .../tiny-robot@latest/...
35+ const match = tinyRobotUrl .match (/ @opentiny\/ tiny-robot@([^ /] + )/ )
36+ const version = match ?.[1 ]?.trim ()
37+ if (version ) {
38+ tinyRobotVersion .value = version
39+ }
40+ }
41+
42+ /**
43+ * Trigger a full recompile of all files in the store.
44+ * Use this when file contents (e.g. src/index.css) are updated in-place so that the preview
45+ * picks up the changes without needing to activate the corresponding tab.
46+ */
47+ function triggerFullRecompile() {
48+ store .setFiles (store .getFiles (), store .mainFile )
49+ }
50+
51+ // Listen for messages from the host app (useful when embedded in an iframe).
52+ const ALLOWED_ORIGINS = [' https://playground.opentiny.design' ] as const
53+
54+ function isAllowedMessageOrigin(origin : string ): boolean {
55+ // Allow local dev (any port) and the official playground domain.
56+ if (! origin ) return false
57+ if (ALLOWED_ORIGINS .includes (origin as (typeof ALLOWED_ORIGINS )[number ])) return true
58+
59+ try {
60+ const url = new URL (origin )
61+ const host = url .hostname .toLowerCase ()
62+ if (host === ' localhost' || host === ' 127.0.0.1' ) return true
63+ return false
64+ } catch {
65+ return false
66+ }
67+ }
68+
69+ // 使用 document.referrer 推导父页面的 origin,在跨域场景下无法直接访问 window.parent.location
70+ function getParentOrigin(): string | null {
71+ try {
72+ const referrer = document .referrer
73+ if (! referrer ) return null
74+ const url = new URL (referrer )
75+ const origin = url .origin
76+ return isAllowedMessageOrigin (origin ) ? origin : null
77+ } catch {
78+ return null
79+ }
80+ }
81+
2182if (location .hash ) {
22- store .deserialize (location .hash )
83+ try {
84+ store .deserialize (location .hash )
85+ syncTinyRobotVersionFromImportMap ()
86+ } catch {
87+ // ignore
88+ }
2389}
2490
25- // persist state to URL hash
26- watchEffect (() => history .replaceState ({}, ' ' , store .serialize ()))
91+ // Persist state to URL hash; when in an iframe, notify the parent so it can sync the URL.
92+ watchEffect (() => {
93+ const serialized = store .serialize ()
94+ history .replaceState ({}, ' ' , serialized )
95+ if (window .self !== window .top ) {
96+ const targetOrigin = getParentOrigin ()
97+ if (targetOrigin ) {
98+ window .parent .postMessage (
99+ { type: ' playground-hash-change' , url: window .location .href , hash: serialized },
100+ targetOrigin ,
101+ )
102+ }
103+ }
104+ })
27105
28106// Watch for TinyRobot version changes and update import map
29107watch (tinyRobotVersion , async (newVersion ) => {
@@ -43,28 +121,35 @@ watch(tinyRobotVersion, async (newVersion) => {
43121 ` @opentiny/tiny-robot@${newVersion }/dist/style.css ` ,
44122 )
45123 if (indexCssFile .code !== updatedCss ) {
46- store .addFile (new File (' src/index.css' , updatedCss ))
124+ indexCssFile .code = updatedCss
125+ triggerFullRecompile ()
47126 }
48127 }
49128})
50129
51- // Load available Vue versions on component mount
130+ // Load available TinyRobot versions on component mount
52131onMounted (async () => {
53132 try {
54- tinyRobotVersions . value = await getVersions (' @opentiny/tiny-robot' , {
133+ const { versions, lastVersion } = await getVersions (' @opentiny/tiny-robot' , {
55134 includePrerelease: true ,
56- includeLatest: false ,
135+ includeLatest: true ,
57136 })
137+ tinyRobotVersions .value = versions
138+ tinyRobotLatestVersion .value = lastVersion
58139 } catch (error ) {
59- console .error (' Failed to load Vue versions:' , error )
140+ console .error (' Failed to load TinyRobot versions:' , error )
60141 }
61142})
62143 </script >
63144
64145<template >
65146 <div class =" playground-container" >
66147 <!-- Header with Vue version selector -->
67- <Header v-model:tiny-robot-version =" tinyRobotVersion" :tiny-robot-versions =" tinyRobotVersions" />
148+ <Header
149+ v-model:tiny-robot-version =" tinyRobotVersion"
150+ :tiny-robot-versions =" tinyRobotVersions"
151+ :tiny-robot-latest-version =" tinyRobotLatestVersion"
152+ />
68153
69154 <!-- Main playground area -->
70155 <main class =" playground-main" >
0 commit comments