Skip to content

Conversation

littla2liyou
Copy link

@littla2liyou littla2liyou commented Sep 30, 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

当前社区内对低代码应用的运行时预览有较强呼声,为了实现运行时渲染,新实现了一个runtime-renderer包来执行整个应用schema的运行时渲染
屏幕截图 2025-09-30 105615

What is the current behavior?

当前仅能在设计器或者预览中查看页面效果,完整的应用效果需要出码
Issue Number: N/A

What is the new behavior?

可以点击预览按钮旁的runtime-deploy这个toolbar按钮进入运行时渲染窗口,直接查看应用运行时渲染效果
image

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features

    • Added an “在线部署运行” toolbar to quickly open a live runtime preview of the current app/page.
    • Introduced a standalone runtime viewer with page routing, default redirects, 404 screen, and loading indicator.
  • Improvements

    • Runtime now supports data sources, global states, i18n, and utility initialization for richer, accurate previews.
    • Enhanced rendering of dynamic components, slots, and collections for schema-driven pages.
    • Reuses an existing runtime window on subsequent runs for a smoother workflow.

Majority is the same as builtin components in package/canvas/renderer. But CanvasRouterLink.vue, CanvasRouterView.vue, and CanvasCollection.vue had changed.
Referenced the implementation of tiny-schema-renderer repo and canvas/render
There are some bugs in lifecycle execution(onMount, onBeforeUpdate, onUpdated, onErrorCaptured, onUnmounted)
Support homepage setting, nested routes, and default navigation to subpages for nested routes
Mainly based on the vue-generator code. Currently in a mixed state of JavaScript and TypeScript, which will be improved later.
Support using destructured npm package imports
In useAppSchema.ts, replace the actual requests in fetchAppSchema(appId) and fetchBlocks() with mock files to use mock files for testing.
Copy link
Contributor

coderabbitai bot commented Sep 30, 2025

Caution

Review failed

Failed to post review comments

Walkthrough

Adds a new runtime rendering capability and an online deploy toolbar. Introduces a runtime renderer package, bootstraps a demo runtime entry, wires dev aliases and TS paths, adds deploy utilities, integrates a RuntimeDeploy toolbar into design-core/layout, and implements router, stores, renderer, data source, HTTP, utilities, and components for runtime rendering.

Changes

