Skip to content

feat: preview support auto update#1327

Merged
hexqi merged 6 commits intoopentiny:release/2.5.0-alpha.6from
chilingling:feat/previewSupportAutoUpdate
Apr 24, 2025
Merged

feat: preview support auto update#1327
hexqi merged 6 commits intoopentiny:release/2.5.0-alpha.6from
chilingling:feat/previewSupportAutoUpdate

Conversation

@chilingling
Copy link
Member

@chilingling chilingling commented Apr 24, 2025

English | 简体中文

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • Built its own designer, fully self-validated

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

Background and solution

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features

    • Improved preview window management with support for custom preview URLs and history mode.
    • Enhanced communication and data synchronization between preview and main windows.
    • Added dynamic breadcrumb updates in the preview toolbar based on the current page state.
  • Bug Fixes

    • Prevented duplicate CSS insertions in generated preview code.
  • Refactor

    • Simplified and centralized preview logic for easier maintenance and more robust lifecycle handling.
    • Streamlined preview invocation across plugins and toolbars.
  • Chores

    • Updated and reorganized dependencies in package configurations.
    • Added type definitions for development purposes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 24, 2025

Walkthrough

This update introduces a comprehensive refactor and enhancement of the preview system across multiple packages. The preview window management is overhauled to support history mode, robust schema synchronization, and improved communication between the main application and preview windows using postMessage and BroadcastChannel APIs. The preview data lifecycle is modularized with new composables for data fetching, code generation, and communication, replacing legacy logic in Vue components. API utilities are restructured for clarity and flexibility, with new functions for fetching page and block data. The preview invocation logic in plugins and toolbars is simplified, and dependency management is updated in several package manifests.

Changes

File(s) or Path(s) Change Summary
packages/common/js/preview.js Refactored preview management: added history mode, schema synchronization, preview window lifecycle, message listeners, and removed previewBlock. Changed previewPage signature and exported setupSchemaChangeListener.
designer-demo/src/preview.js Added previewHotReload: false to preview config, controlling hot reload behavior.
packages/common/package.json Moved @vueuse/core from devDependencies to dependencies.
packages/design-core/package.json Added @types/babel__core to devDependencies.
packages/design-core/src/preview/src/Toolbar.vue Made breadcrumbs reactive to current page changes, removed static URL-based logic.
packages/design-core/src/preview/src/preview/Preview.vue Refactored to use new composables for preview data and communication; removed legacy schema and code generation logic.
packages/design-core/src/preview/src/preview/generate.js Updated processAppJsCode to avoid duplicate CSS insertions and handle empty CSS lists.
packages/design-core/src/preview/src/preview/http.js Removed getSearchParams; added API functions: getPageById, getBlockById, fetchPageHistory.
packages/design-core/src/preview/src/preview/importMap.js Changed getImportMap to accept a scripts parameter; removed dependency on getSearchParams.
packages/design-core/src/preview/src/preview/usePreviewCommunication.ts New module for preview/main window communication via postMessage and BroadcastChannel; provides lifecycle hooks.
packages/design-core/src/preview/src/preview/usePreviewData.ts New module for reactive preview data management, code generation, file updates, and URL sync.
packages/plugins/block/src/BlockSetting.vue Simplified preview logic: switched from previewBlock to previewPage, updated argument structure.
packages/plugins/page/src/PageHistory.vue Simplified preview logic: reduced params for previewPage, removed theme and meta dependencies.
packages/plugins/page/src/composable/usePage.ts Refactored page content update and ancestry logic, changed function signatures for clarity and separation of concerns.
packages/toolbars/preview/meta.js Added previewUrl property to options object in config.
packages/toolbars/preview/src/Main.vue Simplified preview invocation: removed block/page distinction, now calls previewPage() directly, cleaned up imports.

Sequence Diagram(s)

sequenceDiagram
    participant MainApp
    participant PreviewWindow

    MainApp->>PreviewWindow: Open preview window (with config and params)
    PreviewWindow-->>MainApp: postMessage (ready/mounted)
    MainApp->>PreviewWindow: postMessage (schema data)
    PreviewWindow->>PreviewWindow: Update preview content
    MainApp-->>PreviewWindow: postMessage (schema updates, throttled)
    PreviewWindow->>PreviewWindow: React to schema changes (if hot reload enabled)
Loading
sequenceDiagram
    participant PreviewWindow
    participant API

    PreviewWindow->>API: fetchPageById / fetchBlockById / fetchPageHistory
    API-->>PreviewWindow: Return page/block/history data
    PreviewWindow->>PreviewWindow: Generate code, update files, update import map
    PreviewWindow->>PreviewWindow: Update reactive preview state
Loading

Poem

In the warren of code, a preview anew,
Windows now chatter, as rabbits would do.
With schemas that sync and breadcrumbs that gleam,
History and blocks join the previewing dream.
Messages hop from parent to child,
As files and styles are carefully compiled.
🐇✨ Here's to the preview, robust and bright—
May every page render just right!

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added the enhancement New feature or request label Apr 24, 2025
Copy link
Contributor

@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: 11

🧹 Nitpick comments (4)
packages/design-core/src/preview/src/Toolbar.vue (1)

48-57: Null-safe DOM access in setViewPort

document.getElementById('app') can be null, which causes app.style access to throw. Guard against the null case:

-      const app = document.getElementById('app')
+      const app = document.getElementById('app')
 ...
-      app.style.overflow = 'hidden'
+      if (app) {
+        app.style.overflow = 'hidden'
+      }
packages/design-core/src/preview/src/preview/Preview.vue (1)

56-58: Reuse parsed query parameters

queryParams is created once, but later new URLSearchParams(location.search) is rebuilt inside onSchemaReceivedAction.
Reusing the original instance (or a reactive wrapper) avoids redundant parsing and keeps the code DRY.

packages/design-core/src/preview/src/preview/usePreviewData.ts (1)

90-99: Avoid potential infinite recursion / stack overflow

getPageRecursively stops only when parentId === '0'. If an unexpected circular reference is introduced in the data layer (e.g., A → B → A), this will recurse forever. Recommend tracking visited IDs:

-const getPageRecursively = async (id: string): Promise<IPage[]> => {
-  const page = await getPageById(id)
-
-  if (page.parentId === '0') {
-    return [page]
-  }
-
-  return [page, ...(await getPageRecursively(page.parentId))]
-}
+const getPageRecursively = async (id: string, visited = new Set<string>()): Promise<IPage[]> => {
+  if (visited.has(id)) {
+    throw new Error(`Detected circular parentId reference while traversing ancestry for page ${id}`)
+  }
+  visited.add(id)
+
+  const page = await getPageById(id)
+  if (page.parentId === '0') {
+    return [page]
+  }
+  return [page, ...(await getPageRecursively(String(page.parentId), visited))]
+}

Prevents rare but serious runaway recursion.

packages/plugins/page/src/composable/usePage.ts (1)

510-531: Minor optimisations & safety when building familyPages

  1. filter((item) => item.isPage) may drop currentPage if isPage happens to be falsy/undefined in the cached tree.
    Consider explicitly including currentPage at the head of the array to guarantee its presence.

  2. Generating the extended object for every ancestor allocates new objects on each call.
    If this function is invoked frequently (e.g. during auto-update polling) you can avoid churn by memoising or by re-using the original node and augmenting it lazily when details are fetched.

  3. Depending on UX requirements, you might pre-fetch page_content for all ancestors in a single batch endpoint to cut N+1 network chatter.

These are optional tweaks that can smooth edge cases and improve throughput.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 030ece7 and 105e511.

📒 Files selected for processing (16)
  • designer-demo/src/preview.js (1 hunks)
  • packages/common/js/preview.js (1 hunks)
  • packages/common/package.json (1 hunks)
  • packages/design-core/package.json (1 hunks)
  • packages/design-core/src/preview/src/Toolbar.vue (2 hunks)
  • packages/design-core/src/preview/src/preview/Preview.vue (2 hunks)
  • packages/design-core/src/preview/src/preview/generate.js (1 hunks)
  • packages/design-core/src/preview/src/preview/http.js (1 hunks)
  • packages/design-core/src/preview/src/preview/importMap.js (1 hunks)
  • packages/design-core/src/preview/src/preview/usePreviewCommunication.ts (1 hunks)
  • packages/design-core/src/preview/src/preview/usePreviewData.ts (1 hunks)
  • packages/plugins/block/src/BlockSetting.vue (2 hunks)
  • packages/plugins/page/src/PageHistory.vue (1 hunks)
  • packages/plugins/page/src/composable/usePage.ts (2 hunks)
  • packages/toolbars/preview/meta.js (1 hunks)
  • packages/toolbars/preview/src/Main.vue (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
packages/design-core/src/preview/src/preview/importMap.js (1)
Learnt from: yy-wow
PR: opentiny/tiny-engine#940
File: packages/canvas/DesignCanvas/src/importMap.js:51-51
Timestamp: 2025-01-13T03:46:13.817Z
Learning: The `getImportMapData` function in `packages/canvas/DesignCanvas/src/importMap.js` has default parameter handling that initializes `canvasDeps` with empty arrays for `scripts` and `styles`, making additional null checks unnecessary.
🧬 Code Graph Analysis (1)
packages/design-core/src/preview/src/preview/http.js (3)
packages/register/src/common.ts (1)
  • getMetaApi (54-64)
packages/register/src/constants.ts (1)
  • META_SERVICE (1-22)
packages/common/js/preview.js (1)
  • pageId (235-235)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (9)
packages/common/package.json (1)

42-42: Dependency update for enhanced preview functionality

Moving @vueuse/core from devDependencies to dependencies is appropriate as the preview system now directly uses VueUse's reactive utilities at runtime.

packages/toolbars/preview/meta.js (1)

9-10: Support for custom preview URLs added

Adding the previewUrl property enables configuring custom preview URLs, which aligns well with the overall preview system enhancements.

packages/design-core/package.json (1)

106-106: TypeScript typings added for Babel core

Adding @types/babel__core as a development dependency improves TypeScript support for the preview system's code generation and transformation features.

packages/plugins/page/src/PageHistory.vue (1)

57-63: Simplified preview parameters with improved structure

The previewPage function now receives a more streamlined parameter object and explicit history mode flag. This change aligns with the broader preview system refactoring that centralizes preview handling across the application.

packages/design-core/src/preview/src/preview/importMap.js (1)

31-35: Improved modularity and explicit script injection

The function now accepts scripts directly as a parameter with a default empty object, rather than relying on URL search parameters. This change:

  1. Makes the code more modular and testable
  2. Creates a more explicit data flow for script dependencies
  3. Aligns with the broader architectural updates that remove implicit URL parameter dependencies

This is a good architectural improvement.

packages/design-core/src/preview/src/preview/http.js (1)

48-51: Added new API utilities for preview data management

These new functions provide clear, dedicated API endpoints for fetching page and block data for the preview functionality. They follow a consistent pattern and support the enhanced preview data management introduced in the related files.

packages/plugins/block/src/BlockSetting.vue (2)

90-93: Simplified imports and aligned with new preview approach

The imports have been updated to remove unused items and include the previewPage function that now centralizes preview handling across plugins.


249-258: Unified preview handling with consistent history mode support

The previewHistory function has been updated to use previewPage with a simplified parameter structure and explicit history mode flag (true). This change:

  1. Aligns with the centralized preview handling approach
  2. Standardizes how history previews are managed across plugins
  3. Removes unnecessary parameters like framework, platform, and theme

The change is consistent with similar updates in other plugins, indicating a systematic refactoring of the preview system.

packages/design-core/src/preview/src/preview/Preview.vue (1)

61-71:

Details

✅ Verification successful

Race-condition when calling cleanupCommunicationAction()

cleanupCommunicationAction is assigned only after usePreviewCommunication returns.
If onSchemaReceivedAction fires synchronously during usePreviewCommunication initialisation, the variable is still null and the call will throw.

Safeguard the call:

-      if (isHistory || previewHotReload === false) {
-        cleanupCommunicationAction()
+      if (isHistory || previewHotReload === false) {
+        cleanupCommunicationAction?.()
       }

Optionally initialise the variable with a no-op () => {} to eliminate the check.
Please verify that usePreviewCommunication cannot emit events before returning, or adopt this guard.


#!/bin/bash
set -e
# Show usage of usePreviewCommunication with context
rg -n -C3 "usePreviewCommunication" packages/design-core/src/preview/src/preview/Preview.vue

# Show assignments and references to cleanupCommunicationAction with context
rg -n -C3 "cleanupCommunicationAction" packages/design-core/src/preview/src/preview/Preview.vue

#!/bin/bash
set -e

echo "=== Searching for usePreviewCommunication occurrences ==="
rg -n "usePreviewCommunication" -C3

echo
echo "=== Finding definition of usePreviewCommunication ==="
rg -n "export (const|function) usePreviewCommunication" -C3

echo
echo "=== Locating files that reference usePreviewCommunication ==="
rg -l "usePreviewCommunication" -g "*.ts" -g "*.js" -g "*.vue" | while read -r file; do
  echo
  echo "----- $file -----"
  sed -n '1,200p' "$file"
done

🏁 Script executed:

#!/bin/bash
set -e

echo "=== Searching for usePreviewCommunication definitions and calls ==="
rg -n -C5 "usePreviewCommunication" .

echo
echo "=== Searching for cleanupCommunicationAction assignments/references ==="
rg -n -C5 "cleanupCommunicationAction" .

Length of output: 6688


#!/bin/bash
set -e
# Display the first 200 lines of usePreviewCommunication.ts for inspection
sed -n '1,200p' packages/design-core/src/preview/src/preview/usePreviewCommunication.ts

No race condition here – cleanupCommunicationAction is always assigned before any messages can arrive

The component’s setup does the following in order:

  1. Destructure initCommunication and cleanupCommunication from usePreviewCommunication
  2. Immediately assign cleanupCommunicationAction = cleanupCommunication
  3. Register initCommunication on onMounted

Because the assignment to cleanupCommunicationAction happens synchronously before mounting (and thus before any messages can be received), it can never be null when onSchemaReceivedAction runs. No optional‐chaining guard or no‐op initializer is required.

metas: [HttpService, GenerateCodeService]
},
config: { id: 'engine.config', theme: 'light' },
config: { id: 'engine.config', theme: 'light', previewHotReload: false },
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify if hot reload should be disabled by default

Adding previewHotReload: false explicitly disables hot reload for the preview in the demo environment. Consider whether this is the desired default behavior for demonstration purposes.


🏁 Script executed:

#!/bin/bash
# Check if there are any comments or documentation explaining the default hot reload behavior
rg -i "previewHotReload" --type js --type ts --type vue -C 3

# Check if other demo files use different settings
rg -i "previewHotReload.*true" --type js --type ts --type vue

Length of output: 180


Let’s search without restricting file types to see if this setting appears elsewhere:


🏁 Script executed:

#!/bin/bash
# Search for any occurrences of previewHotReload
rg -i previewHotReload -C 3 .

# Search for generic hot reload flags
rg -i hotReload -C 3 .

Length of output: 3124


Clarify previewHotReload default behavior

The previewHotReload option defaults to live‐update (true) when not specified. In the demo you’ve explicitly turned it off:

// designer-demo/src/preview.js:28
config: { id: 'engine.config', theme: 'light', previewHotReload: false },

Please decide which you intend for the demo:

  • Remove the previewHotReload: false override so the preview hot‐reload remains enabled by default.
  • Or keep it disabled but add a comment explaining why live updates are not desired in this environment.

Comment on lines +35 to 47
const { setBreadcrumbPage, setBreadcrumbBlock } = useBreadcrumb()

const { setBreadcrumbPage } = useBreadcrumb()
const { pageInfo } = getSearchParams()
setBreadcrumbPage([pageInfo?.name])
watch(
() => previewState.currentPage,
(newVal) => {
if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
} else {
setBreadcrumbPage([newVal?.name])
}
}
)

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Ensure breadcrumb is initialized on first render

watch will not run until previewState.currentPage changes, so if currentPage is already populated when the component mounts the breadcrumb stays empty.
Add { immediate: true } to invoke the callback once during setup.

-    watch(
-      () => previewState.currentPage,
-      (newVal) => {
+    watch(
+      () => previewState.currentPage,
+      (newVal) => {
         if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
           setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
         } else {
           setBreadcrumbPage([newVal?.name])
         }
-      }
+      },
+      { immediate: true }
     )

🏁 Script executed:

#!/bin/bash
# Locate Toolbar.vue to confirm import and signature of watch
fd --extension vue Toolbar.vue

# Inspect the top of the file to see how watch is imported
sed -n '1,20p' packages/design-core/src/preview/src/Toolbar.vue

Length of output: 959


Add immediate option to breadcrumb watcher
To ensure the breadcrumb initializes on mount, invoke the watcher immediately by adding { immediate: true }:

File: packages/design-core/src/preview/src/Toolbar.vue
Lines: ~35

 watch(
   () => previewState.currentPage,
   (newVal) => {
     if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
       setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
     } else {
       setBreadcrumbPage([newVal?.name])
     }
-  }
+  }, 
+  { immediate: true }
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { setBreadcrumbPage, setBreadcrumbBlock } = useBreadcrumb()
const { setBreadcrumbPage } = useBreadcrumb()
const { pageInfo } = getSearchParams()
setBreadcrumbPage([pageInfo?.name])
watch(
() => previewState.currentPage,
(newVal) => {
if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
} else {
setBreadcrumbPage([newVal?.name])
}
}
)
const { setBreadcrumbPage, setBreadcrumbBlock } = useBreadcrumb()
watch(
() => previewState.currentPage,
(newVal) => {
if (newVal?.page_content?.componentName === constants.COMPONENT_NAME.Block) {
setBreadcrumbBlock([newVal?.name_cn || newVal?.page_content?.fileName])
} else {
setBreadcrumbPage([newVal?.name])
}
},
{ immediate: true }
)

Comment on lines 153 to +166
export const processAppJsCode = (code, cssList) => {
return `${code}${cssList.map((css) => `addCss('${css}')`).join('\n')}`
if (!Array.isArray(cssList) || !cssList.length) {
return code
}

let res = `${code}\n`

cssList.forEach((css) => {
if (!code.includes(`addCss('${css}')`)) {
res += `addCss('${css}')\n`
}
})

return res
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid duplicate/unsafe addCss injections

  1. If cssList itself contains duplicates, identical addCss() lines will still be appended multiple times because the check only inspects the original code, not the growing res.
  2. Direct string interpolation breaks if the path contains a single quote.

Consider de-duplicating first and using JSON.stringify for safe quoting:

-  cssList.forEach((css) => {
-    if (!code.includes(`addCss('${css}')`)) {
-      res += `addCss('${css}')\n`
-    }
-  })
+  Array.from(new Set(cssList)).forEach((css) => {
+    const call = `addCss(${JSON.stringify(css)})`
+    if (!res.includes(call)) {
+      res += `${call}\n`
+    }
+  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const processAppJsCode = (code, cssList) => {
return `${code}${cssList.map((css) => `addCss('${css}')`).join('\n')}`
if (!Array.isArray(cssList) || !cssList.length) {
return code
}
let res = `${code}\n`
cssList.forEach((css) => {
if (!code.includes(`addCss('${css}')`)) {
res += `addCss('${css}')\n`
}
})
return res
export const processAppJsCode = (code, cssList) => {
if (!Array.isArray(cssList) || !cssList.length) {
return code
}
let res = `${code}\n`
Array.from(new Set(cssList)).forEach((css) => {
const call = `addCss(${JSON.stringify(css)})`
if (!res.includes(call)) {
res += `${call}\n`
}
})
return res
}

Comment on lines +61 to 62
previewPage()

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Await previewPage() to propagate errors

previewPage() is async (opens window, waits for post-message, etc.).
Without await, any rejection is unhandled and the surrounding try/catch will miss it.

-      previewPage()
+      await previewPage()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
previewPage()
await previewPage()

Comment on lines +370 to +375
const newFiles = store.getFiles()
const searchParams = new URLSearchParams(location.search)
const appJsCode = processAppJsCode(newFiles['app.js'], JSON.parse(searchParams.get('styles') || '[]'))

newFiles['app.js'] = appJsCode

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against missing app.js to prevent runtime crash

processAppJsCode(newFiles['app.js'], …) will throw if app.js hasn’t been written yet (e.g., the very first run or store corruption). Add a defensive fallback:

-const appJsCode = processAppJsCode(newFiles['app.js'], JSON.parse(searchParams.get('styles') || '[]'))
-
-newFiles['app.js'] = appJsCode
+const rawAppJs = newFiles['app.js'] || ''
+const stylesArr = JSON.parse(searchParams.get('styles') || '[]')
+newFiles['app.js'] = processAppJsCode(rawAppJs, stylesArr)

This prevents a hard error path that breaks the entire preview render.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const newFiles = store.getFiles()
const searchParams = new URLSearchParams(location.search)
const appJsCode = processAppJsCode(newFiles['app.js'], JSON.parse(searchParams.get('styles') || '[]'))
newFiles['app.js'] = appJsCode
const newFiles = store.getFiles()
const searchParams = new URLSearchParams(location.search)
- const appJsCode = processAppJsCode(newFiles['app.js'], JSON.parse(searchParams.get('styles') || '[]'))
-
- newFiles['app.js'] = appJsCode
+ const rawAppJs = newFiles['app.js'] || ''
+ const stylesArr = JSON.parse(searchParams.get('styles') || '[]')
+ newFiles['app.js'] = processAppJsCode(rawAppJs, stylesArr)

Comment on lines +10 to +18
const handleMessage = async (event: MessageEvent) => {
const parsedOrigin = new URL(event.origin)
const parsedHost = new URL(window.location.href)

if (parsedOrigin.origin === parsedHost.origin || parsedOrigin.host === parsedHost.host) {
const { type, data, source } = event.data || {}

if (source === 'designer' && type === 'schema' && data && onSchemaReceivedAction) {
await onSchemaReceivedAction(data)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

new URL(event.origin) throws for "null" origins

For file-scheme pages, sandboxed iframes, or browsers that send "null" as the origin, new URL('null') raises a TypeError, breaking message handling completely. Validate before parsing:

-const parsedOrigin = new URL(event.origin)
-const parsedHost = new URL(window.location.href)
+if (event.origin === 'null') {
+  return // unsupported / untrusted origin
+}
+const parsedOrigin = new URL(event.origin)
+const parsedHost = new URL(window.location.href)

Apply the same guard in handleBroadcastMessage if you later parse origins there.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleMessage = async (event: MessageEvent) => {
const parsedOrigin = new URL(event.origin)
const parsedHost = new URL(window.location.href)
if (parsedOrigin.origin === parsedHost.origin || parsedOrigin.host === parsedHost.host) {
const { type, data, source } = event.data || {}
if (source === 'designer' && type === 'schema' && data && onSchemaReceivedAction) {
await onSchemaReceivedAction(data)
const handleMessage = async (event: MessageEvent) => {
if (event.origin === 'null') {
return // unsupported / untrusted origin
}
const parsedOrigin = new URL(event.origin)
const parsedHost = new URL(window.location.href)
if (parsedOrigin.origin === parsedHost.origin || parsedOrigin.host === parsedHost.host) {
const { type, data, source } = event.data || {}
if (source === 'designer' && type === 'schema' && data && onSchemaReceivedAction) {
await onSchemaReceivedAction(data)

Comment on lines +288 to 296
if (previewWindow && !previewWindow.closed) {
// 如果预览窗口存在,则聚焦预览窗口
previewWindow.focus()
return
}

// 打开新窗口并保存引用
previewWindow = window.open(openUrl, '_blank')

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Existing preview window is focused but not refreshed

When the preview window already exists, open() simply focuses it and returns. Query-string changes (tenant, theme, history, etc.) or a full schema refresh will not be reflected until the next schema mutation occurs, leading to stale previews.

Minimal fix – trigger an immediate sync before early-return:

-  if (previewWindow && !previewWindow.closed) {
-    // 如果预览窗口存在,则聚焦预览窗口
-    previewWindow.focus()
-    return
-  }
+  if (previewWindow && !previewWindow.closed) {
+    previewWindow.focus()
+    // force immediate schema push so that deps / params stay in sync
+    getSchemaParams().then(sendSchemaUpdate).catch(console.error)
+    return
+  }

Ensures the user sees the latest state without needing an extra schema-changing action.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (previewWindow && !previewWindow.closed) {
// 如果预览窗口存在,则聚焦预览窗口
previewWindow.focus()
return
}
// 打开新窗口并保存引用
previewWindow = window.open(openUrl, '_blank')
if (previewWindow && !previewWindow.closed) {
previewWindow.focus()
// force immediate schema push so that deps / params stay in sync
getSchemaParams().then(sendSchemaUpdate).catch(console.error)
return
}
// 打开新窗口并保存引用
previewWindow = window.open(openUrl, '_blank')

Comment on lines +165 to +172
window.addEventListener('message', async (event) => {
const parsedOrigin = new URL(event.origin)
const parsedHost = new URL(window.location.href)
// 确保消息来源安全
if (parsedOrigin.origin === parsedHost.origin || parsedOrigin.host === parsedHost.host) {
const { event: eventType, source } = event.data || {}
// 通过 heartbeat 消息来重新建立连接,避免刷新页面后 previewWindow 为 null
if (source === 'preview' && eventType === 'connect' && !previewWindow) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

event.origin === "null" again crashes URL parsing

Same issue described for usePreviewCommunication.ts: parsing "null" with new URL() fails. Add an early guard:

-    const parsedOrigin = new URL(event.origin)
-    const parsedHost = new URL(window.location.href)
+    if (event.origin === 'null') return
+    const parsedOrigin = new URL(event.origin)
+    const parsedHost = new URL(window.location.href)

Prevents silent failures when the designer is served from file:// or similar contexts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.addEventListener('message', async (event) => {
const parsedOrigin = new URL(event.origin)
const parsedHost = new URL(window.location.href)
// 确保消息来源安全
if (parsedOrigin.origin === parsedHost.origin || parsedOrigin.host === parsedHost.host) {
const { event: eventType, source } = event.data || {}
// 通过 heartbeat 消息来重新建立连接,避免刷新页面后 previewWindow 为 null
if (source === 'preview' && eventType === 'connect' && !previewWindow) {
window.addEventListener('message', async (event) => {
+ if (event.origin === 'null') return
const parsedOrigin = new URL(event.origin)
const parsedHost = new URL(window.location.href)
// 确保消息来源安全
if (parsedOrigin.origin === parsedHost.origin || parsedOrigin.host === parsedHost.host) {
const { event: eventType, source } = event.data || {}
// 通过 heartbeat 消息来重新建立连接,避免刷新页面后 previewWindow 为 null
if (source === 'preview' && eventType === 'connect' && !previewWindow) {
// ...
}
}
})

Comment on lines +466 to 471
const updatePageContent = (familyPages: { id: any; page_content: any }[], currentPage: { id: string; page_content?: any }) => {
const currentPageSchema = familyPages.find((item) => item.id === currentPage.id)
// 替换为当前页面最新的 schema
if (currentPageSchema) {
currentPageSchema.page_content = currentPage.page_content
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make id comparison type-safe & protect against undefined schema overwrite

familyPages.find((item) => item.id === currentPage.id) performs a strict comparison between two values that may be either string or number throughout the codebase (PageData.id is declared as string | number).
If one side is "1" and the other is 1, the match silently fails and the schema is never refreshed.

In addition, when currentPage.page_content is undefined (e.g. when the caller forgot to attach it), the current logic replaces a previously-fetched schema with undefined, leaving the preview in an inconsistent state.

-  const currentPageSchema = familyPages.find((item) => item.id === currentPage.id)
-  // 替换为当前页面最新的 schema
-  if (currentPageSchema) {
-    currentPageSchema.page_content = currentPage.page_content
-  }
+  const currentPageSchema = familyPages.find(
+    (item) => String(item.id) === String(currentPage.id) // tolerate number↔string
+  )
+  // 替换为当前页面最新的 schema(仅当有值时)
+  if (currentPageSchema && currentPage.page_content) {
+    currentPageSchema.page_content = currentPage.page_content
+  }

This guards against both the type mismatch and accidental loss of a valid schema.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updatePageContent = (familyPages: { id: any; page_content: any }[], currentPage: { id: string; page_content?: any }) => {
const currentPageSchema = familyPages.find((item) => item.id === currentPage.id)
// 替换为当前页面最新的 schema
if (currentPageSchema) {
currentPageSchema.page_content = currentPage.page_content
}
const updatePageContent = (
familyPages: { id: any; page_content: any }[],
currentPage: { id: string; page_content?: any }
) => {
const currentPageSchema = familyPages.find(
(item) => String(item.id) === String(currentPage.id) // tolerate number↔string
)
// 替换为当前页面最新的 schema(仅当有值时)
if (currentPageSchema && currentPage.page_content) {
currentPageSchema.page_content = currentPage.page_content
}
}

Comment on lines +492 to 503
const handlePageDetail = async (pages: any[]) => {
const { ROOT_ID } = pageSettingState

if (pages.length > 0) {
await Promise.all(
pages.map(async (page, index) => {
updatePageContent(page, currentPage)
await fetchPageDetailIfNeeded(page)
updateParentId(page, pages, index, ROOT_ID)
})
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Isolate network failures so one bad page does not break the whole family fetch

Promise.all aborts on the first rejection. If a single ancestor fails to load (e.g. network hiccup, 404), no page detail is returned and the caller of getFamily receives a rejected promise, potentially breaking page-switch operations and the preview.

A safer pattern is to use Promise.allSettled (or wrap each call in a catch) so that you can still return the pages that were loaded successfully and surface individual errors via Notify or a consolidated warning.

-  if (pages.length > 0) {
-    await Promise.all(
-      pages.map(async (page, index) => {
-        await fetchPageDetailIfNeeded(page)
-        updateParentId(page, pages, index, ROOT_ID)
-      })
-    )
-  }
+  if (!pages.length) return
+
+  const results = await Promise.allSettled(
+    pages.map(async (page, index) => {
+      await fetchPageDetailIfNeeded(page)
+      updateParentId(page, pages, index, ROOT_ID)
+      return page
+    })
+  )
+
+  results
+    .filter((r): r is PromiseRejectedResult => r.status === 'rejected')
+    .forEach((err) =>
+      useNotify({ type: 'warning', message: `获取页面(${err.reason?.message ?? ''})失败,已跳过` })
+    )

This keeps the happy path responsive while still informing the user about partial failures.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handlePageDetail = async (pages: any[]) => {
const { ROOT_ID } = pageSettingState
if (pages.length > 0) {
await Promise.all(
pages.map(async (page, index) => {
updatePageContent(page, currentPage)
await fetchPageDetailIfNeeded(page)
updateParentId(page, pages, index, ROOT_ID)
})
)
}
}
const handlePageDetail = async (pages: any[]) => {
const { ROOT_ID } = pageSettingState
if (!pages.length) return
const results = await Promise.allSettled(
pages.map(async (page, index) => {
await fetchPageDetailIfNeeded(page)
updateParentId(page, pages, index, ROOT_ID)
return page
})
)
results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.forEach((err) =>
useNotify({ type: 'warning', message: `获取页面(${err.reason?.message ?? ''})失败,已跳过` })
)
}

@hexqi hexqi merged commit 20c9086 into opentiny:release/2.5.0-alpha.6 Apr 24, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants