@hy-bricks/core
v0.3.0
Published
HyperCard 运行时核心 — 编译用户源码 / CSS scope / 实例注册表 / 跨实例 SDK / JS 静态解析
Maintainers
Readme
@hy-bricks/core
HyperCard 低代码运行时核心 — Vue 3 SDK。把用户写的
{html, js, css}编译成真 Vue 组件、做 CSS scope、跨实例事件总线、统一 libs 注入。
30 秒 Hello World
pnpm add @hy-bricks/core vue element-plus axios// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import axios from 'axios'
import { createHyperCard } from '@hy-bricks/core'
import App from './App.vue'
const app = createApp(App)
app.use(
createHyperCard({
libs: {
ui: ElementPlus, // SDK 自动 app.use + 挂 libs.ui
http: axios, // 普通对象/函数 → 只挂 libs.http
},
}),
)
app.mount('#app')<!-- App.vue -->
<script setup lang="ts">
import { RuntimeBox } from '@hy-bricks/core'
const html = `<div class="hello"><el-button @click="say">点我</el-button></div>`
const js = `
export default {
methods: {
say() {
__HYPERCARD__.libs.http.get('/api/ping')
alert('hello hypercard')
},
},
}
`
const css = `.hello { padding: 16px; }`
</script>
<template>
<RuntimeBox
instance-id="demo-1"
component-id="hello"
:html-source="html"
:js-source="js"
:css-source="css"
/>
</template>启动后控制台应看到:
[hypercard] 已注册 2 个 libs:
ui ✓ Vue plugin → 已 app.use
http · function安装
pnpm add @hy-bricks/core
# peerDeps: vue ^3.5运行时依赖(自动装):postcss mitt acorn acorn-walk。
发布形态:
| 字段 | 路径 |
|---|---|
| main(CJS) | dist/index.cjs |
| module(ESM) | dist/index.mjs |
| types | dist/index.d.ts |
核心 API
Vue plugin 工厂
| API | 说明 | 用法 |
|---|---|---|
| createHyperCard(config) | 主入口,返回 Vue plugin | app.use(createHyperCard({ libs })) |
| isVuePlugin(v) | 检测是不是 { install: fn } 对象 / [plugin, opts] 元组 | if (isVuePlugin(x)) ... |
| HC_INJECT_KEY | provide/inject 的 key,值是字符串 '__HYPERCARD__' | inject(HC_INJECT_KEY) |
| VERSION | SDK 包版本字符串 | '0.1.0' |
| HyperCardConfig HyperCardInstance LibsRegistry | TS 类型 | 见下文 |
编译用户源码
| API | 说明 |
|---|---|
| compileComponent(source) | {html, js} → Promise<CompiledComponent>(Vue.compile + acorn parse ExportDefaultDeclaration + new Function('module','exports', ...) CJS 跑 + LRU 缓存)|
| getCompileCacheSize() | 返回缓存内编译产物个数 |
| clearCompileCache() | 清缓存(切换租户 / 项目时用)|
| CompiledComponent ComponentMeta ComponentSource CustomDecl | TS 类型 |
CSS scope
为同一 scopeId 的多实例共享一份带 [data-vbi-type="<scopeId>"] 前缀的 <style>,refCount 归零自动卸载。
scopeId 是稳定字符串,通常情况:
- 编辑器场景:scopeId = componentId(API 默认)
- 多版本画布场景:scopeId =
${componentId}@${version}(同页同组件不同版本各自隔离)
| API | 说明 |
|---|---|
| scopeCss(rawCss, scopeId) | 纯函数:返回带前缀的 css 字符串 |
| attachScope(scopeId, rawCss) | 实例 mount:refCount++,首次注入 <style> |
| detachScope(scopeId) | 实例 unmount:refCount--,归零移除 <style> |
| injectScopedCss(scopeId, rawCss) | 只刷新内容,不动 refCount(watch(cssSource) 用)|
| removeScopedCss(scopeId) | 强制移除(测试 / reset 用)|
| getScopedCssCount() | 返回已挂载 scope 个数 |
| debugScopedCssRefs() | dev 调试,返回每个 scope 的 {scopeId, componentId(deprecated alias), refCount, cssHash} |
注意:
compileComponent的 LRU 缓存仍按 sourceHash(不分 scopeId)— 编译产物只跟 html/js 有关,加 scopeId 反而降低复用率。scopeId只管 CSS / DOM 隔离。
实例注册表
跨实例方法调用 + 事件总线。RuntimeBox 会自动完成 register / unregister。
| API | 说明 |
|---|---|
| register(instanceId, componentId, vm) | 注册,返回 ComponentInstanceHandle |
| unregister(instanceId) | 注销,自动 dispose mitt |
| getInstance(id) | 拿 handle |
| listInstances({ componentId? }) | 列实例,可按 componentId 过滤 |
| registryVersion | Ref<number>,register/unregister 时 +1,UI watch 它触发刷新 |
| ComponentInstanceHandle | { instanceId, componentId, vm, call, on, emit, setProp, setDataInput } |
setDataInput(key, value)走vm.custom[key].value(customValues / binding runtime 通路)。 跟setProp区分:setProp写顶层vm[key](host 主动改 prop);setDataInput走组件作者声明的custom.<key>.dataInput = trueslot,canvas binding runtime 灌数据用这条。 边界:vm.custom[key]不存在 →console.warn+ no-op;slot 形状不是{ value: ... }→ 同样 warn + no-op。
跨实例 runtime
__HYPERCARD__.runtime 暴露的就是它,直接 import { runtime } from '@hy-bricks/core' 也行。
| 方法 | 签名 |
|---|---|
| runtime.call(id, method, ...args) | 调对方 vm 上的方法 |
| runtime.on(id, event, handler) | 订阅,返回 unsubscribe |
| runtime.emit(id, event, payload) | 触发对方事件 |
| runtime.getInstance(id) / listInstances(filter) | 查 |
assets 当前是 { pickerUrl(id) } 占位,后续接资源中心。
静态解析
import { parseComponentSource } from '@hy-bricks/core'
const parsed = parseComponentSource(jsSource)
// → { methodsDecl: [{ name, params }], emitsDecl: [{ event }], propsDecl: [{ key, value, type, name, enum }] }acorn 静态扫 export default {} 块,抽 methods / emits / custom 字段供属性面板和补全用。
容器组件
<RuntimeBox>
实例容器:接源码 + 配置,自动跑 compile + attachScope + register 全套流程。
Props:
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| instanceId | string | 必传 | 实例唯一 ID,注册表 + 事件总线用 |
| componentId | string | 必传 | 组件 ID,CSS scope 默认键 |
| scopeId | string? | componentId | CSS/DOM 隔离键。编辑器场景不传(默认=componentId);画布场景必传(如 'button@v3',同组件不同版本各自隔离) |
| htmlSource | string | 必传 | 用户写的 HTML(Vue template) |
| jsSource | string | 必传 | 用户写的 JS(Vue Options + 平台扩展字段) |
| cssSource | string? | '' | 用户写的 CSS(PostCSS 自动加 [data-vbi-type] scope) |
| customValues | Record<string, unknown>? | undefined | 覆盖 custom.*.value 的运行时值 — 画布属性面板通过它做所见即所得。不传时用 declaration 默认值(编辑器场景) |
| position | { x: number, y: number }? | undefined | 绝对定位(画布场景),不传时流式布局(编辑器场景) |
| size | { w: number, h: number }? | undefined | 尺寸(画布场景用) |
| zIndex | number? | undefined | 层级(画布场景用) |
画布场景 vs 编辑器场景:
<!-- 编辑器场景:一个组件一个实例,不需要 scopeId -->
<RuntimeBox
instance-id="editor-preview-1"
component-id="cmp_abc"
:html-source="html"
:js-source="js"
:css-source="css"
/>
<!-- 画布场景:同组件不同版本共存,必须传 scopeId;属性面板通过 customValues 透传 -->
<RuntimeBox
instance-id="page-inst-1"
component-id="cmp_abc"
scope-id="cmp_abc@v3"
:html-source="html"
:js-source="js"
:css-source="css"
:custom-values="{ title: 'Hello' }"
:position="{ x: 100, y: 200 }"
:size="{ w: 320, h: 240 }"
:z-index="1"
/><ErrorBoundary>
| 组件 | 作用 |
|---|---|
| <ErrorBoundary> | 渲染期错误兜底,接 label? |
createHyperCard 详细配置
interface HyperCardConfig {
libs?: LibsConfig // 注入到 __HYPERCARD__.libs 的库 / 函数 / 常量,key 任意
strict?: boolean // 默认 false。true 时启动检测一旦 warn 就抛 Error
version?: string // 默认 'v1',挂在 instance.version 上,组件兼容判断用
}libs 槽位允许的形态
createHyperCard({
libs: {
ui: ElementPlus, // ① Vue plugin 对象 → 自动 app.use,libs.ui = plugin
ui2: [ElementPlus, { locale: zhCn }], // ② 元组 → app.use(plugin, options),libs.ui2 = plugin 本身
http: axios, // ③ 函数 → 原样挂(裸函数不当 plugin)
constants: { brand: 'hy' }, // ④ 普通对象 → 原样挂
formatPrice: (n: number) => `¥${n}`, // ⑤ 函数 / 工具
},
})启动检测
install 里会一次性 console.info 出注册清单,例如:
[hypercard] 已注册 5 个 libs:
ui ✓ Vue plugin → 已 app.use
ui2 ✓ Vue plugin (with options) → 已 app.use
http · function
constants · object
formatPrice · function紧接着会 console.warn(strict: true 则 throw):
| 触发条件 | 解释 |
|---|---|
| 多个 Vue plugin 共存 | 全局组件命名可能撞车,提示自查 |
| 命中保留词 | vue / Vue / window / global / globalThis / console / document / process / $ / libs / runtime / assets / version 不能作为 lib 名 |
| app.use(plugin) 抛错 | 普通模式 console.error 继续,strict 模式 throw |
LibsRegistry 类型 augment(必看)
SDK 自身的 LibsRegistry 是空接口,宿主必须在自己项目里 augment 一次,IDE 才能在 __HYPERCARD__.libs.<name> 上给出补全。
新建 src/types/hypercard.d.ts:
import type { default as ElementPlus } from 'element-plus'
import type { AxiosStatic } from 'axios'
declare module '@hy-bricks/core' {
interface LibsRegistry {
ui: typeof ElementPlus
http: AxiosStatic
formatPrice: (n: number) => string
}
}
declare global {
interface Window {
__HYPERCARD__?: import('@hy-bricks/core').HyperCardInstance
}
}
export {}确保 tsconfig.json 的 include 覆盖到该文件。之后:
- 宿主 setup 里
inject(HC_INJECT_KEY).libs.http.→ 完整 axios 补全 - 组件作者源码里
__HYPERCARD__.libs.formatPrice(99)→ 类型校验
提示:
LibsRegistry只影响类型,运行时只看你createHyperCard({ libs })实际传了啥 — augment 写漏 / 写错不会让 SDK 崩,只会少补全。
三种访问 instance 的路径
SDK 把 HyperCardInstance 同时挂在三个位置,按使用场景挑。
1. window.__HYPERCARD__ — 组件作者源码用
{html, js, css} 是字符串,跑在 new Function('module','exports', ...) 里(SDK 通过 acorn parse 把 export default 段重写成 module.exports = ,所以你只能写 export default { ... } Vue Options + 用 __HYPERCARD__.libs.* 拿运行时依赖,不支持 import ... from / named export)。组件作者代码里没有 ESM import 上下文,直接拿全局:
// 组件作者写的 jsSource
export default {
async mounted() {
const { data } = await __HYPERCARD__.libs.http.get('/api/me')
this.user = data
__HYPERCARD__.runtime.emit('header', 'user-loaded', data)
},
}2. inject(HC_INJECT_KEY) — 宿主自己的 Vue setup
import { inject } from 'vue'
import { HC_INJECT_KEY, type HyperCardInstance } from '@hy-bricks/core'
const hc = inject<HyperCardInstance>(HC_INJECT_KEY)!
hc.runtime.call('list-1', 'refresh')3. this.$hc — Options API
export default {
mounted() {
this.$hc.runtime.emit('toast', 'show', { msg: '保存成功' })
},
}类型扩展(ComponentCustomProperties.$hc)由 SDK 内置,import '@hy-bricks/core' 后自动生效。
Troubleshooting
实际接入时踩过的坑,按"症状 → 原因 → 解法"列。
误传函数库当 plugin(Maximum call stack size exceeded)
- 症状:
app.use(createHyperCard({ libs: { http: axios } }))时栈溢出 /axios is not a function。 - 原因:Vue 的
app.use看到函数会当成 legacy plugin 调,axios 又是个函数,递归自调爆栈。 - 解法:SDK 已用
isVuePlugin(只认{ install: fn }对象 /[obj, opts]元组)拒掉裸函数。不要手动包{ install: axios },直接传 axios 即可,SDK 会原样挂到libs.http。极少数老 plugin 是纯函数签名时,自己包成{ install(app) { ... } }。
Tailwind v3 ignore node_modules,SDK 内 class 扫不到
症状:用了
@hy-bricks/editor后样式发飞,inspector 看到 class 没生成。原因:Tailwind v3 默认
content不扫node_modules,SDK dist 内 class 全被 purge。解法:宿主的
tailwind.config.js加 preset 或扩 content:module.exports = { presets: [require('@hy-bricks/editor/tailwind-preset')], content: [ './index.html', './src/**/*.{vue,ts,tsx}', './node_modules/@hy-bricks/editor/dist/**/*.{js,mjs,cjs}', ], }
@import '@hy-bricks/...' PostCSS 不识别 npm scope alias
- 症状:
@import '@hy-bricks/editor/style.css'编译报Can't resolve。 - 原因:PostCSS
@import走自己的解析器,不认 vite/webpack 的 npm scope。 - 解法:在 JS 入口
import '@hy-bricks/editor/style.css'(走 bundler 的 ESM 解析),不要在 CSS 里@import。
Vite ?worker query 在 SDK build 后保留
- 症状:宿主 dev 启动
Could not resolve ".../editor.worker?worker"。 - 原因:
?worker是 Vite 专属语法,打进 dist 后 esbuild dev 不认。 - 解法:
@hy-bricks/editor已改成 monaco worker 工厂动态注册,直接装 npm 上的发布版本即可,宿主 dev 启动后无 query 残留。
启动后 __HYPERCARD__.libs.xxx IDE 无补全
- 症状:类型
unknown,.后没提示。 - 原因:没在宿主项目 augment
LibsRegistry(SDK 自身故意保持空接口,避免硬绑具体库)。 - 解法:照 LibsRegistry 类型 augment 一节,落一份
src/types/hypercard.d.ts。
配合使用 / Ecosystem
- @hy-bricks/editor — 嵌入式低代码编辑器(Monaco 三栏 + 实时预览)。需要 IDE 体验时一起装
- @hy-bricks/canvas — 多组件实例画布 + 自由布局 + binding 协议
- @hy-bricks/devtools — 运行时诊断浮窗
License
MIT © hy_top