Cohort / File(s) Summary
Demo runtime bootstrap
designer-demo/runtime.html, designer-demo/src/runtime.js, designer-demo/package.json
Adds a minimal runtime HTML entry and JS bootstrap that initializes the new runtime renderer; adds dependency on the runtime renderer.
Dev aliases & TS paths
packages/build/vite-config/src/vite-plugins/devAliasPlugin.js, tsconfig.app.json
Adds dev alias mappings and TS path aliases for runtime renderer and runtime-deploy toolbar packages.
Deploy utilities
packages/common/js/runtime-deploy.js
Adds deployPage and runtimeDeploy to compute/open runtime URLs and reuse/focus an existing runtime window.
Design-core integration
packages/design-core/package.json, packages/design-core/re-export.js, packages/design-core/registry.js, packages/layout/src/defaultLayout.js, packages/register/src/constants.ts
Adds dependencies; re-exports RuntimeDeploy; registers toolbar entry guarded by removal flag; inserts toolbar into layout; adds META_APP.RuntimeDeploy constant.
Toolbar: runtime-deploy
packages/toolbars/runtime-deploy/package.json, packages/toolbars/runtime-deploy/index.ts, packages/toolbars/runtime-deploy/meta.js, packages/toolbars/runtime-deploy/src/Main.vue, packages/toolbars/runtime-deploy/vite.config.ts
New toolbar package with meta, entry component invoking runtimeDeploy, and build config.
Runtime renderer: package & build
packages/runtime-renderer/package.json, packages/runtime-renderer/vite.config.ts
New package with dependencies and Vite library build configuration.
Runtime renderer: bootstrap
packages/runtime-renderer/index.ts, packages/runtime-renderer/src/App.vue
Exports initRuntimeRenderer to fetch schema/blocks, create router/pinia/stores, create app and mount; minimal App rendering router-view.
Runtime renderer: router & stores
packages/runtime-renderer/src/router/index.ts, packages/runtime-renderer/src/stores/index.ts
Builds routes from schema (home, defaults, 404) and exposes createAppRouter; generates Pinia stores from globalStates and creates store instances.
Runtime renderer: renderer core
packages/runtime-renderer/src/renderer/render.ts, packages/runtime-renderer/src/renderer/RenderMain.ts, packages/runtime-renderer/src/renderer/LifecycleWrapper.ts, packages/runtime-renderer/src/renderer/useContext.ts
Adds rendering pipeline, page entry, lifecycle wrapper, and context utility; exports renderer component and helpers.
Runtime renderer: built-ins
packages/runtime-renderer/src/renderer/builtin/*, packages/runtime-renderer/src/renderer/builtin/index.ts
Adds Canvas* basic components (Box, Collection, Icon, Img, Placeholder, RouterLink, RouterView, Slot, Text) and aggregates exports.
Runtime renderer: page-function utilities
packages/runtime-renderer/src/renderer/page-function/*, packages/runtime-renderer/src/renderer/page-function/index.ts
Adds CSS handler, accessor/state utilities, and re-exports.
Runtime renderer: parser
packages/runtime-renderer/src/renderer/parser/parser.ts, packages/runtime-renderer/src/renderer/parser/index.ts
Adds data/JSX/function/state-accessor parsing utilities and barrel export.
Runtime renderer: app-function (HTTP, data, utils)
packages/runtime-renderer/src/app-function/http/*, packages/runtime-renderer/src/app-function/dataSource.ts, packages/runtime-renderer/src/app-function/utils.ts
Adds axios-based HTTP with mock support, data source manager, and utilities loader for npm/functions.
Runtime renderer: composables & components
packages/runtime-renderer/src/composables/useAppSchema.ts, packages/runtime-renderer/src/components/Loading.vue, packages/runtime-renderer/src/components/NotFound.vue, packages/runtime-renderer/src/components/PageRenderer.vue
Adds app schema loader/i18n/CSS injection; loading and 404 components; page renderer wrapper.
Runtime renderer: types & utils
packages/runtime-renderer/src/types/*.ts, packages/runtime-renderer/src/utils/data-utils.ts
Introduces type definitions for schema/config/store and general data utilities.
Runtime renderer: mocks
packages/runtime-renderer/src/mock/blocks.json
Adds mock blocks JSON for testing.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant TB as Toolbar(RuntimeDeploy)
  participant DU as runtime-deploy.js
  participant Win as Browser
  participant RT as Runtime HTML/JS
  participant RR as initRuntimeRenderer
  participant AS as useAppSchema
  participant RTU as Router/Stores/App

  U->>TB: Click "在线部署运行"
  TB->>DU: runtimeDeploy()
  DU->>DU: deployPage() -> build openUrl
  alt Existing window open
    DU->>Win: focus(runtimeWindow) (try/catch)
  else No window
    DU->>Win: window.open(openUrl, 'tiny-engine-runtime')
  end

  Win->>RT: Load runtime.html -> /src/runtime.js
  RT->>RR: initRuntimeRenderer()
  RR->>AS: fetchAppSchema(appId), fetchBlocks()
  AS-->>RR: app schema, blocks ready
  RR->>RTU: create router (from pages), init Pinia/stores, mount App
  RTU-->>U: Runtime app renders via router-view
Loading
sequenceDiagram
  autonumber
  participant Router as createAppRouter
  participant Pages as AppSchema.pages
  participant VRouter as VueRouter
  participant PR as PageRenderer
  participant RM as RenderMain
  participant R as renderer

  Router->>Pages: Build routes (home, defaults, children)
  Router->>VRouter: createRouter (hash history '/runtime.html')
  VRouter-->>Router: router instance

  VRouter->>PR: Lazy load for route(pageId)
  PR->>RM: RenderMain(pageId)
  RM->>R: Render schema tree
  R-->>RM: VNode tree
  RM-->>PR: Page content mounted
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Poem

A rabbit taps deploy with glee,
A window blooms—runtime set free.
Routes weave paths, stores take flight,
Schemas dance in renderer light.
Icons blink, a loading spin—
Hop! The app is up again.
Thump-thump: shipped with carrot grin.

Pre-merge checks and finishing touches and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title “Ospp 2025/runtime rendering” includes branch naming metadata and is not a clear, concise description of the feature; while it mentions the core concept of runtime rendering, the “Ospp 2025/” prefix adds noise and the title does not clearly summarize the primary change of adding a runtime-renderer package and toolbar action for live previews. Please rename the pull request to a concise, descriptive title— for example, “Add runtime rendering and deploy toolbar for live application preview”—to clearly convey the main feature without branch metadata.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • 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
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

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

@github-actions github-actions bot added the ospp ospp label Sep 30, 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: 48

♻️ Duplicate comments (2)
packages/runtime-renderer/src/renderer/page-function/css-handler.ts (1)

90-104: See earlier comment on global state race condition.

This function mutates the global enableScoped flag, which is the source of the race condition flagged in lines 19-20. Refer to that comment for the recommended fix.

packages/runtime-renderer/src/types/schema.ts (1)

238-245: Same API response pattern as AppSchema.

This interface has the same design issues as AppSchema (lines 2-22): mixing response metadata with data, and redundant error fields. Consider applying the same refactoring approach to separate ApiResponse<T> from BlockData.

🧹 Nitpick comments (62)
packages/runtime-renderer/src/mock/blocks.json (2)

329-332: Remove the empty dependency object.

The empty components object serves no purpose and clutters the configuration.

Apply this diff:

         "dependencies": {
           "scripts": [
-            {
-              "components": {}
-            },
             {
               "package": "@opentiny/vue",
               "components": {
                 "TinyButton": "Button"
               }
             }
           ],

322-325: Replace magic number with a descriptive log message.

The console.log(123456) uses a magic number that provides no context. For test/debug mock data, a descriptive message would be more helpful.

Consider:

           "onClickNew1": {
             "type": "JSFunction",
-            "value": "function onClickNew1(event) {\n  this.state.stateblock1.button++\n  console.log(123456)\n}\n"
+            "value": "function onClickNew1(event) {\n  this.state.stateblock1.button++\n  console.log('Button counter incremented:', this.state.stateblock1.button)\n}\n"
           }
packages/runtime-renderer/src/App.vue (1)

2-2: Consider whether forced remounting is necessary.

Using :key="$route.fullPath" forces a full component remount on every route change, which destroys component state and prevents Vue Router's built-in transition/keep-alive optimizations. This pattern is typically only needed when:

  • Child components don't properly react to route param changes
  • You need to reset component state between routes with identical components

For most runtime rendering scenarios, the default router-view behavior (remounting only when the component changes) is more performant and preserves user interactions better.

If forced remounting is required for your schema-driven rendering, consider documenting why in a comment. Otherwise, try removing the key binding:

-  <router-view :key="$route.fullPath" />
+  <router-view />

Verify that route navigation and component updates still work correctly after this change.

packages/runtime-renderer/src/renderer/builtin/CanvasText.vue (1)

1-18: LGTM! Consider optional modernization.

The component implementation is correct with proper prop typing and defaults. The class binding correctly combines static and dynamic classes.

For consistency with other new components in this PR (like PageRenderer.vue which uses script setup), you could optionally modernize to Composition API:

<template>
  <span :class="['canvas-text', className]">{{ text }}</span>
</template>

<script setup lang="ts">
defineProps<{
  text?: string
  className?: string
}>()

withDefaults(defineProps<{ text?: string; className?: string }>(), {
  text: '',
  className: ''
})
</script>

However, the current Options API implementation is perfectly valid and functional.

packages/runtime-renderer/src/types/config.ts (1)

25-30: Consider using unknown instead of any for better type safety.

The use of Record<string, any> eliminates compile-time type checking. While this is pragmatic for dynamic runtime stores from schemas, consider using Record<string, unknown> to maintain some type safety and require explicit type assertions at usage sites.

Apply this diff if you want stricter typing:

 export interface StoreConfig {
   id: string
-  state: Record<string, any>
-  actions: Record<string, any>
-  getters: Record<string, any>
+  state: Record<string, unknown>
+  actions: Record<string, unknown>
+  getters: Record<string, unknown>
 }
packages/runtime-renderer/src/renderer/builtin/CanvasRouterLink.vue (2)

1-13: Replace javascript:void(0) with # for cleaner markup.

The href="javascript:void(0)" is a dated practice. Since you're calling preventDefault() in the click handler, using href="#" is cleaner and more conventional.

Apply this diff:

   <a
-    href="javascript:void(0)"
+    href="#"
     @click="handleClick"

44-49: Consider adding error handling for failed navigation.

The handleClick function correctly prevents default and uses router.push, but navigation can fail (e.g., navigation guards rejecting). Consider adding error handling for better resilience.

     const handleClick = (event: Event) => {
       event.preventDefault()
-      if (props?.to) {
-        router.push(props.to)
+      if (props.to) {
+        router.push(props.to).catch((err) => {
+          console.warn('Navigation failed:', err)
+        })
       }
     }
packages/runtime-renderer/src/stores/index.ts (1)

28-45: Add validation and error handling for store creation.

The createStores function should validate configurations and handle potential failures:

  1. Line 40: Chinese comment should be translated.
  2. Missing validation for required config fields (id, state, etc.).
  3. No error handling if defineStore or store instantiation fails.
  4. Consider logging when stores are created for debugging.

Apply this diff:

 export const createStores = (storesConfig: StoreConfig[], pinia: Pinia) => {
   const stores = shallowReactive<Record<string, any>>({})

   storesConfig.forEach((config) => {
+    // Validate config
+    if (!config.id) {
+      console.error('Store config missing required id field:', config)
+      return
+    }
+
+    try {
-    // 使用 defineStore 创建 Pinia store
+      // Create Pinia store using defineStore
       const useStore = defineStore(config.id, {
         state: () => config.state,
-
         getters: config.getters,
-
         actions: config.actions
       })
-    // 使用useStore创建 store 实例并绑定到 pinia
+      // Create store instance and bind to Pinia
       stores[config.id] = useStore(pinia)
+    } catch (error) {
+      console.error(`Failed to create store: ${config.id}`, error)
+    }
   })

   return stores
 }
packages/runtime-renderer/src/utils/data-utils.ts (4)

1-2: Consider direct named export for clarity.

The destructuring-with-rename pattern export const { parseFunction: generateFunction } = commonUtils is less discoverable than a direct re-export. If generateFunction is intended as a domain-specific alias, consider documenting why the rename is necessary or using a more explicit pattern.

Apply this diff for a clearer approach:

-import { utils as commonUtils } from '@opentiny/tiny-engine-utils'
-export const { parseFunction: generateFunction } = commonUtils
+import { utils as commonUtils } from '@opentiny/tiny-engine-utils'
+export const generateFunction = commonUtils.parseFunction

4-6: Add type annotations for better type safety.

The reset function lacks type annotations, reducing type safety and IDE support.

Apply this diff:

-export const reset = (obj) => {
+export const reset = (obj: Record<string, any>) => {
   Object.keys(obj).forEach((key) => delete obj[key])
 }

8-12: Add type annotations to newFn wrapper.

While the wrapper prevents ESLint errors, it lacks type safety for its arguments.

Apply this diff:

-export const newFn = (...argv) => {
+export const newFn = (...argv: string[]) => {
   const Fn = Function
   return new Fn(...argv)
 }

25-30: Add type annotations for parameter objects.

The function lacks type information for its parameters, reducing type safety.

Apply this diff:

-export const getDeletedKeys = (objA, objB) => {
+export const getDeletedKeys = (objA: Record<string, any>, objB: Record<string, any>): string[] => {
   const keyA = Object.keys(objA)
   const keyB = new Set(Object.keys(objB))
 
   return keyA.filter((item) => !keyB.has(item))
 }
packages/runtime-renderer/src/app-function/http/axios.js (2)

118-136: Mock state management lacks cleanup guarantees.

The disableMock() method at line 131-136 calls mock.restore() but doesn't handle potential errors during restoration. Additionally, there's no way to query or clear accumulated proxy configurations.

Apply this diff:

 disableMock() {
   if (mock) {
-    mock.restore()
+    try {
+      mock.restore()
+    } catch (error) {
+      console.error('[axios-wrapper] Failed to restore mock adapter:', error)
+    }
   }
   mock = undefined
 },

Consider adding a method to clear/reset mock configurations:

resetMock() {
  if (mock) {
    mock.reset()
  }
},

16-143: Convert to TypeScript for type safety.

This file is JavaScript (.js) but the project uses TypeScript. Converting to TypeScript would provide:

  • Type safety for the config parameter and return type
  • Better IDE support
  • Documentation through types
  • Compile-time error catching

Consider renaming to axios.ts and adding type definitions:

import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
import MockAdapter from 'axios-mock-adapter'

interface HttpClient {
  request: AxiosInstance['request']
  get: AxiosInstance['get']
  // ... other methods
  mock(config: MockConfig[]): MockAdapter
  disableMock(): void
  isMock(): boolean
}

export default (config: AxiosRequestConfig): HttpClient => {
  // implementation
}
packages/runtime-renderer/src/renderer/parser/parser.ts (2)

233-236: Use strict equality for condition checks.

Line 234 uses loose equality (== null) which matches both null and undefined. While this might be intentional, it's better to be explicit for maintainability.

Apply this diff for clarity:

 export const parseCondition = (condition, scope, ctx) => {
-  // eslint-disable-next-line no-eq-null
-  return condition == null ? true : parseData(condition, scope, ctx)
+  return condition === null || condition === undefined ? true : parseData(condition, scope, ctx)
 }

282-299: v-model binding generation has fragile event name detection.

The regex /^on[A-Z]/ at line 286 checks for event handlers, but it doesn't validate that the event name actually corresponds to the model value property. This could miss edge cases or incorrectly identify handlers.

Improve the detection logic:

   // 处理 v-model 双向绑定
   const propsEntries = Object.entries(data)
   const modelValue = propsEntries.find(([_key, value]) => value?.type === 'JSExpression' && value?.model === true)
-  const hasUpdateModelValue = propsEntries.find(
-    ([key]) => /^on[A-Z]/.test(key) && key.startsWith(`onUpdate:${modelValue?.[0]}`)
-  )
+  const hasUpdateModelValue = modelValue && propsEntries.find(
+    ([key]) => key === `onUpdate:${modelValue[0]}`
+  )
 
   if (modelValue && !hasUpdateModelValue) {
     // 添加 onUpdate:modelKey 事件
-    res[`onUpdate:${modelValue?.[0]}`] = parseData(
+    res[`onUpdate:${modelValue[0]}`] = parseData(
       {
         type: 'JSFunction',
         value: `(value) => ${modelValue[1].value}=value`
       },
       scope,
       ctx
     )
   }
packages/runtime-renderer/src/renderer/builtin/CanvasPlaceholder.vue (1)

14-16: Consider internationalization for the placeholder text.

The hardcoded Chinese default text '请将元素拖放到这里' may limit usability for non-Chinese users. Consider using the i18n mechanism already present in the codebase (based on learnings mentioning tiny-engine-i18n-host).

For example, you could use a translated key:

   placeholder: {
     type: String,
-    default: '请将元素拖放到这里'
+    default: () => i18n.global.t('canvas.placeholder.dragHere')
   }

Or make the component i18n-aware by importing and using the translation service.

packages/runtime-renderer/src/app-function/http/config.js (1)

13-15: Consider making HTTP configuration runtime-customizable.

The hardcoded withCredentials: false is a safe default, but applications may need to customize this (e.g., for CORS requests with cookies). Consider exposing this as a configurable option that can be overridden at runtime initialization.

Example refactor:

-export default {
-  withCredentials: false
+export const createHttpConfig = (overrides = {}) => ({
+  withCredentials: false,
+  ...overrides
+})
+
+export default createHttpConfig()
packages/runtime-renderer/src/renderer/builtin/CanvasCollection.vue (3)

2-2: Add inheritAttrs: false for explicit attribute control.

When using v-bind="$attrs" on a non-root or specific element, it's best practice to set inheritAttrs: false in the component options to prevent Vue from automatically applying attributes to the root element. This makes the attribute forwarding behavior explicit and predictable.

Apply this diff:

 export default {
+  inheritAttrs: false,
   components: {
     CanvasPlaceholder
   },

9-27: Consider adding TypeScript type annotations.

The script uses lang="ts" but lacks type annotations for props and component options. Consider adding proper TypeScript interfaces or using defineComponent from Vue for better type safety.

Example refactor:

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import CanvasPlaceholder from './CanvasPlaceholder.vue'

export default defineComponent({
  name: 'CanvasCollection',
  components: {
    CanvasPlaceholder
  },
  props: {
    tag: {
      type: String as PropType<string>,
      default: 'div'
    },
    schema: {
      type: Object as PropType<Record<string, any>>,
      default: () => ({})
    },
    dataSource: {
      type: [String, Array, Number] as PropType<string | any[] | number>
    }
  }
})
</script>

20-24: Remove or document unused props in CanvasCollection.vue
Both schema (line 20) and dataSource (line 24) are declared but never referenced in the component’s template or script, nor bound by any parent. Drop them or add documentation explaining their intended use.

packages/runtime-renderer/src/renderer/builtin/CanvasImg.vue (1)

12-15: Consider using Object type for style prop.

The style prop is defined as String, which limits flexibility and type safety. Vue's style binding typically accepts Object or Array types for better validation and developer experience.

Apply this diff to improve type safety:

     style: {
-      type: String,
-      default: ''
+      type: [Object, String],
+      default: () => ({})
     }
packages/runtime-renderer/src/app-function/http/index.js (1)

22-22: Incomplete re-login flow.

The TODO comment indicates a missing authentication re-login implementation.

Do you want me to generate a re-login handler implementation or open a new issue to track this task?

packages/runtime-renderer/src/renderer/builtin/CanvasSlot.vue (2)

13-20: Props name and params are declared but never used.

The component defines name and params props but does not reference them in the template or pass them explicitly to child components. If these props are intended for future use or external APIs, consider documenting their purpose.


3-3: Consider disabling attribute inheritance to prevent prop duplication.

The component uses v-bind="$attrs" to forward attributes to CanvasPlaceholder. However, by default Vue inherits all attributes (including name and params props) to the root element (the <slot>), and then forwards them again via $attrs. This may cause duplicate attributes on CanvasPlaceholder.

Apply this diff to explicitly control attribute inheritance:

 <script>
 import CanvasPlaceholder from './CanvasPlaceholder.vue'
 export default {
+  inheritAttrs: false,
   components: {
     CanvasPlaceholder
   },

Also applies to: 7-21

packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue (1)

7-16: Consider migrating to <script setup> for consistency.

The component uses Options API. For a new codebase feature, consider using <script setup> with Composition API for better TypeScript support, reduced boilerplate, and alignment with modern Vue 3 patterns.

Example refactor:

<script setup lang="ts">
defineOptions({
  inheritAttrs: false
})

interface Props {
  tag?: string
}

withDefaults(defineProps<Props>(), {
  tag: 'div'
})
</script>
packages/runtime-renderer/package.json (3)

23-23: Consider upgrading axios-mock-adapter to latest stable version.

The package specifies axios-mock-adapter: ^1.19.0, but version 2.1.0 is available and includes maintenance fixes and improvements. Review the changelog for any breaking changes before upgrading.

Based on learnings.

-    "axios-mock-adapter": "^1.19.0",
+    "axios-mock-adapter": "^2.1.0",

29-31: Vite and plugin versions are older than other packages in the monorepo.

This package uses vite: ^4.0.0 and @vitejs/plugin-vue: ^4.0.0, while the runtime-deploy toolbar package uses Vite 5.x and plugin-vue 5.x. Consider aligning versions across the monorepo for consistency.


19-19: Upgrade @vue/babel-plugin-jsx to v1.5.0.
In packages/runtime-renderer/package.json (line 19), change "@vue/babel-plugin-jsx": "^1.2.5""^1.5.0". Release notes list only feature and bugfix updates since v1.2.5 with no breaking changes in v1.3.x or v1.4.x, so this bump is safe and ensures you get the latest fixes and improvements.

packages/runtime-renderer/index.ts (2)

26-27: Consider parallelizing independent async operations.

fetchAppSchema and fetchBlocks appear to be independent operations that could run in parallel to reduce initialization time.

-  await fetchAppSchema(appId)
-  await fetchBlocks()
+  await Promise.all([
+    fetchAppSchema(appId),
+    fetchBlocks()
+  ])

22-39: Consider adding user-facing error handling.

While useAppSchema may handle fetch errors internally, initRuntimeRenderer has no try-catch or fallback UI. If initialization fails, users see a blank screen. Consider wrapping the initialization in error handling and displaying an error message.

Example:

export const initRuntimeRenderer = async () => {
  try {
    // ... existing initialization code
  } catch (error) {
    console.error('Failed to initialize runtime renderer:', error)
    document.getElementById('app')!.innerHTML = `
      <div style="padding: 2rem; color: red;">
        <h2>Failed to load application</h2>
        <p>${error instanceof Error ? error.message : 'Unknown error'}</p>
      </div>
    `
    throw error
  }
}
packages/runtime-renderer/src/renderer/page-function/state.ts (3)

15-17: Consider performance implications of clearing all keys.

When clear is true, this iterates and deletes all keys. For large state objects, this could be slow. Consider whether Object.keys(state).forEach((key) => delete state[key]) is necessary when you could reassign the reactive reference or use a more efficient clearing mechanism.

However, given Vue 3's reactive system, direct deletion is correct to maintain reactivity. The current approach is acceptable for typical use cases.


19-23: getDeletedKeys performs only shallow key comparison
The implementation in packages/runtime-renderer/src/utils/data-utils.ts (and its mirror in packages/canvas/render/src/data-utils.ts) filters only top-level property names via Object.keys, so it doesn’t handle nested objects, distinguish undefined vs null, or compare array contents. If you need those cases covered, implement deep-diff logic or explicitly document that deletions are shallow.


28-39: Use explicit null/undefined checks for accessor.value (page-function/state.ts:29–34)
isStateAccessor correctly filters accessor objects, but if (accessor?.getter?.value) (and similarly for setter) treats empty strings ('') as falsy and will skip them. Replace these truthiness guards with explicit checks—e.g. if (accessor.getter.value != null)—to ensure empty-string values aren’t inadvertently ignored.

packages/toolbars/runtime-deploy/src/Main.vue (2)

4-4: Hardcoded Chinese text should be internationalized.

The content text "在线部署运行" and message "请先创建页面" (Line 35) are hardcoded. For better i18n support, use the i18n system that appears to be available in the project (tiny-engine-i18n-host).

Consider extracting strings to i18n keys:

-      content="在线部署运行"
+      :content="t('toolbar.runtimeDeploy')"
-          message: '请先创建页面'
+          message: t('toolbar.createPageFirst')

30-42: Wrap runtimeDeploy() in a try/catch and await its promise

To surface deployment errors and prevent unhandled rejections, update the call to:

     const deploy = async () => {
       // 检查页面状态,确保有内容
       if (useLayout().isEmptyPage()) {
         useNotify({
           type: 'warning',
           message: '请先创建页面'
         })

         return
       }

-      runtimeDeploy()
+      try {
+        await runtimeDeploy()
+      } catch (error) {
+        useNotify({
+          type: 'error',
+          message: '部署失败,请重试'
+        })
+        console.error('Runtime deploy failed:', error)
+      }
     }
packages/toolbars/runtime-deploy/vite.config.ts (1)

20-36: Vite config looks correct for a library build.

The configuration appropriately:

  • Enables sourcemaps for debugging
  • Externalizes Vue and @opentiny dependencies to avoid bundling peer dependencies
  • Uses ES format for modern module consumption
  • Disables publicDir for library builds

One minor observation: Line 23 has an empty resolve: {} object. If there are no resolve options needed, consider removing it for cleanliness.

   publicDir: false,
-  resolve: {},
   build: {
packages/runtime-renderer/src/components/Loading.vue (1)

46-59: Animation keyframes are correct but could use will-change hint.

The rotation animation is smooth, but for better performance, especially on mobile devices, add will-change: transform to the .loading class to hint to the browser that this element will be animated.

 .loading {
   width: 60px;
   height: 60px;
   margin: 0 auto;
   position: relative;
   animation: load 3s linear infinite;
+  will-change: transform;
 }
packages/runtime-renderer/src/renderer/page-function/blockContext.ts (1)

47-47: JSON.parse/stringify is inefficient for deep cloning.

Using JSON.parse(JSON.stringify(data)) has limitations:

  • Doesn't handle functions, undefined, Symbols, or circular references
  • Performance overhead for large objects

Consider using a dedicated cloning library like structuredClone (native in modern browsers/Node) or lodash's cloneDeep.

-    const newSchema = JSON.parse(JSON.stringify(data))
+    const newSchema = structuredClone(data)

Note: structuredClone is available in Node 17+ and modern browsers. If supporting older environments, keep JSON approach or add a polyfill.

packages/runtime-renderer/src/renderer/useContext.ts (2)

15-17: Consider adding generic type parameter for better type safety.

The Context interface currently allows any values. While this provides flexibility, consider making the factory generic to allow consumers to specify their context shape:

-interface Context {
+interface Context<T = any> {
-  [key: string]: any
+  [key: string]: T
 }

Then update the factory signature:

export default <T = any>(): UseContextReturn<T> => { ... }

This would enable type-safe context usage while maintaining backward compatibility via the default any type.


30-35: LGTM with optional refactor for clear logic.

The setContext implementation correctly merges new context values. The clear parameter properly removes existing keys before assignment.

For slightly cleaner code, consider this refactor:

   const setContext = (ctx: Context, clear?: boolean) => {
     if (clear) {
-      Object.keys(context).forEach((key) => delete context[key])
+      Object.keys(context).forEach((key) => {
+        delete context[key]
+      })
     }
     Object.assign(context, ctx)
   }

Or use a more functional approach:

const setContext = (ctx: Context, clear?: boolean) => {
  if (clear) {
    for (const key in context) {
      delete context[key]
    }
  }
  Object.assign(context, ctx)
}
packages/runtime-renderer/src/components/NotFound.vue (2)

11-37: Add internationalization and improve accessibility.

The template has hardcoded Chinese text and lacks proper accessibility attributes:

  1. I18n: Text like "页面未找到", "抱歉,您访问的页面不存在或已被删除", etc., should be externalized for internationalization, especially given the PR context mentions tiny-engine-i18n-host as a workspace dependency.
  2. Accessibility: The SVG icon lacks a title or aria-label, and interactive elements could benefit from better ARIA attributes.

For i18n, consider using the i18n system:

<h1>{{ t('notFound.title') }}</h1>
<p>{{ t('notFound.description') }}</p>

For accessibility:

       <svg viewBox="0 0 200 200" width="120" height="120">
+        <title>404 错误图标</title>
         <circle cx="100" cy="100" r="80" fill="#f0f0f0" stroke="#ddd" stroke-width="2" />
         <text x="100" y="110" text-anchor="middle" font-size="48" fill="#999">404</text>
       </svg>
-        <button @click="refresh" class="action-button secondary">刷新页面</button>
+        <button @click="refresh" class="action-button secondary" aria-label="刷新当前页面">
+          刷新页面
+        </button>

49-51: Consider router-based navigation instead of full page reload.

window.location.reload() forces a full page refresh, losing all application state and causing a jarring user experience. In a Vue SPA, consider navigating to the home route or previous page instead:

+import { useRouter } from 'vue-router'
+
 const route = useRoute()
+const router = useRouter()

 const refresh = () => {
-  window.location.reload()
+  // Navigate to home page or previous route
+  router.push('/')
+  // Or go back if there's history:
+  // router.back()
 }

If a full reload is genuinely required (e.g., to reset service workers or clear cache), consider confirming with the user first.

packages/runtime-renderer/src/composables/useAppSchema.ts (2)

11-18: Consider propagating initialization errors.

The error is caught and logged but not propagated to the caller. This means initializeAppConfig and its caller won't know if utility initialization failed, potentially leading to silent failures at runtime when utilities are accessed.

Consider one of these approaches:

  1. Re-throw the error after logging:
 const initializeUtils = async (utils: Util[]) => {
   try {
     await initUtils(utils)
   } catch (error) {
     // eslint-disable-next-line no-console
     console.error('工具函数初始化失败:', error)
+    throw error
   }
 }
  1. Or return a success/failure indicator:
-const initializeUtils = async (utils: Util[]) => {
+const initializeUtils = async (utils: Util[]): Promise<boolean> => {
   try {
     await initUtils(utils)
+    return true
   } catch (error) {
     // eslint-disable-next-line no-console
     console.error('工具函数初始化失败:', error)
+    return false
   }
 }

29-34: Remove unsafe type assertion.

The as any type assertion on line 32 bypasses TypeScript's type checking. According to the schema types, I18nConfig values are Record<string, any>, which should be compatible with mergeLocaleMessage.

 const initializeI18n = (i18nConfig: I18nConfig) => {
   if (!i18nConfig) return
   Object.entries(i18nConfig).forEach(([loc, msgs]) => {
-    i18n.global.mergeLocaleMessage(loc, msgs as any)
+    i18n.global.mergeLocaleMessage(loc, msgs)
   })
 }

If there's a legitimate type mismatch, consider adding proper type guards or updating the interface definition.

packages/runtime-renderer/src/renderer/page-function/accessor.ts (1)

20-21: Unused propsAccessorMap.

The propsAccessorMap is declared and exposed in the return object but never populated or used within this module. It appears to be reserved for future use with prop accessors (similar to how generateStateAccessors populates stateAccessorMap).

Consider either:

  1. Implementing a generatePropsAccessors function if prop accessors are needed
  2. Removing propsAccessorMap if it's not currently used elsewhere in the codebase
  3. Adding a TODO comment explaining its future purpose

Run this script to check if propsAccessorMap is used elsewhere:

#!/bin/bash
# Search for propsAccessorMap usage in the codebase
rg -n "propsAccessorMap" --type ts --type tsx -C 2
packages/runtime-renderer/src/router/index.ts (1)

82-89: Remove or guard debug code for production.

Lines 82-89 assign the router to window.__DEBUG_ROUTER__ and log all routes to the console. This debug code should either be removed or guarded behind a development-only flag.

 export async function createAppRouter() {
   const routes = await createRouterConfig()
   const router = createRouter({ history: createWebHashHistory('/runtime.html'), routes })

-  if (typeof window !== 'undefined') {
+  if (typeof window !== 'undefined' && import.meta.env.DEV) {
     window.__DEBUG_ROUTER__ = router
     // eslint-disable-next-line no-console
     console.log(
       '所有路由:',
       router.getRoutes().map((r) => ({ path: r.path, name: r.name, redirect: r.redirect }))
     )
   }
   return router
 }

Also add a TypeScript declaration for the debug property:

declare global {
  interface Window {
    __DEBUG_ROUTER__?: Router
  }
}
packages/runtime-renderer/src/renderer/render.ts (1)

218-244: Minor duplication: Mapper and block schema binding.

Lines 229-231 and 234-237 both add bindProps.schema = schema for different component types. This logic could be consolidated.

 const getBindProps = (schema, scope, context) => {
   const { componentName } = schema

   if (componentName === 'CanvasPlaceholder') {
     return {}
   }

   const bindProps = {
     ...parseData(schema.props, scope, context)
   }

-  if (Mapper[componentName]) {
-    bindProps.schema = schema
-  }
-
-  // 如果是区块组件,传递完整的 schema
-  const blockSchema = getBlock(componentName)
-  if (blockSchema) {
+  // Mapper components and block components both need the full schema
+  if (Mapper[componentName] || getBlock(componentName)) {
     bindProps.schema = schema
   }

   // 绑定组件属性时需要将 className 重命名为 class,防止覆盖组件内置 class
   bindProps.class = bindProps.className
   delete bindProps.className

   return bindProps
 }
packages/runtime-renderer/src/renderer/page-function/css-handler.ts (1)

13-17: Remove unused interface property.

The enableModernCSS option in CSSHandlerOptions is declared but never referenced in the implementation.

Apply this diff to remove the unused property:

 interface CSSHandlerOptions {
   pageId?: string
   enableScoped?: boolean
-  enableModernCSS?: boolean
 }
packages/runtime-renderer/src/types/schema.ts (5)

2-22: Consider refining the AppSchema type.

The interface mixes API response metadata (code, message, error, errMsg, success) with actual schema data. Consider:

  1. Redundant error fields: Both error and errMsg are typed as any and serve similar purposes.
  2. Type specificity: any types reduce type safety.

Consider separating concerns:

+// API response wrapper
+export interface ApiResponse<T> {
+  data: T
+  code: string
+  message: string
+  error?: unknown
+  success: boolean
+}
+
+// Application data schema
+export interface AppData {
+  bridge: any[]
+  componentsMap: ComponentMap[]
+  componentsTree: PageSchema[]
+  css: string
+  dataSource: DataSourceConfig
+  utils: Util[]
+  packages: PackageConfig[]
+  meta: AppMeta
+  config: AppConfig
+  constants: string
+  i18n: I18nConfig
+  version: string
+}
+
-export interface AppSchema {
-  data: {
-    bridge: any[]
-    componentsMap: ComponentMap[]
-    componentsTree: PageSchema[]
-    css: string
-    dataSource: DataSourceConfig
-    utils: Util[]
-    packages: PackageConfig[]
-    meta: AppMeta
-    config: AppConfig
-    constants: string
-    i18n: I18nConfig
-    version: string
-  }
-  code: string
-  message: string
-  error: any
-  errMsg: any
-  success: boolean
-}
+export type AppSchema = ApiResponse<AppData>

25-40: Consider typing the generic fields.

Several fields use any which reduces type safety:

  • npmrc?: any (line 31)
  • styles: any[] (line 38)

If these have known shapes, consider adding specific types:

export interface ComponentMap {
  package?: string | null
  destructuring: boolean
  exportName?: string | null
  componentName: string
  version: string
  npmrc?: Record<string, unknown> | string  // or the actual shape
  path?: string
  dependencies?: {
    scripts: Array<{
      package?: string
      components: Record<string, string>
    }>
    styles: string[]  // or Array<{ url: string; type?: string }>
  }
}

43-64: Eliminate duplication between PageSchema and PageContent.

PageContent duplicates all fields from PageSchema except meta. Use TypeScript utility types to maintain DRY principle.

Apply this diff:

-// 页面Schema
-export interface PageSchema {
+export interface PageContent {
   children: ComponentNode[]
   css: string
   componentName: string
   fileName: string
   lifeCycles?: LifeCycles | null
-  meta: PageMeta
   methods: Record<string, JSFunction>
   props: Record<string, any>
   state: Record<string, any>
 }

-export interface PageContent {
-  children: ComponentNode[]
-  css: string
-  componentName: string
-  fileName: string
-  lifeCycles?: LifeCycles | null
-  methods: Record<string, JSFunction>
-  props: Record<string, any>
-  state: Record<string, any>
-}
+// 页面Schema
+export interface PageSchema extends PageContent {
+  meta: PageMeta
+}

77-98: Inconsistent naming convention in PageMeta.

The interface mixes camelCase (isHome, lastUpdatedBy, rootElement) with snake_case (gmt_create, gmt_modified, page_content). While this likely mirrors a backend API, consider normalizing to camelCase for JavaScript consistency or documenting that this is a direct API mapping.

If you prefer consistency, consider a transformer:

// API response type (as-is from backend)
interface PageMetaAPI {
  app: number
  gmt_create: string
  // ... snake_case fields
}

// Normalized application type
export interface PageMeta {
  app: number
  gmtCreate: string
  lastUpdatedBy: string
  // ... camelCase fields
}

Or add a JSDoc comment indicating this mirrors the API shape:

/**
 * Page metadata - mirrors backend API response structure.
 * Field names use snake_case to match API contract.
 */
export interface PageMeta {
  // ...
}

135-139: Document the purpose of the model field.

The model property in JSExpression lacks documentation. Is this for v-model binding detection or two-way data binding?

Add a JSDoc comment:

// JS表达式
export interface JSExpression {
  type: 'JSExpression'
  value: string
  /** Indicates if this expression is used in a v-model or two-way binding context */
  model?: boolean
}
packages/runtime-renderer/src/renderer/LifecycleWrapper.ts (1)

50-58: Type the props more specifically.

Both schema and parent use Object or Object as PropType<any>, reducing type safety. Consider importing the actual schema types.

+import type { PageContent, PageSchema } from '../types/schema'
+
 export const PageLifecycleWrapper = defineComponent({
   name: 'PageLifecycleWrapper',
   props: {
     schema: {
-      type: Object as PropType<any>,
+      type: Object as PropType<Record<string, any>>,
       required: true
     },
     parent: {
-      type: Object,
-      default: () => ({})
+      type: Object as PropType<Partial<PageSchema>>,
+      default: () => ({} as Partial<PageSchema>)
     }
   },
packages/runtime-renderer/src/renderer/RenderMain.ts (2)

41-44: Add null-safety for missing pages.

Line 43 uses JSON.parse(JSON.stringify(page)) which will return undefined if page is undefined, but this could throw or return unexpected values. Add explicit handling.

 const currentSchema = computed(() => {
-  const page = getPageById(props.pageId)?.meta?.page_content // 通过 pageId 获取最新的页面对象
-  return JSON.parse(JSON.stringify(page))
+  const page = getPageById(props.pageId)?.meta?.page_content
+  if (!page) return null
+  return JSON.parse(JSON.stringify(page))
 })

This makes the null-case explicit and avoids JSON.parse(JSON.stringify(undefined)).


52-52: Add type safety for injected stores.

Line 52 injects 'stores' without a default or type, which could result in undefined. Consider adding a default or type assertion.

-const stores = inject('stores')
+const stores = inject<Record<string, any>>('stores', {})

Or if stores must be provided by parent:

const stores = inject<Record<string, any>>('stores')
if (!stores) {
  console.error('stores must be provided by parent component')
}
packages/runtime-renderer/src/app-function/dataSource.ts (5)

11-24: Consider adding validation for item structure.

The normalizeItem function does not validate that item.data exists or that expected properties are present. If the schema is malformed, this could silently produce objects with undefined properties, leading to runtime errors downstream.

Apply this diff to add a basic guard:

 const normalizeItem = (item: any) => {
+  if (!item || !item.data) {
+    console.warn('normalizeItem received invalid item:', item)
+    return null
+  }
   return {
     id: item.id,
     name: item.name,

Then filter out null results when mapping:

-  list: (rawConfig.list || []).map(normalizeItem)
+  list: (rawConfig.list || []).map(normalizeItem).filter(Boolean)

31-31: Consider typing dataSourceMap more strictly.

The Record<string, any> type provides no type safety for consumers. If the dataSource shape is well-defined, consider defining an interface or type to improve maintainability and IDE support.

Example:

interface DataSource {
  config: any // or a more specific config type
  status: 'init' | 'loading' | 'loaded' | 'error'
  data: any
  error?: any
  load: (params?: any, customUrl?: string) => Promise<any>
}

export const dataSourceMap: Record<string, DataSource> = {}

53-55: Clarify the intent of returning undefined when shouldFetch is false.

When shouldFetch() returns false, the function resolves with undefined. This behavior should be documented, as callers must explicitly handle this case. Alternatively, consider returning a resolved promise with a sentinel value or a specific status to make the intent clearer.

Example:

   if (!shouldFetch()) {
-    return Promise.resolve(undefined)
+    // Do not fetch; return a no-op response
+    return Promise.resolve({ code: 'NO_FETCH', msg: 'Fetch skipped by shouldFetch', data: null })
   }

36-71: Inconsistent error handling between local and remote data paths.

The local data path (lines 38-51) catches exceptions, updates dataSource.status and dataSource.error, and rejects the promise. However, the remote data path (lines 57-70) delegates error handling to the HTTP interceptors (configured later at lines 103-104). This asymmetry means that errors in the local path are handled inline, while remote errors are handled elsewhere.

For consistency and easier debugging, consider explicitly handling remote errors in the load function or clearly documenting that error handling is centralized in the interceptors.

Apply this diff to add explicit error handling for the remote path:

-  return http.request(config)
+  return http.request(config).catch((error) => {
+    dataSource.status = 'error'
+    dataSource.error = error
+    throw error
+  })

Alternatively, document that error handling is delegated to interceptors.


121-121: Both default and named exports are provided.

Exporting dataSourceMap as both a named export (line 31) and the default export (line 121) is acceptable, but may cause confusion for consumers. Consider standardizing on one pattern across the codebase for consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 108919d and e7212cd.

📒 Files selected for processing (56)
  • designer-demo/package.json (1 hunks)
  • designer-demo/runtime.html (1 hunks)
  • designer-demo/src/runtime.js (1 hunks)
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (1 hunks)
  • packages/common/js/runtime-deploy.js (1 hunks)
  • packages/design-core/package.json (1 hunks)
  • packages/design-core/re-export.js (1 hunks)
  • packages/design-core/registry.js (2 hunks)
  • packages/layout/src/defaultLayout.js (1 hunks)
  • packages/register/src/constants.ts (1 hunks)
  • packages/runtime-renderer/index.ts (1 hunks)
  • packages/runtime-renderer/package.json (1 hunks)
  • packages/runtime-renderer/src/App.vue (1 hunks)
  • packages/runtime-renderer/src/app-function/dataSource.ts (1 hunks)
  • packages/runtime-renderer/src/app-function/http/axios.js (1 hunks)
  • packages/runtime-renderer/src/app-function/http/config.js (1 hunks)
  • packages/runtime-renderer/src/app-function/http/index.js (1 hunks)
  • packages/runtime-renderer/src/app-function/utils.ts (1 hunks)
  • packages/runtime-renderer/src/components/Loading.vue (1 hunks)
  • packages/runtime-renderer/src/components/NotFound.vue (1 hunks)
  • packages/runtime-renderer/src/components/PageRenderer.vue (1 hunks)
  • packages/runtime-renderer/src/composables/useAppSchema.ts (1 hunks)
  • packages/runtime-renderer/src/mock/blocks.json (1 hunks)
  • packages/runtime-renderer/src/renderer/LifecycleWrapper.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/RenderMain.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/CanvasBox.vue (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/CanvasCollection.vue (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/CanvasIcon.vue (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/CanvasImg.vue (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/CanvasPlaceholder.vue (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/CanvasRouterLink.vue (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/CanvasRouterView.vue (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/CanvasSlot.vue (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/CanvasText.vue (1 hunks)
  • packages/runtime-renderer/src/renderer/builtin/index.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/page-function/accessor.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/page-function/blockContext.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/page-function/css-handler.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/page-function/index.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/page-function/state.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/parser/index.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/parser/parser.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/render.ts (1 hunks)
  • packages/runtime-renderer/src/renderer/useContext.ts (1 hunks)
  • packages/runtime-renderer/src/router/index.ts (1 hunks)
  • packages/runtime-renderer/src/stores/index.ts (1 hunks)
  • packages/runtime-renderer/src/types/config.ts (1 hunks)
  • packages/runtime-renderer/src/types/schema.ts (1 hunks)
  • packages/runtime-renderer/src/utils/data-utils.ts (1 hunks)
  • packages/runtime-renderer/vite.config.ts (1 hunks)
  • packages/toolbars/runtime-deploy/index.ts (1 hunks)
  • packages/toolbars/runtime-deploy/meta.js (1 hunks)
  • packages/toolbars/runtime-deploy/package.json (1 hunks)
  • packages/toolbars/runtime-deploy/src/Main.vue (1 hunks)
  • packages/toolbars/runtime-deploy/vite.config.ts (1 hunks)
  • tsconfig.app.json (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-01-14T06:49:00.797Z
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Applied to files:

  • packages/runtime-renderer/src/renderer/builtin/index.ts
  • packages/runtime-renderer/src/renderer/builtin/CanvasIcon.vue
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • designer-demo/package.json
  • tsconfig.app.json
  • packages/runtime-renderer/package.json
📚 Learning: 2025-01-14T06:54:37.955Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/page-block-function/css.ts:3-7
Timestamp: 2025-01-14T06:54:37.955Z
Learning: The setPageCss function in packages/canvas/render/src/page-block-function/css.ts is intentionally kept simple without additional type safety or error handling as per the maintainer's preference.

Applied to files:

  • packages/runtime-renderer/src/renderer/page-function/css-handler.ts
📚 Learning: 2025-01-14T06:49:00.797Z
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered using `app.component('SvgIcon', SvgIcon)` in `packages/svgs/index.js`, making it available throughout Vue components without requiring explicit imports.

Applied to files:

  • packages/runtime-renderer/src/renderer/builtin/CanvasIcon.vue
🧬 Code graph analysis (18)
packages/common/js/runtime-deploy.js (1)
packages/register/src/common.ts (1)
  • getMergeMeta (53-55)
packages/runtime-renderer/src/renderer/LifecycleWrapper.ts (1)
packages/runtime-renderer/src/types/schema.ts (1)
  • JSFunction (129-132)
packages/runtime-renderer/src/app-function/http/index.js (2)
packages/runtime-renderer/src/app-function/http/axios.js (1)
  • config (16-143)
packages/runtime-renderer/src/app-function/dataSource.ts (1)
  • response (110-112)
packages/runtime-renderer/src/app-function/utils.ts (2)
packages/runtime-renderer/src/types/schema.ts (1)
  • Util (184-188)
packages/runtime-renderer/src/utils/data-utils.ts (1)
  • parseJSFunction (15-23)
packages/runtime-renderer/index.ts (3)
packages/runtime-renderer/src/composables/useAppSchema.ts (1)
  • useAppSchema (9-164)
packages/runtime-renderer/src/router/index.ts (1)
  • createAppRouter (78-91)
packages/runtime-renderer/src/stores/index.ts (2)
  • generateStoresConfig (7-26)
  • createStores (28-45)
packages/runtime-renderer/src/renderer/RenderMain.ts (8)
packages/runtime-renderer/src/composables/useAppSchema.ts (1)
  • useAppSchema (9-164)
packages/canvas/render/src/page-block-function/context.ts (1)
  • useContext (15-31)
packages/plugins/materials/src/composable/types.ts (1)
  • Schema (305-317)
packages/runtime-renderer/src/renderer/page-function/state.ts (1)
  • useState (6-46)
packages/runtime-renderer/src/renderer/parser/parser.ts (1)
  • parseData (228-231)
packages/runtime-renderer/src/app-function/utils.ts (1)
  • getUtilsAll (68-70)
packages/runtime-renderer/src/renderer/page-function/css-handler.ts (1)
  • setPageCss (72-82)
packages/runtime-renderer/src/renderer/LifecycleWrapper.ts (1)
  • PageLifecycleWrapper (48-208)
packages/runtime-renderer/src/composables/useAppSchema.ts (2)
packages/runtime-renderer/src/types/schema.ts (4)
  • AppSchema (2-22)
  • Util (184-188)
  • I18nConfig (232-235)
  • BlockItem (248-287)
packages/runtime-renderer/src/app-function/utils.ts (1)
  • initUtils (45-66)
packages/runtime-renderer/src/renderer/page-function/state.ts (2)
packages/runtime-renderer/src/renderer/page-function/accessor.ts (1)
  • useAccessorMap (10-50)
packages/runtime-renderer/src/renderer/parser/parser.ts (2)
  • parseData (228-231)
  • isStateAccessor (66-67)
packages/layout/src/defaultLayout.js (1)
packages/register/src/constants.ts (1)
  • META_APP (24-81)
packages/runtime-renderer/src/renderer/render.ts (3)
packages/runtime-renderer/src/renderer/page-function/blockContext.ts (1)
  • getBlockContext (72-76)
packages/canvas/render/src/render.ts (1)
  • renderDefault (27-35)
packages/runtime-renderer/src/renderer/parser/parser.ts (3)
  • parseData (228-231)
  • parseLoopArgs (238-245)
  • parseCondition (233-236)
packages/runtime-renderer/src/app-function/dataSource.ts (4)
packages/runtime-renderer/src/composables/useAppSchema.ts (1)
  • useAppSchema (9-164)
packages/runtime-renderer/src/utils/data-utils.ts (1)
  • parseJSFunction (15-23)
packages/runtime-renderer/src/app-function/http/axios.js (1)
  • config (16-143)
packages/register/src/hooks.ts (1)
  • useHttp (89-89)
packages/runtime-renderer/src/stores/index.ts (3)
packages/runtime-renderer/src/composables/useAppSchema.ts (1)
  • useAppSchema (9-164)
packages/runtime-renderer/src/utils/data-utils.ts (1)
  • parseJSFunction (15-23)
packages/runtime-renderer/src/types/config.ts (1)
  • StoreConfig (25-30)
packages/runtime-renderer/src/router/index.ts (2)
packages/runtime-renderer/src/composables/useAppSchema.ts (1)
  • useAppSchema (9-164)
packages/runtime-renderer/src/types/config.ts (1)
  • RouteConfig (7-23)
packages/runtime-renderer/src/app-function/http/axios.js (1)
packages/runtime-renderer/src/app-function/dataSource.ts (1)
  • response (110-112)
packages/runtime-renderer/src/renderer/page-function/css-handler.ts (1)
packages/common/js/preview.js (1)
  • pageId (236-236)
packages/runtime-renderer/src/renderer/page-function/blockContext.ts (5)
packages/canvas/render/src/page-block-function/context.ts (1)
  • useContext (15-31)
packages/runtime-renderer/src/renderer/page-function/state.ts (1)
  • useState (6-46)
packages/runtime-renderer/src/renderer/parser/parser.ts (1)
  • parseData (228-231)
packages/runtime-renderer/src/app-function/utils.ts (1)
  • getUtilsAll (68-70)
packages/runtime-renderer/src/renderer/page-function/css-handler.ts (1)
  • getCSSHandler (90-104)
designer-demo/src/runtime.js (1)
packages/runtime-renderer/index.ts (1)
  • initRuntimeRenderer (22-39)
packages/runtime-renderer/src/renderer/parser/parser.ts (4)
packages/vue-generator/src/utils/index.js (1)
  • fnRegexp (20-20)
packages/runtime-renderer/src/utils/data-utils.ts (1)
  • parseJSFunction (15-23)
packages/canvas/render/src/material-function/material-getter.ts (1)
  • getIcon (106-106)
packages/vue-generator/src/generator/vue/sfc/generateAttribute.js (3)
  • propsEntries (641-641)
  • modelValue (642-642)
  • hasUpdateModelValue (643-643)

Comment on lines +14 to +16
async function startApp() {
initRuntimeRenderer()
}
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 | 🔴 Critical

Missing await on async initialization call.

The initRuntimeRenderer() function is async (returns a Promise), but line 15 doesn't await it. This creates several issues:

  1. Initialization errors will become unhandled promise rejections
  2. The returned app instance is lost
  3. startApp() completes before the renderer finishes initializing

Apply this diff to properly await the initialization:

 async function startApp() {
-  initRuntimeRenderer()
+  await initRuntimeRenderer()
 }

Additionally, consider adding error handling:

 async function startApp() {
-  initRuntimeRenderer()
+  try {
+    await initRuntimeRenderer()
+  } catch (error) {
+    console.error('Failed to initialize runtime renderer:', error)
+  }
 }
📝 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
async function startApp() {
initRuntimeRenderer()
}
async function startApp() {
try {
await initRuntimeRenderer()
} catch (error) {
console.error('Failed to initialize runtime renderer:', error)
}
}
🤖 Prompt for AI Agents
In designer-demo/src/runtime.js around lines 14 to 16, the call to the async
initRuntimeRenderer() is not awaited; change startApp to await the call and
return the renderer instance so initialization errors propagate and the app
instance isn't lost, and wrap the awaited call in a try/catch to log or handle
errors (and rethrow or surface them) to avoid unhandled promise rejections.

Comment on lines +17 to +25
const getQueryParams = () => {
const paramsMap = new URLSearchParams(location.search)
const tenant = paramsMap.get('tenant') || ''
const platform = getMergeMeta('engine.config')?.platformId
const appId = paramsMap.get('id')

let query = `id=${appId}&tenant=${tenant}&platform=${platform}`
return query
}
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 | 🟠 Major

Handle missing or null query parameters.

The getQueryParams function concatenates parameters without validating that appId and platform exist. This can result in URLs like id=null&tenant=&platform=undefined.

Apply this diff to handle missing values:

 const getQueryParams = () => {
   const paramsMap = new URLSearchParams(location.search)
   const tenant = paramsMap.get('tenant') || ''
   const platform = getMergeMeta('engine.config')?.platformId
   const appId = paramsMap.get('id')
 
-  let query = `id=${appId}&tenant=${tenant}&platform=${platform}`
+  const params = new URLSearchParams()
+  if (appId) params.set('id', appId)
+  if (tenant) params.set('tenant', tenant)
+  if (platform) params.set('platform', platform)
+  
+  return params.toString()
-  return query
 }

This ensures proper URL encoding and omits parameters with null/undefined values.

📝 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 = () => {
const paramsMap = new URLSearchParams(location.search)
const tenant = paramsMap.get('tenant') || ''
const platform = getMergeMeta('engine.config')?.platformId
const appId = paramsMap.get('id')
let query = `id=${appId}&tenant=${tenant}&platform=${platform}`
return query
}
const getQueryParams = () => {
const paramsMap = new URLSearchParams(location.search)
const tenant = paramsMap.get('tenant') || ''
const platform = getMergeMeta('engine.config')?.platformId
const appId = paramsMap.get('id')
const params = new URLSearchParams()
if (appId) params.set('id', appId)
if (tenant) params.set('tenant', tenant)
if (platform) params.set('platform', platform)
return params.toString()
}
🤖 Prompt for AI Agents
In packages/common/js/runtime-deploy.js around lines 17 to 25, getQueryParams
currently builds a query string by interpolating values directly which can
produce id=null or platform=undefined; change it to create a new URLSearchParams
object and only append keys whose values are neither null nor undefined (and
skip empty tenant if desired), using the existing paramsMap for id/tenant and
getMergeMeta('engine.config')?.platformId for platform; rely on
URLSearchParams.toString() to properly URL-encode values and return that string
so missing values are omitted instead of serialized as "null" or "undefined".

Comment on lines +27 to +42
export const deployPage = async () => {
const href = window.location.href.split('?')[0] || './'
const query = getQueryParams()

const customDeployUrl = getMergeMeta('engine.toolbars.runtimeDeploy')?.options?.deployUrl
const defaultDeployUrl = isDevelopEnv ? `./runtime.html` : `${href.endsWith('/') ? href : `${href}/`}runtime`

let openUrl = ''
openUrl = customDeployUrl
? typeof customDeployUrl === 'function'
? customDeployUrl(defaultDeployUrl, query)
: `${customDeployUrl}?${query}`
: `${defaultDeployUrl}?${query}`

return { openUrl }
}
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 | 🟠 Major

Remove unnecessary async and validate custom deploy URLs.

Two concerns:

  1. The function is marked async but contains no await expressions.
  2. If customDeployUrl is a function, its return value is not validated, which could lead to unexpected behavior or open redirect vulnerabilities if user-controlled data influences the URL.

Apply this diff:

-export const deployPage = async () => {
+export const deployPage = () => {
   const href = window.location.href.split('?')[0] || './'
   const query = getQueryParams()
 
   const customDeployUrl = getMergeMeta('engine.toolbars.runtimeDeploy')?.options?.deployUrl
   const defaultDeployUrl = isDevelopEnv ? `./runtime.html` : `${href.endsWith('/') ? href : `${href}/`}runtime`
 
   let openUrl = ''
   openUrl = customDeployUrl
     ? typeof customDeployUrl === 'function'
-      ? customDeployUrl(defaultDeployUrl, query)
+      ? String(customDeployUrl(defaultDeployUrl, query) || defaultDeployUrl)
       : `${customDeployUrl}?${query}`
     : `${defaultDeployUrl}?${query}`
 
   return { openUrl }
 }

If customDeployUrl functions can be async, keep the async keyword and add await:

? await customDeployUrl(defaultDeployUrl, query)
🤖 Prompt for AI Agents
In packages/common/js/runtime-deploy.js around lines 27 to 42, the function is
incorrectly handling customDeployUrl: keep the async keyword (to support async
custom functions), change the call to await customDeployUrl(defaultDeployUrl,
query) when customDeployUrl is a function, then validate the returned value is a
non-empty string and matches expected URL constraints (e.g., same-origin or a
safe path/pattern) before assigning to openUrl; if validation fails, fall back
to the computed defaultDeployUrl with query and log/handle the invalid custom
URL case.

Comment on lines +44 to +58
export const runtimeDeploy = async () => {
const { openUrl } = await deployPage()

if (runtimeWindow && !runtimeWindow.closed) {
try {
runtimeWindow.focus()
} catch (e) {
// eslint-disable-next-line no-console
console.warn('[runtime-deploy] focus runtime window failed:', e)
}
return
}

runtimeWindow = window.open(openUrl, 'tiny-engine-runtime')
}
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 | 🟠 Major

Handle popup blocker scenario when opening runtime window.

The window.open call can return null if blocked by the browser's popup blocker, but this case is not handled. The code will silently fail and leave runtimeWindow as null, causing subsequent calls to always attempt to open a new window.

Apply this diff:

 export const runtimeDeploy = async () => {
   const { openUrl } = await deployPage()
 
   if (runtimeWindow && !runtimeWindow.closed) {
     try {
       runtimeWindow.focus()
     } catch (e) {
       // eslint-disable-next-line no-console
       console.warn('[runtime-deploy] focus runtime window failed:', e)
     }
     return
   }
 
   runtimeWindow = window.open(openUrl, 'tiny-engine-runtime')
+  
+  if (!runtimeWindow) {
+    // eslint-disable-next-line no-console
+    console.error('[runtime-deploy] Failed to open runtime window. Popup may be blocked.')
+    // Optionally notify the user via UI
+  }
 }
📝 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 runtimeDeploy = async () => {
const { openUrl } = await deployPage()
if (runtimeWindow && !runtimeWindow.closed) {
try {
runtimeWindow.focus()
} catch (e) {
// eslint-disable-next-line no-console
console.warn('[runtime-deploy] focus runtime window failed:', e)
}
return
}
runtimeWindow = window.open(openUrl, 'tiny-engine-runtime')
}
export const runtimeDeploy = async () => {
const { openUrl } = await deployPage()
if (runtimeWindow && !runtimeWindow.closed) {
try {
runtimeWindow.focus()
} catch (e) {
// eslint-disable-next-line no-console
console.warn('[runtime-deploy] focus runtime window failed:', e)
}
return
}
runtimeWindow = window.open(openUrl, 'tiny-engine-runtime')
if (!runtimeWindow) {
// eslint-disable-next-line no-console
console.error('[runtime-deploy] Failed to open runtime window. Popup may be blocked.')
// Optionally notify the user via UI
}
}
🤖 Prompt for AI Agents
In packages/common/js/runtime-deploy.js around lines 44 to 58, handle the case
when window.open is blocked and returns null: assign the result of window.open
to a local variable, check if it's null, and if so log/warn about the popup
blocker and use a fallback (for example navigate the current tab to openUrl or
surface a user-facing notification asking to enable popups). If window.open
returns a Window object, assign it to runtimeWindow as before; otherwise leave
runtimeWindow null and return after performing the fallback so subsequent calls
can retry.

Comment on lines +23 to +26
const searchParams = new URLSearchParams(location.search)
const appId = searchParams.get('id')
const { fetchAppSchema, fetchBlocks } = useAppSchema()
await fetchAppSchema(appId || '')
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 | 🟠 Major

Add validation and error handling for the appId parameter.

The code does not validate or handle the case where appId is null or empty. If the id query parameter is missing, fetchAppSchema will be called with an empty string, which may cause a silent failure or unexpected behavior.

Add validation:

  const searchParams = new URLSearchParams(location.search)
  const appId = searchParams.get('id')
+
+  if (!appId) {
+    throw new Error('Missing required "id" query parameter')
+  }
+
  const { fetchAppSchema, fetchBlocks } = useAppSchema()
-  await fetchAppSchema(appId || '')
+  await fetchAppSchema(appId)
  await fetchBlocks()
📝 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 searchParams = new URLSearchParams(location.search)
const appId = searchParams.get('id')
const { fetchAppSchema, fetchBlocks } = useAppSchema()
await fetchAppSchema(appId || '')
const searchParams = new URLSearchParams(location.search)
const appId = searchParams.get('id')
if (!appId) {
throw new Error('Missing required "id" query parameter')
}
const { fetchAppSchema, fetchBlocks } = useAppSchema()
await fetchAppSchema(appId)
await fetchBlocks()
🤖 Prompt for AI Agents
In packages/runtime-renderer/index.ts around lines 23 to 26, the code reads the
id query param and calls fetchAppSchema with an empty string when id is missing;
add validation to ensure appId is present and non-empty before calling
fetchAppSchema, return or throw a clear error (or display a user-friendly
message) when appId is null/empty, and log the invalid input; optionally
validate the format of appId if required and avoid calling
fetchBlocks/fetchAppSchema until a valid id is provided.

Comment on lines +38 to +54
if (isChildRoute) {
const parentId = parseInt(page.meta.parentId)
const parentRoute = routesConfig.find((r) => r.meta?.pageId === parentId)
if (parentRoute) {
parentRoute.children = parentRoute.children || []
parentRoute.children.push(routeConfigCurrent)
parentRoute.meta.hasChildren = true
if (routeConfigCurrent.meta.isDefault) {
parentRoute.meta.hasDefault = true
parentRoute.meta.defaultPath = `${parentRoute.path}/${routeConfigCurrent.path}`
parentRoute.redirect = parentRoute.meta.defaultPath
}
return
}
} else {
routesConfig.push(routeConfigCurrent)
}
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 | 🟠 Major

Handle missing parent route explicitly.

If a child route's parent is not found (line 40), the route is silently skipped (line 50 returns early). This could lead to confusing bugs where child routes disappear without any error message, especially if the page order in the schema is unexpected.

Add explicit error handling:

 if (isChildRoute) {
   const parentId = parseInt(page.meta.parentId, 10)
   const parentRoute = routesConfig.find((r) => r.meta?.pageId === parentId)
   if (parentRoute) {
     parentRoute.children = parentRoute.children || []
     parentRoute.children.push(routeConfigCurrent)
     parentRoute.meta.hasChildren = true
     if (routeConfigCurrent.meta.isDefault) {
       parentRoute.meta.hasDefault = true
       parentRoute.meta.defaultPath = `${parentRoute.path}/${routeConfigCurrent.path}`
       parentRoute.redirect = parentRoute.meta.defaultPath
     }
     return
+  } else {
+    // eslint-disable-next-line no-console
+    console.error(
+      `父路由未找到: 页面 "${page.meta.name}" (ID: ${page.meta.id}) 引用的父路由 ID ${parentId} 不存在`
+    )
   }
 } else {

Also add radix parameter to parseInt:

-  const parentId = parseInt(page.meta.parentId)
+  const parentId = parseInt(page.meta.parentId, 10)
📝 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 (isChildRoute) {
const parentId = parseInt(page.meta.parentId)
const parentRoute = routesConfig.find((r) => r.meta?.pageId === parentId)
if (parentRoute) {
parentRoute.children = parentRoute.children || []
parentRoute.children.push(routeConfigCurrent)
parentRoute.meta.hasChildren = true
if (routeConfigCurrent.meta.isDefault) {
parentRoute.meta.hasDefault = true
parentRoute.meta.defaultPath = `${parentRoute.path}/${routeConfigCurrent.path}`
parentRoute.redirect = parentRoute.meta.defaultPath
}
return
}
} else {
routesConfig.push(routeConfigCurrent)
}
if (isChildRoute) {
const parentId = parseInt(page.meta.parentId, 10)
const parentRoute = routesConfig.find((r) => r.meta?.pageId === parentId)
if (parentRoute) {
parentRoute.children = parentRoute.children || []
parentRoute.children.push(routeConfigCurrent)
parentRoute.meta.hasChildren = true
if (routeConfigCurrent.meta.isDefault) {
parentRoute.meta.hasDefault = true
parentRoute.meta.defaultPath = `${parentRoute.path}/${routeConfigCurrent.path}`
parentRoute.redirect = parentRoute.meta.defaultPath
}
return
} else {
// eslint-disable-next-line no-console
console.error(
`父路由未找到: 页面 "${page.meta.name}" (ID: ${page.meta.id}) 引用的父路由 ID ${parentId} 不存在`
)
}
} else {
routesConfig.push(routeConfigCurrent)
}
🤖 Prompt for AI Agents
In packages/runtime-renderer/src/router/index.ts around lines 38 to 54, child
routes are silently skipped when their parent is not found and parseInt is
called without a radix; update the code to call parseInt(page.meta.parentId, 10)
and, if parentRoute is not found, log or throw an explicit error/warning
(include page id and route info) and still add the child to a fallback
collection or push it to routesConfig as a top-level route so it is not lost;
ensure the error path does not just return silently but provides context for
debugging.

Comment on lines +7 to +26
export const generateStoresConfig = () => {
const { globalStates } = useAppSchema()
if (globalStates.value.length === 0) return []
return globalStates.value.map((store) => ({
id: store.id,
state: JSON.parse(JSON.stringify(store.state)),
actions: Object.fromEntries(
Object.keys(store.actions || {}).map((key) => {
// 使用 parseJSFunction ,但是上下文由pinia内部绑定
return [key, parseJSFunction(store.actions[key], {}, {})]
})
),
getters: Object.fromEntries(
Object.keys(store.getters || {}).map((key) => {
// 同样处理 getters
return [key, parseJSFunction(store.getters[key], {}, {})]
})
)
}))
}
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 | 🟠 Major

Fix deep cloning method and add error handling.

There are several issues with generateStoresConfig:

  1. Line 12: Using JSON.parse(JSON.stringify()) for deep cloning is fragile and will fail or produce incorrect results for:

    • Functions (will be dropped)
    • Date objects (converted to strings)
    • undefined values (dropped)
    • Circular references (throws error)
    • Special objects like Map, Set, RegExp
  2. Lines 16, 22: No error handling if parseJSFunction fails or returns undefined.

  3. Line 15: Chinese comment should be translated to English.

Apply this diff:

+import { cloneDeep } from 'lodash-es' // or implement a proper deep clone utility
+
 export const generateStoresConfig = () => {
   const { globalStates } = useAppSchema()
   if (globalStates.value.length === 0) return []
   return globalStates.value.map((store) => ({
     id: store.id,
-    state: JSON.parse(JSON.stringify(store.state)),
+    state: cloneDeep(store.state), // Use a proper deep clone utility
     actions: Object.fromEntries(
       Object.keys(store.actions || {}).map((key) => {
-        // 使用 parseJSFunction ,但是上下文由pinia内部绑定
-        return [key, parseJSFunction(store.actions[key], {}, {})]
+        // parseJSFunction returns the parsed function; context is bound by Pinia internally
+        const fn = parseJSFunction(store.actions[key], {}, {})
+        if (!fn) {
+          console.error(`Failed to parse action: ${key} in store: ${store.id}`)
+          return [key, () => {}] // fallback to noop
+        }
+        return [key, fn]
       })
     ),
     getters: Object.fromEntries(
       Object.keys(store.getters || {}).map((key) => {
-        // 同样处理 getters
-        return [key, parseJSFunction(store.getters[key], {}, {})]
+        // Parse getters similarly
+        const fn = parseJSFunction(store.getters[key], {}, {})
+        if (!fn) {
+          console.error(`Failed to parse getter: ${key} in store: ${store.id}`)
+          return [key, () => undefined] // fallback
+        }
+        return [key, fn]
       })
     )
   }))
 }

Note: You'll need to install lodash-es or implement a proper deep clone utility that handles functions, dates, and circular references correctly.

📝 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 generateStoresConfig = () => {
const { globalStates } = useAppSchema()
if (globalStates.value.length === 0) return []
return globalStates.value.map((store) => ({
id: store.id,
state: JSON.parse(JSON.stringify(store.state)),
actions: Object.fromEntries(
Object.keys(store.actions || {}).map((key) => {
// 使用 parseJSFunction ,但是上下文由pinia内部绑定
return [key, parseJSFunction(store.actions[key], {}, {})]
})
),
getters: Object.fromEntries(
Object.keys(store.getters || {}).map((key) => {
// 同样处理 getters
return [key, parseJSFunction(store.getters[key], {}, {})]
})
)
}))
}
import { cloneDeep } from 'lodash-es' // or replace with your deep-clone utility of choice
export const generateStoresConfig = () => {
const { globalStates } = useAppSchema()
if (globalStates.value.length === 0) return []
return globalStates.value.map((store) => ({
id: store.id,
state: cloneDeep(store.state), // Use a proper deep clone utility
actions: Object.fromEntries(
Object.keys(store.actions || {}).map((key) => {
// parseJSFunction returns the parsed function; context is bound by Pinia internally
const fn = parseJSFunction(store.actions[key], {}, {})
if (!fn) {
console.error(`Failed to parse action: ${key} in store: ${store.id}`)
return [key, () => {}] // fallback to noop
}
return [key, fn]
})
),
getters: Object.fromEntries(
Object.keys(store.getters || {}).map((key) => {
// Parse getters similarly
const fn = parseJSFunction(store.getters[key], {}, {})
if (!fn) {
console.error(`Failed to parse getter: ${key} in store: ${store.id}`)
return [key, () => undefined] // fallback
}
return [key, fn]
})
)
}))
}
🤖 Prompt for AI Agents
In packages/runtime-renderer/src/stores/index.ts around lines 7 to 26, replace
the fragile JSON.parse(JSON.stringify(...)) deep clone with a proper deep-clone
that preserves functions, Date objects and handles circular refs (e.g., use
lodash-es cloneDeep or an existing project clone utility), translate the Chinese
comments to English, and wrap each parseJSFunction call for actions and getters
in try/catch so failures return a safe fallback (e.g., undefined or a noop) and
log or surface the error; ensure the mapped actions/getters entries use the
cloned state and continue to produce an Object.fromEntries shape even if some
functions fail.

Comment on lines +14 to +23
// 用于解析store中的actions和getters
export const parseJSFunction = (data: any, _scope: any = null, _ctx: any = null) => {
try {
const fn = newFn(`return ${data.value}`).call(null) // 拿到函数本体,不绑定任何 this
return fn
} catch (error) {
// eslint-disable-next-line no-console
console.error('函数声明解析报错:', error, 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 | 🔴 Critical

Critical: Unsafe dynamic code execution without sanitization.

parseJSFunction uses the Function constructor with untrusted data.value input, creating a code injection vulnerability. This pattern allows arbitrary code execution if an attacker can control the schema data. Additionally, errors are only logged to the console rather than being surfaced to monitoring systems.

Consider these security improvements:

  1. Validate and sanitize data.value before execution
  2. Implement a whitelist of allowed function patterns
  3. Use a safe expression parser instead of dynamic function creation
  4. Add proper error logging/monitoring instead of console.error
  5. Consider using a sandboxed execution environment

Example safer pattern:

export const parseJSFunction = (data: any, _scope: any = null, _ctx: any = null) => {
  try {
    // Validate input format
    if (!data?.value || typeof data.value !== 'string') {
      throw new Error('Invalid function data')
    }
    
    // Add validation/sanitization here
    if (containsUnsafePatterns(data.value)) {
      throw new Error('Unsafe function pattern detected')
    }
    
    const fn = newFn(`return ${data.value}`).call(null)
    return fn
  } catch (error) {
    // Use proper logging framework
    logger.error('Function parsing failed:', { error, data })
    // Consider throwing or returning a safe default
    return undefined
  }
}
🤖 Prompt for AI Agents
In packages/runtime-renderer/src/utils/data-utils.ts around lines 14 to 23,
parseJSFunction currently uses the Function constructor on untrusted data.value
and only console.errors, creating a code injection risk; replace this by first
validating that data?.value is a non-empty string, run a whitelist or
regexp-based check for allowed patterns (or prefer using a safe expression
parser/evaluator such as jsep/safe-eval/sandboxed interpreter instead of new
Function), avoid calling the Function constructor on raw input, and ensure
failures are reported via the project logger (logger.error with error and data)
and return undefined or a safe default rather than executing unsafe code; if
dynamic execution is absolutely required, perform strict sanitization, limit
allowed identifiers/operators, and run inside a sandboxed environment.

default: 'video'
},
renderType: 'icon',
deployUrl: ''
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 | 🟠 Major

🧩 Analysis chain

Document deployUrl configuration expectations.

The deployUrl option defaults to an empty string. Ensure that:

  • Runtime configuration or user setup properly initializes this value
  • The toolbar handles missing/empty deployUrl gracefully (shows error or disables the button)
  • Documentation explains how to configure the deploy URL

🏁 Script executed:

#!/bin/bash
# Search for deployUrl usage and configuration handling
rg -n "deployUrl" packages/toolbars/runtime-deploy/ -A 3 -B 1

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Find all occurrences of deployUrl in the repository
rg -n "deployUrl" -C3 .

Length of output: 963


🏁 Script executed:

#!/bin/bash
sed -n '1,100p' packages/common/js/runtime-deploy.js

Length of output: 1898


Add documentation for deployUrl configuration
deployUrl defaults to an empty string—at runtime this falsy value causes the toolbar to fall back to the built-in defaultDeployUrl. Add or update your docs to cover:

  • How to override deployUrl via the runtime meta config
  • The default fallback URL format (./runtime.html in dev, or …/runtime otherwise)
  • How empty or missing deployUrl values are handled gracefully ([runtime-deploy.js lines 27–37])
🤖 Prompt for AI Agents
In packages/toolbars/runtime-deploy/meta.js around line 10, the meta file
currently sets deployUrl to an empty string but lacks documentation; update the
project docs (or add inline comments) to explain how to override deployUrl in
the runtime meta config, state the default fallback behavior (use ./runtime.html
in dev, and ../runtime or the production runtime path otherwise), and note that
empty or missing deployUrl values are handled gracefully by the runtime (see
runtime-deploy.js lines 27–37) so users know the runtime will fall back to
defaultDeployUrl; include an example override, the exact fallback URL formats,
and a short note referencing the runtime code location.

"@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"vite": "^5.4.2"
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 | 🔴 Critical

🧩 Analysis chain

Verify Vite version consistency across packages.

This package uses vite: ^5.4.2, but packages/runtime-renderer/package.json specifies vite: ^4.0.0. Version mismatches can cause build inconsistencies in a monorepo.

Run the following script to check Vite versions across all packages:


🏁 Script executed:

#!/bin/bash
# Description: Find all Vite version declarations across the monorepo

echo "=== Vite versions in package.json files ==="
fd -t f '^package\.json$' -x grep -H '"vite"' {} \;

Length of output: 4071


Align Vite version in runtime-renderer with monorepo

packages/runtime-renderer/package.json

-    "vite": "^4.0.0"
+    "vite": "^5.4.2"

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

🤖 Prompt for AI Agents
In packages/toolbars/runtime-deploy/package.json around line 35, the vite
dependency is set to "^5.4.2" but the PR comment requests aligning the Vite
version with packages/runtime-renderer; update this dependency to match the
exact "vite" version specified in packages/runtime-renderer/package.json, save
the change, then run the repo's package manager (pnpm/yarn/npm) to update
lockfiles so all workspace packages use the same Vite version.

@littla2liyou littla2liyou changed the title Ospp 2025/runtime rendering feat: Add runtime-rendering-solution Sep 30, 2025
@github-actions github-actions bot added the enhancement New feature or request label Sep 30, 2025
@littla2liyou littla2liyou changed the title feat: Add runtime-rendering-solution feat: Add runtime rendering solution Sep 30, 2025
@littla2liyou littla2liyou changed the title feat: Add runtime rendering solution feat: add runtime rendering solution Sep 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request ospp ospp
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant