Skip to content

feat/preview support auto update#1338

Closed
chilingling wants to merge 9 commits intoopentiny:release/2.5.0-alpha.7from
chilingling:feat/previewSupportAutoUpdate
Closed

feat/preview support auto update#1338
chilingling wants to merge 9 commits intoopentiny:release/2.5.0-alpha.7from
chilingling:feat/previewSupportAutoUpdate

Conversation

@chilingling
Copy link
Member

@chilingling chilingling commented Apr 27, 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

    • Added comprehensive documentation for the Preview API, including configuration options, URL customization, and hot reload toggling.
    • Introduced a new preview window management system supporting history mode, schema synchronization, and custom preview URLs.
    • Added a toolbar configuration option for specifying a preview URL.
  • Refactor

    • Streamlined preview logic across multiple components, unifying preview invocation and removing redundant parameters.
    • Centralized preview data handling and communication into reusable composables for improved maintainability.
    • Breadcrumbs in the preview toolbar now update reactively based on the current page.
  • Bug Fixes

    • Prevented duplicate CSS additions in generated preview code.
  • Documentation

    • Updated API documentation index and catalog to include the new Preview API entry.
  • Chores

    • Adjusted dependencies in package configurations for improved build and type support.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 27, 2025

Walkthrough

This change introduces a comprehensive refactor and enhancement of the preview functionality within the platform. The preview window management is overhauled to support custom URLs, history mode, and robust schema synchronization between the main application and the preview window using postMessage and BroadcastChannel. New Vue composables modularize preview data handling and inter-window communication, replacing direct data fetching and code generation in components. The documentation is updated to reflect these changes, including new configuration options for the preview API and hot reload toggling. Several components and utility functions are simplified, with redundant logic and dependencies removed.

Changes

File(s) Change Summary
docs/README.md, docs/api/frontend-api/preview-api.md, docs/catalog.json Added documentation entry and detailed guide for the Preview API, including configuration, custom URL, and hot reload options. Updated catalog to include the new article.
packages/common/js/preview.js Major refactor: enhanced preview window management, schema synchronization, history preview, custom URL support, removal of previewBlock, and new exports for schema listener.
packages/common/package.json Moved @vueuse/core from devDependencies to dependencies.
packages/design-core/package.json Added @types/babel__core as a devDependency.
packages/design-core/src/preview/src/Toolbar.vue Made breadcrumb reactive to current page state; removed static initialization and unused imports.
packages/design-core/src/preview/src/preview/Preview.vue Refactored to use new composables for preview data and communication; removed direct data fetching and code generation logic.
packages/design-core/src/preview/src/preview/generate.js Improved CSS dependency handling in processAppJsCode to prevent duplicates.
packages/design-core/src/preview/src/preview/http.js Removed getSearchParams; added new API functions for fetching page/block/history data.
packages/design-core/src/preview/src/preview/importMap.js Refactored getImportMap to accept scripts parameter directly; removed dependency on removed getSearchParams.
packages/design-core/src/preview/src/preview/usePreviewCommunication.ts Added new composable for preview window/main window communication using postMessage and BroadcastChannel.
packages/design-core/src/preview/src/preview/usePreviewData.ts Added new composable for managing preview data, fetching, transforming, and updating code files and state.
packages/plugins/block/src/BlockSetting.vue, packages/plugins/page/src/PageHistory.vue Simplified preview history logic; switched to using previewPage with new parameters and removed theme/metadata dependencies.
packages/plugins/page/src/composable/usePage.ts Refactored page family fetching and updating logic; updated function signatures for clarity and separation of concerns.
packages/toolbars/preview/meta.js Added previewUrl option to toolbar preview configuration.
packages/toolbars/preview/src/Main.vue Simplified preview logic; removed block/page distinction and direct metadata handling; calls previewPage directly.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Toolbar
    participant PreviewPage (Main App)
    participant PreviewWindow

    User->>Toolbar: Clicks Preview
    Toolbar->>PreviewPage: Calls previewPage(params)
    PreviewPage->>PreviewWindow: Opens or focuses preview window with URL and params
    PreviewWindow->>PreviewPage: Sends "ready" message (postMessage/BroadcastChannel)
    PreviewPage->>PreviewWindow: Sends schema and dependencies (postMessage)
    loop On schema changes
        PreviewPage->>PreviewWindow: Sends updated schema (throttled)
    end
    PreviewWindow->>PreviewPage: Requests schema update (on mount/heartbeat)
Loading

Possibly related PRs

  • opentiny/tiny-engine#1327: Extensive refactor and enhancement of preview functionality, including preview window management, schema synchronization, and hot reload toggling, with overlapping changes in preview modules.

Suggested labels

enhancement

Poem

🐇
A window opens, code takes flight,
Preview’s magic, shining bright.
Schemas sync and channels chat,
Breadcrumbs dance where pages sat.
With docs anew and logic neat,
This bunny hops to a preview beat!
🪄✨

✨ 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 documentation Improvements or additions to documentation enhancement New feature or request labels Apr 27, 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: 8

🧹 Nitpick comments (11)
packages/design-core/src/preview/src/preview/usePreviewCommunication.ts (4)

1-4: Strong type interface for preview communication.

The PreviewCommunicationOptions interface clearly defines the required callback functions. Consider making the data parameter in onSchemaReceived more specific than any for better type safety.

interface PreviewCommunicationOptions {
-  onSchemaReceived: (data: any) => Promise<void>
+  onSchemaReceived: (data: { schema?: Record<string, any>; [key: string]: any }) => Promise<void>
  loadInitialData: () => Promise<void>
}

6-9: Consider using reactive state instead of module-level variables.

Using module-level variables (onSchemaReceivedAction, previewChannel) makes testing more difficult and could cause issues if multiple instances are needed. Consider using Vue's reactive state management or a more encapsulated approach.

-let onSchemaReceivedAction: PreviewCommunicationOptions['onSchemaReceived'] | null = null
-// 创建 BroadcastChannel 实例,与主页面通信
-let previewChannel: BroadcastChannel | null = null
-
+export const usePreviewCommunication = ({
+  onSchemaReceived,
+  loadInitialData: loadInitialDataFn
+}: PreviewCommunicationOptions) => {
+  // Keep state within the composable's closure
+  let previewChannel: BroadcastChannel | null = null;

31-53: Consider adding more robust error handling.

The fallback handling in sendReadyMessage could be enhanced with more specific error types and proper error reporting rather than just console warnings.

const sendReadyMessage = () => {
  // 尝试获取父窗口引用
  const opener = window.opener

  const fallbackHandler = (reason?: string) => {
    const logger = console
-    logger.warn('无法获取主窗口引用,将使用 URL 参数初始化预览')
+    logger.warn(`无法获取主窗口引用,将使用 URL 参数初始化预览: ${reason || '未知原因'}`)
    loadInitialData?.()
  }

  if (opener) {
    try {
      opener.postMessage({ event: 'onMounted', source: 'preview' }, opener.origin || window.location.origin)
    } catch (error) {
-      fallbackHandler()
+      fallbackHandler(error instanceof Error ? error.message : String(error))
    }
    return
  }

-  fallbackHandler()
+  fallbackHandler('No opener window available')
}

66-80: Consider adding more flexible channel naming.

The BroadcastChannel name is hardcoded. Consider making it configurable, especially if multiple instances of the preview might run simultaneously.

-    previewChannel = new BroadcastChannel('tiny-engine-preview-channel')
+    const channelName = window.TINY_ENGINE_CONFIG?.previewChannelName || 'tiny-engine-preview-channel'
+    previewChannel = new BroadcastChannel(channelName)
packages/common/js/preview.js (3)

90-100: previewWindow.origin is always undefined

window.open returns a Window object that does not expose an origin property.
The right-hand side therefore always falls back to window.location.origin, and the useless left-hand operand can be removed. If the preview may eventually live on another origin, you should pass '*', otherwise keep the current origin explicitly.

-  previewWindow.postMessage(
+  const targetOrigin = window.location.origin
+  previewWindow.postMessage(
 ...
-  , previewWindow.origin || window.location.origin
-  )
+  , targetOrigin)

295-299: Open the preview window with noopener to avoid window.opener hijacking

Without noopener, the preview can change the designer’s location via window.opener.location, posing a phishing/vector risk.

-previewWindow = window.open(openUrl, '_blank')
+previewWindow = window.open(openUrl, '_blank', 'noopener=yes')

[security]


165-195: Potential listener leak: multiple imports add duplicate message listeners

setupMessageListener() is executed at module top-level. If the module gets hot-reloaded by Vite or imported twice in tests, you will attach multiple identical listeners.

Guard with a module-scoped flag:

let messageListenerAttached = false
const setupMessageListener = () => {
  if (messageListenerAttached) return
  messageListenerAttached = true
  window.addEventListener('message', ...)
}
packages/plugins/page/src/composable/usePage.ts (2)

466-472: Potential redundant mutation & missing null-guard in updatePageContent

updatePageContent always overwrites page_content when it finds the current page, even when the incoming currentPage.page_content is undefined.
This can accidentally erase a previously-fetched schema (the call to handlePageDetail may already have populated it).

-  if (currentPageSchema) {
-    currentPageSchema.page_content = currentPage.page_content
+  if (currentPageSchema && currentPage.page_content) {
+    currentPageSchema.page_content = currentPage.page_content
   }

Adding the explicit guard prevents losing data when the caller provides an incomplete currentPage object.


492-503: I/O in tight loop – fetches can be parallelised safely

handlePageDetail awaits each fetchPageDetailIfNeeded sequentially inside the map callback.
Although wrapped in Promise.all, the inner await forces serial execution, negating the parallelism.

-      pages.map(async (page, index) => {
-        await fetchPageDetailIfNeeded(page)
-        updateParentId(page, pages, index, ROOT_ID)
-      })
+      pages.map((page, index) =>
+        fetchPageDetailIfNeeded(page).then(() =>
+          updateParentId(page, pages, index, ROOT_ID)
+        )
+      )

This keeps the parent-id adjustment logic but allows the HTTP requests to run concurrently, shortening latency for long ancestor chains.

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

56-58: Minor: theme is initial-only

The theme attribute is read from the URL once on component creation.
After onSchemaReceivedAction mutates the URL (updateUrl), the <html data-theme> attribute will not follow.
If runtime theme switching is expected, watch route.query.theme or call the setter again after updateUrl.

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

49-88: URL-update logic can thrash due to unstable JSON.stringify order

JSON.stringify does not guarantee key order, so identical script/style objects can stringify differently, causing unnecessary replaceState calls.

Consider a stable stringify helper or URLSearchParams per-entry updates.

const stableStringify = (obj) =>
  JSON.stringify(Object.keys(obj).sort().reduce((a,k)=>{a[k]=obj[k];return a;},{}))

Then compare the stable strings.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 09f2c50 and b773c98.

📒 Files selected for processing (18)
  • docs/README.md (1 hunks)
  • docs/api/frontend-api/preview-api.md (1 hunks)
  • docs/catalog.json (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/usePreviewData.ts (7)
packages/design-core/src/preview/src/preview/http.js (14)
  • getPageById (48-48)
  • getPageById (48-48)
  • fetchPageHistory (50-51)
  • fetchPageHistory (50-51)
  • getBlockById (49-49)
  • getBlockById (49-49)
  • fetchImportMap (39-42)
  • fetchImportMap (39-42)
  • fetchAppSchema (44-44)
  • fetchAppSchema (44-44)
  • fetchMetaData (31-37)
  • fetchMetaData (31-37)
  • fetchBlockSchema (45-46)
  • fetchBlockSchema (45-46)
packages/design-core/src/preview/src/preview/importMap.js (2)
  • getImportMap (31-38)
  • getImportMap (31-38)
packages/register/src/common.ts (1)
  • getMetaApi (54-64)
packages/vue-generator/src/generator/page.js (1)
  • generatePageCode (464-502)
packages/design-core/src/preview/src/constant/index.js (1)
  • PanelType (13-15)
packages/design-core/src/preview/src/preview/srcFiles.js (1)
  • srcFiles (27-27)
packages/design-core/src/preview/src/preview/generate.js (3)
  • res (158-158)
  • processAppJsCode (153-167)
  • processAppJsCode (153-167)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: push-check
🔇 Additional comments (23)
packages/common/package.json (1)

42-42: Dependency package movement is justified.

Moving @vueuse/core from devDependencies to dependencies is appropriate since it's now required at runtime for the enhanced preview functionality rather than just during development.

docs/catalog.json (1)

128-129: Documentation entry added for new Preview API.

Good addition of the Preview API documentation to the catalog, ensuring users have access to the newly implemented preview functionality.

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

9-10: New previewUrl option supports custom preview URLs.

Adding the previewUrl configuration option enables custom preview URLs, aligning with the broader enhancements in the preview system across the codebase.

packages/design-core/src/preview/src/preview/usePreviewCommunication.ts (3)

10-21: Good security practice for origin validation.

The origin checking logic properly verifies that messages come from trusted sources, which is an important security practice when using postMessage.


55-64: Good cleanup handling to prevent memory leaks.

The cleanupCommunication function properly removes event listeners and closes the BroadcastChannel, which helps prevent memory leaks.


82-93: Well-structured composable with clear return values.

The usePreviewCommunication function follows the composable pattern well by returning the necessary functions for lifecycle management.

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

104-108: Type definitions look good

Adding @types/babel__core complements the existing Babel tool-chain and prevents TS2688 errors when importing @babel/core in .ts files. No further action required.

docs/README.md (1)

73-74: Nice addition to the docs index

Linking the new Preview API ensures discoverability. Verify that docs/api/frontend-api/preview-api.md exists in this PR to avoid broken links during site generation.

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

31-38: Clean refactoring to improve modularity

The function now accepts an explicit scripts parameter instead of implicitly merging with URL parameters. This decouples URL parsing from import map generation, making the data flow more explicit and modular.

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

54-64: Simplified preview handling

The preview implementation is now more streamlined by:

  1. Using the spread operator to include all properties from the history item
  2. Explicitly passing the true flag to indicate history mode

This aligns with the broader refactoring that centralizes preview functionality.

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

88-93: Simplified imports improve code clarity

The removal of unused imports (getMetaApi, META_SERVICE, and previewBlock) and replacement with the appropriate previewPage import aligns with the PR's refactoring goals.


247-259: Preview functionality consolidated effectively

The refactoring from previewBlock to previewPage with a history flag parameter simplifies the codebase by consolidating preview functionality into a more general-purpose implementation.

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

153-167: Improved CSS dependency handling

This implementation enhances the robustness and efficiency of CSS dependency handling by:

  1. Validating the CSS list input
  2. Preventing duplicate CSS inclusions
  3. Only modifying the code when necessary

This should prevent potential issues with duplicate style injections and improve performance.

packages/design-core/src/preview/src/Toolbar.vue (2)

17-23: Good implementation of reactive imports

The added imports support the new reactive approach for breadcrumb management, with watch from Vue for reactivity, constants from utilities, and previewState from the new composable.


37-46: Well-implemented reactive breadcrumb updates

This watch implementation replaces the previous static breadcrumb initialization with a reactive approach that updates the breadcrumb dynamically when the current page changes. The conditional logic correctly handles both Block components and regular pages, displaying appropriate information in each case.

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

48-51: Good API function implementation

These new API functions align well with the preview system refactoring, replacing the previous URL parameter parsing with explicit API calls. The functions follow the established pattern in the file and have clear, descriptive names.

docs/api/frontend-api/preview-api.md (4)

1-9: Clear documentation of preview URL behavior

The introduction and explanation of default URL behavior for different environments is well-written and informative.


10-26: Good documentation for static URL configuration

The documentation clearly explains how to configure a custom static URL for the preview page, with a practical example showing its implementation in a toolbar setup.


27-51: Well-documented function-based URL configuration

This section provides a clear explanation and example of using a function to dynamically modify the preview URL and query parameters, which offers greater customization capabilities.


52-65: Useful documentation for hot reload configuration

This section clearly documents the new hot reload toggle feature introduced in v2.5, explaining how to disable it if not needed, with a practical configuration example.

packages/toolbars/preview/src/Main.vue (3)

5-5: Good defensive programming with optional chaining

The addition of optional chaining for accessing the icon properties prevents potential runtime errors if these properties are undefined.


14-15: Good simplification of imports

The imports have been significantly simplified, removing unnecessary dependencies and aligning with the centralized preview logic architecture.


61-61: Excellent simplification of preview functionality

The direct call to previewPage() without arguments represents a significant simplification from the previous implementation, making the code more maintainable and aligning with the centralized preview logic in the codebase.

Comment on lines +30 to 32
// 保存预览窗口引用
let previewWindow = null

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

Global mutable previewWindow can cause race conditions in multi-tab usage

Because previewWindow is a single global reference, opening previews from two different designer tabs will overwrite each other and break the schema-sync channel for the first tab.
If multi-tab editing is a supported use-case, consider storing the reference per-designer-tab (e.g. in sessionStorage keyed by window.name) or passing the target Window instance explicitly to the messaging helpers instead of relying on a singleton.

Comment on lines +200 to +221
const handleHistoryPreview = (params, url) => {
let historyPreviewWindow = null
const handlePreviewReady = (event) => {
if (event.origin === window.location.origin || event.origin.includes(window.location.hostname)) {
const { event: eventType, source } = event.data || {}
if (source === 'preview' && eventType === 'onMounted' && historyPreviewWindow) {
const { scripts, styles, ancestors = [], ...rest } = params

historyPreviewWindow.postMessage(
{
source: 'designer',
type: 'schema',
data: deepClone({
currentPage: rest,
ancestors,
scripts,
styles
})
},
previewWindow.origin || window.location.origin
)

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

History preview re-uses the wrong origin variable

Inside handleHistoryPreview the message is sent to
previewWindow.origin || window.location.origin.
previewWindow here might be null or point to another live preview, not the just-opened historyPreviewWindow, so the origin is almost certainly wrong.

Replace with the event.origin you already validated or compute from historyPreviewWindow.location after a try/catch guarded same-origin check.

Comment on lines +33 to +48
const getScriptAndStyleDeps = () => {
const { scripts, styles } = useMaterial().getCanvasDeps()
const utilsDeps = useResource().getUtilsDeps()

params.scripts = [...scripts, ...utilsDeps].reduce((res, item) => {
const scriptsDeps = [...scripts, ...utilsDeps].reduce((res, item) => {
res[item.package] = item.script

return res
}, {})
params.styles = [...styles]
const stylesDeps = [...styles]

return {
scripts: scriptsDeps,
styles: stylesDeps
}
}
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

Loss of dependency order & duplicates when building scriptsDeps

reduce((res, item) => { res[item.package] = item.script }) keeps only the last script for each package, silently discarding:

  1. Multiple builds of the same package (e.g. esm + iife artefacts).
  2. Dependencies whose package field is intentionally duplicated (forks, aliases).

If order matters for the runtime (e.g. polyfills before business scripts) or multiple artefacts of one package are required, this will break the preview.

-  const scriptsDeps = [...scripts, ...utilsDeps].reduce((res, item) => {
-    res[item.package] = item.script
-    return res
-  }, {})
+  // Preserve insertion order and allow duplicates
+  const scriptsDeps = [...scripts, ...utilsDeps].map((item) => ({
+    name: item.package,
+    url: item.script
+  }))
📝 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 getScriptAndStyleDeps = () => {
const { scripts, styles } = useMaterial().getCanvasDeps()
const utilsDeps = useResource().getUtilsDeps()
params.scripts = [...scripts, ...utilsDeps].reduce((res, item) => {
const scriptsDeps = [...scripts, ...utilsDeps].reduce((res, item) => {
res[item.package] = item.script
return res
}, {})
params.styles = [...styles]
const stylesDeps = [...styles]
return {
scripts: scriptsDeps,
styles: stylesDeps
}
}
const getScriptAndStyleDeps = () => {
const { scripts, styles } = useMaterial().getCanvasDeps()
const utilsDeps = useResource().getUtilsDeps()
// Preserve insertion order and allow duplicates
const scriptsDeps = [...scripts, ...utilsDeps].map((item) => ({
name: item.package,
url: item.script
}))
const stylesDeps = [...styles]
return {
scripts: scriptsDeps,
styles: stylesDeps
}
}

Comment on lines +233 to +246
const getQueryParams = (params = {}, isHistory = false) => {
const paramsMap = new URLSearchParams(location.search)
const tenant = paramsMap.get('tenant') || ''
const pageId = paramsMap.get('pageid')
const blockId = paramsMap.get('blockid')
const theme = getMetaApi(META_SERVICE.ThemeSwitch)?.getThemeState()?.theme
const framework = getMergeMeta('engine.config')?.dslMode
const platform = getMergeMeta('engine.config')?.platformId
const { scripts, styles } = getScriptAndStyleDeps()

let query = `tenant=${tenant}&id=${paramsMap.get('id')}&theme=${theme}&framework=${framework}`

query += `&platform=${platform}&scripts=${JSON.stringify(scripts)}&styles=${JSON.stringify(styles)}`

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

Unsafe query-string: JSON is not URI-encoded and can exceed URL length

JSON.stringify(scripts) and JSON.stringify(styles) add braces, quotes and spaces that are neither percent-encoded nor guaranteed to stay within common browser URL limits (≈2 kB for IE, 8 kB for others).

-  query += `&platform=${platform}&scripts=${JSON.stringify(scripts)}&styles=${JSON.stringify(styles)}`
+  query += `&platform=${platform}` +
+           `&scripts=${encodeURIComponent(JSON.stringify(scripts))}` +
+           `&styles=${encodeURIComponent(JSON.stringify(styles))}`

Consider switching to POST + sessionStorage for large payloads.

📝 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 getQueryParams = (params = {}, isHistory = false) => {
const paramsMap = new URLSearchParams(location.search)
const tenant = paramsMap.get('tenant') || ''
const pageId = paramsMap.get('pageid')
const blockId = paramsMap.get('blockid')
const theme = getMetaApi(META_SERVICE.ThemeSwitch)?.getThemeState()?.theme
const framework = getMergeMeta('engine.config')?.dslMode
const platform = getMergeMeta('engine.config')?.platformId
const { scripts, styles } = getScriptAndStyleDeps()
let query = `tenant=${tenant}&id=${paramsMap.get('id')}&theme=${theme}&framework=${framework}`
query += `&platform=${platform}&scripts=${JSON.stringify(scripts)}&styles=${JSON.stringify(styles)}`
const getQueryParams = (params = {}, isHistory = false) => {
const paramsMap = new URLSearchParams(location.search)
const tenant = paramsMap.get('tenant') || ''
const pageId = paramsMap.get('pageid')
const blockId = paramsMap.get('blockid')
const theme = getMetaApi(META_SERVICE.ThemeSwitch)?.getThemeState()?.theme
const framework = getMergeMeta('engine.config')?.dslMode
const platform = getMergeMeta('engine.config')?.platformId
const { scripts, styles } = getScriptAndStyleDeps()
let query = `tenant=${tenant}&id=${paramsMap.get('id')}&theme=${theme}&framework=${framework}`
query += `&platform=${platform}` +
`&scripts=${encodeURIComponent(JSON.stringify(scripts))}` +
`&styles=${encodeURIComponent(JSON.stringify(styles))}`

Comment on lines +505 to 531
const getFamily = async (currentPage: { id: string }) => {
if (pageSettingState.pages.length === 0) {
await getPageList()
}

const familyPages = getAncestorsRecursively(previewParams.id)
const familyPages = getAncestorsRecursively(currentPage.id)
.filter((item) => item.isPage)
.reverse()

await handlePageDetail(familyPages, previewParams)
.map((item) => ({
id: item.id,
page_content: item.page_content,
name: item.name,
parentId: item.parentId,
route: item.route,
isPage: item.isPage,
isBody: item.isBody,
isHome: item.isHome,
group: item.group,
isDefault: item.isDefault,
depth: item.depth
}))

await handlePageDetail(familyPages)

updatePageContent(familyPages, currentPage)

return familyPages
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

Type inconsistency & missed early-exit path in getFamily

  1. The parameter is declared as { id: string }, yet the function always reads currentPage.page_content.
    page_content should be marked required (or the call sites updated) to avoid undefined access.

  2. If the incoming currentPage is already populated with children & ancestors, we still go through an expensive tree search + HTTP calls.
    An early exit could bail when currentPage.parentId === ROOT_ID.

Adding both would improve type safety and performance.

Comment on lines +61 to 70
let cleanupCommunicationAction = null
const onSchemaReceivedAction = async (data) => {
updateUrl(data.currentPage, { scripts: data.scripts, styles: data.styles })
const isHistory = new URLSearchParams(location.search).get('history')
const previewHotReload = getMergeMeta('engine.config').previewHotReload
// 如果是历史预览,则不需要实时预览,接收到消息之后直接取消监听(需要监听到第一次消息接受页面信息)
// 如果预览热更新关闭,则不需要实时预览
if (isHistory || previewHotReload === false) {
cleanupCommunicationAction()
}
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

Race-condition guard for cleanupCommunicationAction()

cleanupCommunicationAction is initialised after onSchemaReceivedAction is defined.
If, for some reason, a schema message arrives before cleanupCommunicationAction is assigned, a TypeError will be thrown.

-      if (isHistory || previewHotReload === false) {
-        cleanupCommunicationAction()
+      if ((isHistory || previewHotReload === false) && typeof cleanupCommunicationAction === 'function') {
+        cleanupCommunicationAction()
       }

This simple guard makes the callback resilient to unexpected ordering.

📝 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
let cleanupCommunicationAction = null
const onSchemaReceivedAction = async (data) => {
updateUrl(data.currentPage, { scripts: data.scripts, styles: data.styles })
const isHistory = new URLSearchParams(location.search).get('history')
const previewHotReload = getMergeMeta('engine.config').previewHotReload
// 如果是历史预览,则不需要实时预览,接收到消息之后直接取消监听(需要监听到第一次消息接受页面信息)
// 如果预览热更新关闭,则不需要实时预览
if (isHistory || previewHotReload === false) {
cleanupCommunicationAction()
}
let cleanupCommunicationAction = null
const onSchemaReceivedAction = async (data) => {
updateUrl(data.currentPage, { scripts: data.scripts, styles: data.styles })
const isHistory = new URLSearchParams(location.search).get('history')
const previewHotReload = getMergeMeta('engine.config').previewHotReload
// 如果是历史预览,则不需要实时预览,接收到消息之后直接取消监听(需要监听到第一次消息接受页面信息)
// 如果预览热更新关闭,则不需要实时预览
if ((isHistory || previewHotReload === false) && typeof cleanupCommunicationAction === 'function') {
cleanupCommunicationAction()
}
}

Comment on lines +371 to +376
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

app.js may be undefined – guard before processing

store.getFiles() can return an object without app.js (e.g., on first load with a blank REPL).
Passing undefined to processAppJsCode leads to Cannot read properties of undefined (reading 'includes').

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

Pre-checking avoids a hard runtime failure.

📝 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)
if (newFiles['app.js']) {
newFiles['app.js'] = processAppJsCode(
newFiles['app.js'],
JSON.parse(searchParams.get('styles') || '[]')
)
}

Comment on lines +90 to +98
const getPageRecursively = async (id: string): Promise<IPage[]> => {
const page = await getPageById(id)

if (page.parentId === '0') {
return [page]
}

return [page, ...(await getPageRecursively(page.parentId))]
}
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

Infinite-recursion risk in getPageRecursively

ROOT_ID is the string '0', but API data may return the numeric 0.
Strict comparison (=== '0') will fail, causing endless recursion for root pages.

-  if (page.parentId === '0') {
+  if (String(page.parentId) === ROOT_ID) {
     return [page]
   }

Cast to string (or use ==) before the check.

Committable suggestion skipped: line range outside the PR's diff.

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

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant