Skip to content

Conversation

@gene9831
Copy link
Collaborator

@gene9831 gene9831 commented Feb 4, 2026

  • Post playground-hash-change to parent when URL hash changes in iframe
  • Listen for playground-parent-url from host to deserialize hash and sync state
  • Sync TinyRobot version from import-map when loading from hash
  • Restrict postMessage to allowed origins (localhost, playground.opentiny.design)
  • Drop TINY_ROBOT_VERSION; use "latest" and derive version from hash/import-map
  • getVersions returns lastVersion; header shows "latest (x.y.z)" when applicable

PR:Playground iframe 下的 hash 同步能力

Related to opentiny/playground#21

功能概述

当 Playground 以 iframe 方式嵌入时,支持将自身的 URL hash 变化同步给父页面,保证地址栏状态与 iframe 内的示例状态尽量保持一致(便于分享与还原)。

通信方式

  • 通道:浏览器 postMessage
  • 方向单向(iframe → 父页面)
  • 触发条件:仅在 window.self !== window.top(运行在 iframe 中)时开启通信
  • 安全约束:根据 document.referrer 推导父页面 origin,并通过白名单校验,仅在受信任来源下发送消息

事件约定

playground-hash-change(iframe → 父页面)

  • 发送方:Playground(iframe 内)
  • 触发时机:Playground 将当前状态序列化到自身 URL hash 时
  • 主要字段
    • type: 'playground-hash-change'
    • url: iframe 当前完整 URL
    • hash: 序列化后的 hash(如 #code/...
  • 父页面建议处理
    • 监听该事件后,使用 history.replaceState 或路由更新,将父页面地址栏同步为最新的 Playground 状态;
    • 也可以据此更新自身路由或记录埋点等。

安全策略

发送 playground-hash-change 事件前,会基于以下策略决定是否发送以及发送目标 origin

  • 通过 document.referrer 推导父页面 URL,并取其 origin
  • 仅当该 origin 满足以下任一条件时才发送:
    • https://playground.opentiny.design
    • localhost(任意端口)
    • 127.0.0.1(任意端口)
  • 对于不在白名单内的来源,将 跳过发送,避免在恶意父页面中泄露 Playground 状态。

Summary by CodeRabbit

  • New Features

    • Share button to copy a permalink for embedded playgrounds.
    • In-app notifications for copy/feedback messages.
  • Improvements

    • Secure parent-child messaging and URL/hash persistence when embedded.
    • Playground now shows the actual latest TinyRobot version in the selector and falls back to "latest" when unavailable.
    • CSS edits now trigger a full recompile so style changes apply reliably.

- Post playground-hash-change to parent when URL hash changes in iframe
- Listen for playground-parent-url from host to deserialize hash and sync state
- Sync TinyRobot version from import-map when loading from hash
- Restrict postMessage to allowed origins (localhost, playground.opentiny.design)
- Drop __TINY_ROBOT_VERSION__; use "latest" and derive version from hash/import-map
- getVersions returns lastVersion; header shows "latest (x.y.z)" when applicable
@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

Warning

Rate limit exceeded

@gene9831 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 53 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Remove build-time version injection; add runtime sync of TinyRobot version from the playground import-map, iframe host messaging with origin validation and URL/hash persistence, and surface the latest TinyRobot release alongside available versions.

Changes

Cohort / File(s) Summary
Vite Configuration
docs/.vitepress/config.mts, packages/playground/vite.config.ts
Removed import of package.json and the define that injected __TINY_ROBOT_VERSION__; build-time version injection eliminated.
Docs Theme / Window
docs/.vitepress/theme/index.ts
Removed global __TINY_ROBOT_VERSION__ augmentation; replaced runtime reads with a hardcoded 'latest' fallback.
Playground App Core
packages/playground/src/App.vue
Added import-map-based tinyRobot version synchronization, iframe host communication (origin whitelist), URL/hash persistence with parent postMessage, forced full recompile trigger for CSS updates, lifecycle message listener registration; added methods syncTinyRobotVersionFromImportMap(), triggerFullRecompile(), isAllowedMessageOrigin(), onHostMessage(), and tinyRobotLatestVersion ref.
Header Component
packages/playground/src/Header.vue
Added optional tinyRobotLatestVersion prop; updated version dropdown label to show latest concrete version when 'latest' is selected; added share button and clipboard/share URL logic; layout/styling adjustments.
Version Management Utility
packages/playground/src/utils/versions.ts
Introduced VersionsResult type { versions, lastVersion }; changed getVersions() to return VersionsResult; updated cache to Map<string, VersionsResult> and improved error handling.
Playground Components & Utilities
packages/playground/src/components/IconShare.vue, packages/playground/src/components/Notification.vue, packages/playground/src/utils/notify.ts
Added IconShare component, Notification component, and notify() utility for in-app transient notifications; programmatic mount/unmount behavior for notifications.
Misc (playground imports/props)
packages/playground/src/*
Adjusted imports (removed File from @vue/repl, added onBeforeUnmount), updated prop passing to Header to include tiny-robot-latest-version, and integrated version sync into store/load flows.

Sequence Diagram

sequenceDiagram
    participant Host
    participant App as App.vue
    participant Store as Store (files / import-map.json)
    participant VersionAPI as Version API
    participant Header as Header.vue

    Host->>App: postMessage(hash, origin)
    App->>App: onHostMessage() / isAllowedMessageOrigin(origin)
    App->>App: Deserialize hash → update Store
    App->>Store: Read `import-map.json` (`@opentiny/tiny-robot` URL)
    App->>App: syncTinyRobotVersionFromImportMap()
    App->>VersionAPI: getVersions(pkg)
    VersionAPI-->>App: { versions[], lastVersion }
    App->>App: set tinyRobotLatestVersion, update state
    App->>App: triggerFullRecompile()
    App->>Host: postMessage(newHash, targetOrigin)
    App->>Header: pass tiny-robot-version, tiny-robot-versions, tiny-robot-latest-version
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through maps and versions bright,
I swapped the build-time for runtime light,
I whisper hashes to the host with care,
I fetch the latest and show it there,
A tiny rabbit syncing code at night. 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main feature being added: iframe hash synchronization support for the playground component.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/playground/src/App.vue`:
- Around line 60-67: The outgoing postMessage call in the watchEffect block
leaks state by using '*' as the target origin; update the code to validate
window.parent.origin (or derive a safe origin from document.referrer) against
the ALLOWED_ORIGINS via the isAllowedMessageOrigin function and only call
window.parent.postMessage({ type: 'playground-hash-change', ... }, parentOrigin)
when the parentOrigin is allowed; if the origin is not allowed, skip posting.
Move the ALLOWED_ORIGINS and isAllowedMessageOrigin definitions above the
watchEffect so they can be reused.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

✅ Preview build completed successfully!

Click the image above to preview.
Preview will be automatically removed when this PR is closed.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

- Implement getParentOrigin to derive the parent page's origin using document.referrer
- Update postMessage to use the derived target origin for improved security
- Consolidate origin validation logic for better maintainability
- Eliminate onHostMessage function and associated event listeners to streamline code
- Retain essential lifecycle hooks and imports for component functionality
- Introduce IconShare component for sharing URLs
- Implement getShareUrl function to generate shareable links based on iframe context
- Add handleShareClick method to copy share URL to clipboard with user notification
- Create Notification component for displaying messages with auto-hide functionality
- Develop notify utility function to programmatically show notifications
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/playground/src/components/Notification.vue`:
- Around line 21-33: The mounted hook schedules a requestAnimationFrame and two
setTimeouts (in onMounted) but never clears them, so add cleanup in an
onUnmounted handler: store the requestAnimationFrame id and both timeout ids
(the show rAF, the auto-hide timeout, and the inner emit timeout) as local
refs/vars, call cancelAnimationFrame for the rAF id and clearTimeout for both
timeouts inside onUnmounted, and guard the emit('close') callback if needed to
avoid emitting after unmount; update the handlers that use visible and emit (the
requestAnimationFrame callback, the outer setTimeout using props.duration, and
the inner setTimeout that calls emit('close')) to use those stored ids so they
can be cleared on unmount.

In `@packages/playground/src/Header.vue`:
- Around line 48-62: The catch block in handleShareClick currently does nothing,
so when navigator.clipboard.writeText fails users get no feedback; update
handleShareClick to implement the promised fallback: after catching the error,
present the URL to the user (e.g., via window.prompt or a visible modal) so they
can manually copy it and call notify to inform them of the fallback action; use
getShareUrl() to fetch the URL, include the error message in console.error for
debugging, and ensure notify('链接已复制到剪贴板') is replaced or supplemented with a
different message like notify('请手动复制链接') when using the fallback.
🧹 Nitpick comments (1)
packages/playground/src/Header.vue (1)

23-45: Hardcoded pathname may require future maintenance.

The pathname /tiny-robot.html is hardcoded at line 35. If the parent page URL structure changes, this will need to be updated. Consider extracting this to a constant or making it configurable if flexibility is needed.

- Update getShareUrl function to handle iframe and non-iframe contexts more clearly
- Simplify error handling in getShareUrl
- Enhance Notification component to manage animation frames and timers for better performance
- Ensure cleanup of timers on component unmount to prevent memory leaks
@hexqi hexqi merged commit ca832d1 into opentiny:develop Feb 4, 2026
4 checks passed
@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

🧹 Preview Cleaned Up

The preview deployment has been removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants