@vobs/vobs 0.3.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 vobsjs
3
+ Copyright (c) 2026 vobs contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @vobs/vobs
2
+
3
+ The umbrella package of the vobs framework: reactivity + runtime + DOM renderer in a single import, plus application bootstrap (`createVobs`), context (`provide`/`inject`), and the JSX runtime.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @vobs/vobs
9
+ ```
10
+
11
+ Pair with [`@vobs/vite-plugin`](../vite-plugin) to compile TSX.
12
+
13
+ ## Quick start
14
+
15
+ ```tsx
16
+ import { createVobs, createInjectionKey, state, inject, provide } from '@vobs/vobs'
17
+
18
+ const GreetingKey = createInjectionKey<string>('greeting')
19
+
20
+ function Hello() {
21
+ const greeting = injectRequired(GreetingKey)
22
+ return <h1>{greeting}</h1>
23
+ }
24
+
25
+ const app = createVobs({
26
+ render: () => <Hello />,
27
+ plugins: []
28
+ })
29
+
30
+ app.mount('#app') // or app.hydrate('#app') for SSR markup
31
+ ```
32
+
33
+ Plugins receive an app context and can provide values, register router/auth/etc., and return a cleanup function that runs on `app.destroy()`.
34
+
35
+ ### Context
36
+
37
+ ```tsx
38
+ import { createInjectionKey, provide, inject } from '@vobs/vobs'
39
+
40
+ const ThemeKey = createInjectionKey<'dark' | 'light'>('theme')
41
+
42
+ function Toolbar() {
43
+ provide(ThemeKey, 'dark') // scoped to the current owner subtree
44
+ const theme = inject(ThemeKey, 'light') // read with fallback
45
+ }
46
+ ```
47
+
48
+ ### Reactivity and rendering
49
+
50
+ Everything from `@vobs/reactivity` (`state`, `memo`, `effect`, `batch`, owner APIs) and the rendering primitives from `@vobs/runtime` (`insertDynamic`, `insertList`, boundaries, refs) are re-exported, with the DOM renderer pre-registered:
51
+
52
+ ```tsx
53
+ import { state, ErrorBoundary } from '@vobs/vobs'
54
+
55
+ function Counter() {
56
+ const count = state(0)
57
+ return <button onClick={() => count.value++}>Count: {count.value}</button>
58
+ }
59
+
60
+ <ErrorBoundary fallback={error => <span>failed: {error.message}</span>}>
61
+ <Counter />
62
+ </ErrorBoundary>
63
+ ```
64
+
65
+ ## App lifecycle
66
+
67
+ | Member | Description |
68
+ | --- | --- |
69
+ | `app.mount(target)` | Clears the target and renders into it. |
70
+ | `app.hydrate(target)` | Claims server-rendered markup instead of clearing it; mismatched nodes are replaced with a reported error. |
71
+ | `app.update()` | Flushes pending scheduler work synchronously (used by HMR and tests). |
72
+ | `app.use(plugin)` | Installs a plugin before or after mount. |
73
+ | `app.provide(key, value, options?)` | Provides a value into the root owner. |
74
+ | `app.destroy()` | Runs plugin cleanups, then disposes the owner tree and clears the container. |
75
+
76
+ ## API highlights
77
+
78
+ | Signature | Description |
79
+ | --- | --- |
80
+ | `createVobs(options)` | Creates an app (`render`, `plugins`). |
81
+ | `createInjectionKey(description)` | Typed injection key (`Symbol`-based). |
82
+ | `provide(key, value, options?)` / `inject(key, fallback?)` / `injectRequired(key)` | Owner-scoped context. |
83
+ | `Fragment` / `Vobs` | JSX fragment and runtime entry points (`jsx-runtime`, `jsx-dev-runtime`). |
84
+ | `createDOMRenderer()` | The built-in DOM renderer. |
85
+ | re-exports | Full `@vobs/reactivity` + runtime ops, bindings, lists, boundaries, HMR. |
86
+
87
+ ## Types
88
+
89
+ `VobsApp`, `VobsPlugin`, `VobsPluginContext`, `InjectionKey<T>`, plus all re-exported reactivity/runtime types.
package/package.json CHANGED
@@ -1,54 +1,23 @@
1
1
  {
2
- "name": "@vobs/vobs",
3
- "version": "0.3.0",
4
- "description": "The default application entry for the vobs frontend framework.",
5
- "type": "module",
6
- "publishConfig": {
7
- "access": "public"
8
- },
9
2
  "license": "MIT",
10
- "author": "vobsjs",
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/vobsjs/vobs.git",
14
- "directory": "packages/framework/vobs"
15
- },
16
- "bugs": {
17
- "url": "https://github.com/vobsjs/vobs/issues"
18
- },
19
- "homepage": "https://github.com/vobsjs/vobs#readme",
20
- "dependencies": {
21
- "@vobs/router": "0.3.0",
22
- "@vobs/reactivity": "0.3.0",
23
- "@vobs/runtime-dom": "0.3.0"
24
- },
25
3
  "files": [
26
- "dist"
4
+ "src",
5
+ "README.md",
6
+ "LICENSE"
27
7
  ],
8
+ "name": "@vobs/vobs",
9
+ "version": "1.1.0",
10
+ "type": "module",
11
+ "main": "src/index.ts",
12
+ "types": "src/index.ts",
28
13
  "exports": {
29
- ".": {
30
- "types": "./dist/index.d.ts",
31
- "import": "./dist/index.js"
32
- },
33
- "./reactivity": {
34
- "types": "./dist/reactivity.d.ts",
35
- "import": "./dist/reactivity.js"
36
- },
37
- "./router": {
38
- "types": "./dist/router.d.ts",
39
- "import": "./dist/router.js"
40
- },
41
- "./runtime-dom": {
42
- "types": "./dist/runtime-dom.d.ts",
43
- "import": "./dist/runtime-dom.js"
44
- },
45
- "./package.json": "./package.json"
14
+ ".": "./src/index.ts",
15
+ "./jsx-runtime": "./src/jsx-runtime.ts",
16
+ "./jsx-dev-runtime": "./src/jsx-dev-runtime.ts"
46
17
  },
47
- "types": "./dist/index.d.ts",
48
- "module": "./dist/index.js",
49
- "main": "./dist/index.js",
50
- "sideEffects": false,
51
- "engines": {
52
- "node": ">=22.12.0"
18
+ "dependencies": {
19
+ "@vobs/reactivity": "1.1.0",
20
+ "@vobs/dom": "1.1.0",
21
+ "@vobs/runtime": "1.1.0"
53
22
  }
54
23
  }
@@ -0,0 +1,245 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { effect } from '@vobs/reactivity'
3
+ import {
4
+ createDOMRenderer,
5
+ createComponent,
6
+ createInjectionKey,
7
+ createElement,
8
+ createText,
9
+ createVobs,
10
+ inject,
11
+ injectRequired,
12
+ provide,
13
+ setRenderer,
14
+ addEventListener,
15
+ type VobsLocatedError,
16
+ type VobsPlugin
17
+ } from './index'
18
+
19
+ describe('createVobs', () => {
20
+ beforeEach(() => {
21
+ setRenderer(createDOMRenderer())
22
+ })
23
+
24
+ it('没有 render 时抛错', () => {
25
+ expect(() => createVobs({} as any)).toThrow()
26
+ })
27
+
28
+ it('支持应用级错误观察器且不会覆盖原始异常', () => {
29
+ const errors: unknown[] = []
30
+ const app = createVobs({
31
+ render: () => { throw new Error('render failed') },
32
+ onError: error => errors.push(error)
33
+ })
34
+ expect(() => app.mount(document.createElement('div'))).toThrow('render failed')
35
+ expect(errors[0]).toBeInstanceOf(Error)
36
+ expect((errors[0] as Error).message).toBe('render failed')
37
+ })
38
+
39
+ it('挂载后 mounted 为 true', () => {
40
+ const app = createVobs({
41
+ render: () => {
42
+ return createText('hello')
43
+ }
44
+ })
45
+
46
+ const container = document.createElement('div')
47
+ app.mount(container)
48
+
49
+ expect(app.mounted).toBe(true)
50
+ expect(container.textContent).toBe('hello')
51
+
52
+ app.destroy()
53
+ expect(app.mounted).toBe(false)
54
+ expect(container.innerHTML).toBe('')
55
+ })
56
+
57
+ it('按依赖顺序安装插件,并逆序清理', () => {
58
+ const calls: string[] = []
59
+ const dependency = {
60
+ name: 'dependency',
61
+ install: () => {
62
+ calls.push('install-dependency')
63
+ return () => calls.push('cleanup-dependency')
64
+ }
65
+ }
66
+ const plugin: VobsPlugin = {
67
+ name: 'plugin',
68
+ requires: [dependency],
69
+ install: () => {
70
+ calls.push('install-plugin')
71
+ return () => calls.push('cleanup-plugin')
72
+ }
73
+ }
74
+
75
+ const app = createVobs({ render: () => createText(''), plugins: [plugin] })
76
+ app.destroy()
77
+
78
+ expect(calls).toEqual([
79
+ 'install-dependency',
80
+ 'install-plugin',
81
+ 'cleanup-plugin',
82
+ 'cleanup-dependency'
83
+ ])
84
+ })
85
+
86
+ it('注入项默认不可被静默覆盖', () => {
87
+ const key = createInjectionKey<string>('test.value')
88
+ const plugin: VobsPlugin = {
89
+ name: 'context',
90
+ install: context => {
91
+ context.provide(key, 'first')
92
+ expect(() => context.provide(key, 'second')).toThrow('已存在')
93
+ context.provide(key, 'second', { override: true })
94
+ expect(context.inject(key)).toBe('second')
95
+ }
96
+ }
97
+
98
+ createVobs({ render: () => createText(''), plugins: [plugin] }).destroy()
99
+ })
100
+
101
+ it('插件可访问 app、注册销毁回调并声明可选依赖', () => {
102
+ const calls: string[] = []
103
+ let contextApp: ReturnType<typeof createVobs> | undefined
104
+ const optional: VobsPlugin = {
105
+ name: 'optional',
106
+ install: () => { calls.push('optional-install') }
107
+ }
108
+ const plugin: VobsPlugin = {
109
+ name: 'lifecycle',
110
+ version: '0.1.0',
111
+ optional: [optional],
112
+ install(context) {
113
+ contextApp = context.app
114
+ context.onDestroy(() => { calls.push('on-destroy') })
115
+ return () => { calls.push('cleanup') }
116
+ }
117
+ }
118
+
119
+ const app = createVobs({ render: () => createText(''), plugins: [plugin] })
120
+ expect(contextApp).toBe(app)
121
+ expect(calls).toEqual([])
122
+
123
+ app.destroy()
124
+ expect(calls).toEqual(['cleanup', 'on-destroy'])
125
+ })
126
+
127
+ it('组件 provide/inject 按 Owner 层级查找,并可覆盖父级值', () => {
128
+ const key = createInjectionKey<string>('test.component-context')
129
+ let injected: string | undefined
130
+ let nestedInjected: string | undefined
131
+
132
+ function Child(): ReturnType<typeof createText> {
133
+ injected = inject(key)
134
+ provide(key, 'child')
135
+ nestedInjected = inject(key)
136
+ return createText('child')
137
+ }
138
+
139
+ function Parent(): ReturnType<typeof createText> {
140
+ provide(key, 'parent')
141
+ return createComponent(Child, {}) as ReturnType<typeof createText>
142
+ }
143
+
144
+ const app = createVobs({ render: () => createComponent(Parent, {}) })
145
+ app.mount(document.createElement('div'))
146
+
147
+ expect(injected).toBe('parent')
148
+ expect(nestedInjected).toBe('child')
149
+ app.destroy()
150
+ })
151
+
152
+ it('组件外调用 provide 会给出作用域错误', () => {
153
+ const key = createInjectionKey<string>('test.outside-context')
154
+ expect(() => provide(key, 'value')).toThrow('Owner 作用域')
155
+ })
156
+
157
+ it('inject 支持默认值和必需注入校验', () => {
158
+ const key = createInjectionKey<string>('test.optional')
159
+ expect(inject(key, 'fallback')).toBe('fallback')
160
+ expect(() => injectRequired(key, 'test.optional')).toThrow('必需注入项')
161
+ const app = createVobs({
162
+ render: () => {
163
+ provide(key, 'provided')
164
+ return createText(`${injectRequired(key)}:${inject(key, 'fallback')}`)
165
+ }
166
+ })
167
+ const container = document.createElement('div')
168
+ app.mount(container)
169
+ expect(container.textContent).toBe('provided:provided')
170
+ app.destroy()
171
+ })
172
+
173
+ it('插件可以观察启动错误,但不能吞掉原始错误', () => {
174
+ const failure = new Error('install failed')
175
+ let reported: unknown
176
+ const observer: VobsPlugin = {
177
+ name: 'observer',
178
+ install(context) {
179
+ context.onError(error => { reported = error })
180
+ }
181
+ }
182
+ const broken: VobsPlugin = {
183
+ name: 'broken',
184
+ install() {
185
+ throw failure
186
+ }
187
+ }
188
+
189
+ expect(() => createVobs({ render: () => createText(''), plugins: [observer, broken] }))
190
+ .toThrow(failure)
191
+ expect(reported).toBe(failure)
192
+ })
193
+
194
+ it('组件错误保留编译器提供的源码位置', () => {
195
+ const failure = new Error('render failed')
196
+ const app = createVobs({
197
+ render: () => createComponent(() => { throw failure }, {}, {
198
+ file: 'src/Page.tsx',
199
+ line: 12,
200
+ column: 7
201
+ })
202
+ })
203
+
204
+ expect(() => app.mount(document.createElement('div'))).toThrow(failure)
205
+ expect((failure as VobsLocatedError).vobsSource).toEqual({
206
+ file: 'src/Page.tsx',
207
+ line: 12,
208
+ column: 7
209
+ })
210
+ })
211
+
212
+ it('组件 effect 错误也保留源码位置并继续向应用传播', () => {
213
+ const failure = new Error('effect failed')
214
+ const app = createVobs({
215
+ render: () => createComponent(() => {
216
+ effect(() => { throw failure })
217
+ return createText('ready')
218
+ }, {}, { file: 'src/EffectPage.tsx', line: 8, column: 3 })
219
+ })
220
+
221
+ expect(() => app.mount(document.createElement('div'))).toThrow(failure)
222
+ expect((failure as VobsLocatedError).vobsSource).toEqual({
223
+ file: 'src/EffectPage.tsx',
224
+ line: 8,
225
+ column: 3
226
+ })
227
+ })
228
+
229
+ it('组件 Owner 销毁时自动移除事件监听器', () => {
230
+ let button!: HTMLButtonElement
231
+ let clicks = 0
232
+ const app = createVobs({
233
+ render: () => createComponent(() => {
234
+ button = createElement('button') as HTMLButtonElement
235
+ addEventListener(button, 'click', () => { clicks++ })
236
+ return button
237
+ }, {})
238
+ })
239
+ app.mount(document.createElement('div'))
240
+ button.dispatchEvent(new Event('click'))
241
+ app.destroy()
242
+ button.dispatchEvent(new Event('click'))
243
+ expect(clicks).toBe(1)
244
+ })
245
+ })
package/src/app.ts ADDED
@@ -0,0 +1,264 @@
1
+ import { createOwner, scheduler, setOwnerDebugName, type Owner } from '@vobs/reactivity'
2
+ import { createDOMRenderer } from '@vobs/dom'
3
+ import { insertBefore as insertRuntimeNode, setRenderer, type VobsFragment, type VobsRenderer } from '@vobs/runtime'
4
+ import { injectFromOwner, provideToOwner } from './context'
5
+
6
+ export type InjectionKey<T> = symbol & {
7
+ readonly __vobsType?: T
8
+ }
9
+
10
+ export interface ProvideOptions {
11
+ override?: boolean
12
+ }
13
+
14
+ export interface VobsPlugin {
15
+ name: string
16
+ version?: string
17
+ requires?: readonly VobsPlugin[]
18
+ optional?: readonly VobsPlugin[]
19
+ install?: (ctx: VobsContext) => void | (() => void)
20
+ }
21
+
22
+ export interface VobsContext {
23
+ readonly app: VobsApp
24
+ provide<T>(key: InjectionKey<T>, value: T, options?: ProvideOptions): void
25
+ inject<T>(key: InjectionKey<T>): T | undefined
26
+ injectRequired<T>(key: InjectionKey<T>, description?: string): T
27
+ onDestroy(cleanup: () => void): void
28
+ onError(handler: (error: unknown) => void): () => void
29
+ }
30
+
31
+ export interface VobsConfig<
32
+ NodeType = Node,
33
+ TextNode extends NodeType = NodeType,
34
+ ElementNode extends NodeType = NodeType,
35
+ CommentNode extends NodeType = NodeType
36
+ > {
37
+ render: () => NodeType | VobsFragment | Node | null | undefined
38
+ renderer?: VobsRenderer<NodeType, TextNode, ElementNode, CommentNode>
39
+ plugins?: VobsPlugin[]
40
+ /** Optional application-level error observer. Observers are isolated from the app. */
41
+ onError?: (error: unknown) => void
42
+ }
43
+
44
+ export interface VobsApp<NodeType = Node> {
45
+ readonly mounted: boolean
46
+ readonly destroyed: boolean
47
+ use(plugin: VobsPlugin): VobsApp<NodeType>
48
+ mount(target: string | NodeType): void
49
+ hydrate(target: string | NodeType): void
50
+ update(): void
51
+ destroy(): void
52
+ }
53
+
54
+ export function createInjectionKey<T>(description: string): InjectionKey<T> {
55
+ return Symbol(description) as InjectionKey<T>
56
+ }
57
+
58
+ export function createVobs(
59
+ config: VobsConfig<Node, Text, Element, Comment>
60
+ ): VobsApp<Node>
61
+ export function createVobs<
62
+ NodeType = Node,
63
+ TextNode extends NodeType = NodeType,
64
+ ElementNode extends NodeType = NodeType,
65
+ CommentNode extends NodeType = NodeType
66
+ >(
67
+ config: VobsConfig<NodeType, TextNode, ElementNode, CommentNode> & {
68
+ renderer: VobsRenderer<NodeType, TextNode, ElementNode, CommentNode>
69
+ }
70
+ ): VobsApp<NodeType>
71
+ export function createVobs(
72
+ config: VobsConfig<any, any, any, any>
73
+ ): VobsApp<any> {
74
+ if (!config.render) throw new Error('createVobs: render 不能为空')
75
+
76
+ const renderer = config.renderer ?? createDOMRenderer()
77
+ const rootOwner: Owner = createOwner()
78
+ setOwnerDebugName(rootOwner, 'App')
79
+ const cleanups: Array<() => void> = []
80
+ const errorHandlers = new Set<(error: unknown) => void>()
81
+ const installed = new Set<string>()
82
+ const installing = new Set<string>()
83
+ let mounted = false
84
+ let destroyed = false
85
+ let container: any = null
86
+ let app!: VobsApp<any>
87
+
88
+ if (config.onError) {
89
+ errorHandlers.add(config.onError)
90
+ cleanups.push(() => errorHandlers.delete(config.onError!))
91
+ }
92
+
93
+ const context: VobsContext = {
94
+ get app(): VobsApp<any> {
95
+ return app
96
+ },
97
+
98
+ provide<T>(key: InjectionKey<T>, value: T, options: ProvideOptions = {}): void {
99
+ provideToOwner(rootOwner, key, value, options)
100
+ },
101
+
102
+ inject<T>(key: InjectionKey<T>): T | undefined {
103
+ return injectFromOwner(rootOwner, key)
104
+ },
105
+
106
+ injectRequired<T>(key: InjectionKey<T>, description?: string): T {
107
+ const value = injectFromOwner(rootOwner, key)
108
+ if (value === undefined) {
109
+ throw new Error(`Vobs: 找不到必需注入项${description ? ` ${description}` : ''}`)
110
+ }
111
+ return value
112
+ },
113
+
114
+ onDestroy(cleanup: () => void): void {
115
+ cleanups.push(cleanup)
116
+ },
117
+
118
+ onError(handler: (error: unknown) => void): () => void {
119
+ errorHandlers.add(handler)
120
+ const remove = () => errorHandlers.delete(handler)
121
+ cleanups.push(remove)
122
+ return remove
123
+ }
124
+ }
125
+
126
+ function notifyError(error: unknown): void {
127
+ for (const handler of [...errorHandlers]) {
128
+ try {
129
+ handler(error)
130
+ } catch {
131
+ // 错误处理器不能覆盖触发它的原始错误。
132
+ }
133
+ }
134
+ }
135
+
136
+ function installPlugin(plugin: VobsPlugin): void {
137
+ if (installed.has(plugin.name)) return
138
+ if (installing.has(plugin.name)) {
139
+ throw new Error(`Vobs: 插件依赖存在循环:${plugin.name}`)
140
+ }
141
+
142
+ installing.add(plugin.name)
143
+ try {
144
+ for (const dependency of plugin.requires ?? []) installPlugin(dependency)
145
+ const cleanup = plugin.install?.(context)
146
+ if (cleanup) cleanups.push(cleanup)
147
+ installed.add(plugin.name)
148
+ } finally {
149
+ installing.delete(plugin.name)
150
+ }
151
+ }
152
+
153
+ function cleanup(clearContainer = true): unknown {
154
+ let firstError: unknown
155
+ for (let index = cleanups.length - 1; index >= 0; index--) {
156
+ try {
157
+ cleanups[index]()
158
+ } catch (error) {
159
+ firstError ??= error
160
+ }
161
+ }
162
+ cleanups.length = 0
163
+
164
+ try {
165
+ rootOwner.dispose()
166
+ } catch (error) {
167
+ firstError ??= error
168
+ }
169
+
170
+ if (container && clearContainer) {
171
+ try {
172
+ renderer.clear(container)
173
+ } catch (error) {
174
+ firstError ??= error
175
+ }
176
+ }
177
+ return firstError
178
+ }
179
+
180
+ function start(target: string | any, hydrating: boolean): void {
181
+ if (destroyed) throw new Error('Vobs: 已销毁的应用不能挂载')
182
+ if (mounted) return
183
+
184
+ container = typeof target === 'string' ? document.querySelector(target) : target
185
+ if (!container) throw new Error(`mount: 目标不存在: ${target}`)
186
+ if (hydrating && !renderer.beginHydration) {
187
+ throw new Error('Vobs: 当前渲染器不支持 Hydration')
188
+ }
189
+
190
+ setRenderer(renderer)
191
+ try {
192
+ if (hydrating) renderer.beginHydration?.()
193
+ const rootNode = rootOwner.run(config.render)
194
+ if (!hydrating) renderer.clear(container)
195
+ if (rootNode) insertRuntimeNode(container, rootNode as any, null)
196
+ if (hydrating) renderer.completeHydration?.()
197
+ mounted = true
198
+ } catch (error) {
199
+ notifyError(error)
200
+ const cleanupError = cleanup(!hydrating)
201
+ destroyed = true
202
+ throw cleanupError ?? error
203
+ }
204
+ }
205
+
206
+ app = {
207
+ get mounted(): boolean {
208
+ return mounted
209
+ },
210
+
211
+ get destroyed(): boolean {
212
+ return destroyed
213
+ },
214
+
215
+ use(plugin: VobsPlugin): VobsApp<any> {
216
+ if (destroyed) throw new Error('Vobs: 已销毁的应用不能安装插件')
217
+ installPlugin(plugin)
218
+ return app
219
+ },
220
+
221
+ mount(target: string | any): void {
222
+ start(target, false)
223
+ },
224
+
225
+ hydrate(target: string | any): void {
226
+ start(target, true)
227
+ },
228
+
229
+ update(): void {
230
+ if (destroyed) throw new Error('Vobs: 已销毁的应用不能更新')
231
+ try {
232
+ scheduler.flush()
233
+ } catch (error) {
234
+ notifyError(error)
235
+ throw error
236
+ }
237
+ },
238
+
239
+ destroy(): void {
240
+ if (destroyed) return
241
+ try {
242
+ const cleanupError = cleanup()
243
+ if (cleanupError) {
244
+ notifyError(cleanupError)
245
+ throw cleanupError
246
+ }
247
+ } finally {
248
+ mounted = false
249
+ destroyed = true
250
+ }
251
+ }
252
+ }
253
+
254
+ try {
255
+ for (const plugin of config.plugins ?? []) app.use(plugin)
256
+ } catch (error) {
257
+ notifyError(error)
258
+ cleanup()
259
+ destroyed = true
260
+ throw error
261
+ }
262
+
263
+ return app
264
+ }
package/src/context.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { getCurrentOwner, type Owner } from '@vobs/reactivity'
2
+ import type { InjectionKey, ProvideOptions } from './app'
3
+
4
+ const ownerProviders = new WeakMap<Owner, Map<InjectionKey<unknown>, unknown>>()
5
+
6
+ export function provide<T>(key: InjectionKey<T>, value: T, options: ProvideOptions = {}): void {
7
+ const owner = getCurrentOwner()
8
+ if (!owner) throw new Error('Vobs: provide 必须在组件或 Owner 作用域内调用')
9
+ provideToOwner(owner, key, value, options)
10
+ }
11
+
12
+ export function inject<T>(key: InjectionKey<T>): T | undefined
13
+ export function inject<T>(key: InjectionKey<T>, fallback: T): T
14
+ export function inject<T>(key: InjectionKey<T>, fallback?: T): T | undefined {
15
+ const owner = getCurrentOwner()
16
+ const value = owner ? injectFromOwner(owner, key) : undefined
17
+ return value === undefined ? fallback : value
18
+ }
19
+
20
+ export function injectRequired<T>(key: InjectionKey<T>, description?: string): T {
21
+ const value = inject(key)
22
+ if (value === undefined) {
23
+ throw new Error(`Vobs: 找不到必需注入项${description ? ` ${description}` : ''}`)
24
+ }
25
+ return value
26
+ }
27
+
28
+ export function provideToOwner<T>(
29
+ owner: Owner,
30
+ key: InjectionKey<T>,
31
+ value: T,
32
+ options: ProvideOptions = {}
33
+ ): void {
34
+ let providers = ownerProviders.get(owner)
35
+ if (!providers) {
36
+ providers = new Map<InjectionKey<unknown>, unknown>()
37
+ ownerProviders.set(owner, providers)
38
+ owner.onDispose(() => ownerProviders.delete(owner))
39
+ }
40
+ if (providers.has(key) && !options.override) {
41
+ throw new Error(`Vobs: 注入项 ${String(key)} 已存在;如需覆盖请传入 override: true`)
42
+ }
43
+ providers.set(key, value)
44
+ }
45
+
46
+ export function injectFromOwner<T>(owner: Owner, key: InjectionKey<T>): T | undefined {
47
+ let current: Owner | null = owner
48
+ while (current) {
49
+ const providers = ownerProviders.get(current)
50
+ if (providers?.has(key)) return providers.get(key) as T
51
+ current = current.parent
52
+ }
53
+ return undefined
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,89 @@
1
+ /// <reference path="./jsx.d.ts" />
2
+
3
+ export * from '@vobs/reactivity'
4
+ export { createDOMRenderer } from '@vobs/dom'
5
+ export type { VobsRenderer } from '@vobs/runtime'
6
+ export {
7
+ getRuntimeDebugContext,
8
+ getRuntimeDebugHooks,
9
+ pushRuntimeDebugContext,
10
+ runWithRuntimeDebugContext,
11
+ setRuntimeDebugHooks
12
+ } from '@vobs/runtime'
13
+ export type {
14
+ RuntimeDebugContext,
15
+ RuntimeDebugEnvironment,
16
+ RuntimeDebugHooks,
17
+ RuntimeDomMutation,
18
+ RuntimeDomMutationOperation,
19
+ RuntimeErrorEvent,
20
+ RuntimeHydrationMismatch
21
+ } from '@vobs/runtime'
22
+ export {
23
+ setRenderer,
24
+ getRenderer,
25
+ createText,
26
+ createElement,
27
+ createComment,
28
+ insertBefore,
29
+ removeChild,
30
+ setTextContent,
31
+ setProperty,
32
+ setAttribute,
33
+ setStaticProps,
34
+ spreadProps,
35
+ addEventListener,
36
+ removeEventListener,
37
+ clear,
38
+ createComponent,
39
+ createFragment,
40
+ isVobsFragment,
41
+ disposeNodeOwner
42
+ } from '@vobs/runtime'
43
+ export type { FragmentFactory, VobsFragment, VobsNode } from '@vobs/runtime'
44
+ export { bindText, bindAttribute, bindProperty } from '@vobs/runtime'
45
+ export type { ValueSource } from '@vobs/runtime'
46
+ export { ref, setRef } from '@vobs/runtime'
47
+ export type { Ref, RefTarget } from '@vobs/runtime'
48
+ export { insertDynamic, insertDynamicValue, insertList, normalizeDynamicChild } from '@vobs/runtime'
49
+ export type { DynamicChild, NodeFactory } from '@vobs/runtime'
50
+ export { createTemplate, cloneTemplate } from '@vobs/runtime'
51
+ export type { VobsLocatedError, VobsSourceLocation } from '@vobs/runtime'
52
+ export {
53
+ VobsError,
54
+ createVobsError,
55
+ formatVobsError,
56
+ isVobsError,
57
+ normalizeVobsError
58
+ } from '@vobs/runtime'
59
+ export type {
60
+ FormatVobsErrorOptions,
61
+ VobsErrorDefaults,
62
+ VobsErrorLayer,
63
+ VobsErrorLocation,
64
+ VobsErrorOptions,
65
+ VobsErrorSeverity
66
+ } from '@vobs/runtime'
67
+ export {
68
+ createHmrStateStore,
69
+ disposeHmrModule,
70
+ resolveComponent,
71
+ updateHmrModule
72
+ } from '@vobs/runtime'
73
+ export type { HmrComponent, HmrInstance, HmrStateStore } from '@vobs/runtime'
74
+ export { insertErrorBoundary } from '@vobs/runtime'
75
+ export { ErrorBoundary, insertBoundary } from '@vobs/runtime'
76
+ export type { BoundaryFallback, BoundaryOptions, BoundaryRetry, ErrorBoundaryFallback, ErrorBoundaryOptions, ErrorBoundaryProps } from '@vobs/runtime'
77
+ export { AsyncBoundary, insertAsyncBoundary, Profiler, insertProfiler } from '@vobs/runtime'
78
+ export type { AsyncBoundaryFallback, AsyncBoundaryOptions, AsyncBoundaryProps, AsyncBoundaryView, ProfilerOptions, ProfilerProps, ProfilerRenderInfo } from '@vobs/runtime'
79
+ export { createVobs, createInjectionKey } from './app'
80
+ export { Fragment, Vobs } from './jsx-dev-runtime'
81
+ export { provide, inject, injectRequired } from './context'
82
+ export type {
83
+ InjectionKey,
84
+ ProvideOptions,
85
+ VobsApp,
86
+ VobsConfig,
87
+ VobsPlugin,
88
+ VobsContext
89
+ } from './app'
@@ -0,0 +1,58 @@
1
+ import {
2
+ createComponent,
3
+ createElement,
4
+ createFragment,
5
+ insertBefore,
6
+ normalizeDynamicChild,
7
+ spreadProps,
8
+ type VobsNode
9
+ } from '@vobs/runtime'
10
+
11
+ export function jsxDEV(
12
+ tag: string | Function,
13
+ props: Record<string, unknown> | null,
14
+ _key?: unknown,
15
+ _isStaticChildren?: boolean,
16
+ _source?: unknown,
17
+ _self?: unknown
18
+ ): VobsNode {
19
+ if (typeof tag === 'function') {
20
+ return createComponent(tag as (props: Record<string, unknown>) => VobsNode, props ?? {})
21
+ }
22
+
23
+ const { children, ...attrs } = props ?? {}
24
+
25
+ const el = createElement(tag as string)
26
+
27
+ spreadProps(el, attrs)
28
+
29
+ appendChildren(el, children)
30
+
31
+ return el
32
+ }
33
+
34
+ function appendChildren(parent: Node, children: unknown, anchor: Node | null = null): void {
35
+ if (children === undefined || children === null) return
36
+
37
+ const childArray = Array.isArray(children) ? children : [children]
38
+
39
+ for (const child of childArray) {
40
+ const node = normalizeDynamicChild(child as Parameters<typeof normalizeDynamicChild>[0])
41
+ if (node) insertBefore(parent, node, anchor)
42
+ }
43
+ }
44
+
45
+ export function jsx(tag: string | Function, props: Record<string, unknown> | null): VobsNode {
46
+ return jsxDEV(tag, props)
47
+ }
48
+
49
+ export function jsxs(tag: string | Function, props: Record<string, unknown> | null): VobsNode {
50
+ return jsxDEV(tag, props)
51
+ }
52
+
53
+ export function Fragment(props: { children?: unknown }): VobsNode {
54
+ return createFragment((parent, anchor) => appendChildren(parent, props?.children, anchor))
55
+ }
56
+
57
+ /** Namespace for explicit built-in JSX primitives such as <Vobs.Fragment>. */
58
+ export const Vobs = { Fragment }
@@ -0,0 +1 @@
1
+ export { Fragment, jsx, jsxs } from './jsx-dev-runtime'
package/src/jsx.d.ts ADDED
@@ -0,0 +1,97 @@
1
+ import type { VobsNode } from './fragment'
2
+ import type { Ref, RefTarget } from '@vobs/runtime'
3
+
4
+ // JSX 类型声明
5
+
6
+ type VobsEventHandler<T extends Event = Event> = (event: T) => void
7
+
8
+ interface VobsHTMLAttributes {
9
+ ref?: RefTarget<any>
10
+ accessKey?: string
11
+ autofocus?: boolean
12
+ class?: string
13
+ className?: string
14
+ cols?: number
15
+ disabled?: boolean
16
+ height?: number | string
17
+ hidden?: boolean
18
+ id?: string
19
+ max?: number | string
20
+ maxLength?: number
21
+ min?: number | string
22
+ minLength?: number
23
+ multiple?: boolean
24
+ name?: string
25
+ placeholder?: string
26
+ readOnly?: boolean
27
+ required?: boolean
28
+ rows?: number
29
+ selected?: boolean
30
+ size?: number
31
+ src?: string
32
+ step?: number | string
33
+ style?: string | Readonly<Record<string, string | number | boolean | null | undefined>>
34
+ tabIndex?: number
35
+ title?: string
36
+ type?: string
37
+ value?: string | number | readonly string[]
38
+ key?: string | number
39
+ width?: number | string
40
+ children?: unknown
41
+ onClick?: VobsEventHandler<MouseEvent>
42
+ onDblClick?: VobsEventHandler<MouseEvent>
43
+ onFocus?: VobsEventHandler<FocusEvent>
44
+ onBlur?: VobsEventHandler<FocusEvent>
45
+ onInput?: VobsEventHandler<InputEvent>
46
+ onChange?: VobsEventHandler<Event>
47
+ onKeyDown?: VobsEventHandler<KeyboardEvent>
48
+ onKeyUp?: VobsEventHandler<KeyboardEvent>
49
+ onSubmit?: VobsEventHandler<SubmitEvent>
50
+ [name: `data-${string}`]: string | number | boolean | undefined
51
+ [name: `aria-${string}`]: string | number | boolean | undefined
52
+ }
53
+
54
+ declare global {
55
+ namespace JSX {
56
+ type Element = VobsNode
57
+ interface ElementChildrenAttribute { children: {} }
58
+ interface IntrinsicElements {
59
+ a: VobsHTMLAttributes
60
+ article: VobsHTMLAttributes
61
+ aside: VobsHTMLAttributes
62
+ button: VobsHTMLAttributes
63
+ code: VobsHTMLAttributes
64
+ div: VobsHTMLAttributes
65
+ footer: VobsHTMLAttributes
66
+ form: VobsHTMLAttributes
67
+ h1: VobsHTMLAttributes
68
+ h2: VobsHTMLAttributes
69
+ h3: VobsHTMLAttributes
70
+ header: VobsHTMLAttributes
71
+ img: VobsHTMLAttributes
72
+ input: VobsHTMLAttributes
73
+ label: VobsHTMLAttributes
74
+ li: VobsHTMLAttributes
75
+ main: VobsHTMLAttributes
76
+ nav: VobsHTMLAttributes
77
+ ol: VobsHTMLAttributes
78
+ option: VobsHTMLAttributes
79
+ pre: VobsHTMLAttributes
80
+ p: VobsHTMLAttributes
81
+ section: VobsHTMLAttributes
82
+ select: VobsHTMLAttributes
83
+ span: VobsHTMLAttributes
84
+ table: VobsHTMLAttributes
85
+ tbody: VobsHTMLAttributes
86
+ td: VobsHTMLAttributes
87
+ textarea: VobsHTMLAttributes
88
+ tfoot: VobsHTMLAttributes
89
+ th: VobsHTMLAttributes
90
+ thead: VobsHTMLAttributes
91
+ tr: VobsHTMLAttributes
92
+ ul: VobsHTMLAttributes
93
+ }
94
+ }
95
+ }
96
+
97
+ export {}
@@ -0,0 +1,26 @@
1
+ import { describe, expectTypeOf, it } from 'vitest'
2
+ import { createInjectionKey, inject, provide } from './index'
3
+ import { state, type Signal } from '@vobs/reactivity'
4
+
5
+ interface AuthContext {
6
+ readonly loggedIn: Signal<boolean>
7
+ }
8
+
9
+ describe('Vobs public types', () => {
10
+ it('state 保留泛型并由初始值推导', () => {
11
+ const count = state(0)
12
+ expectTypeOf(count).toEqualTypeOf<Signal<number>>()
13
+ })
14
+
15
+ it('InjectionKey 将 inject 和 provide 约束到同一类型', () => {
16
+ const key = createInjectionKey<AuthContext>('test.auth')
17
+ const check = (): void => {
18
+ const value: AuthContext | undefined = inject(key)
19
+ if (value) value.loggedIn.value = true
20
+ provide(key, { loggedIn: state(false) })
21
+ // @ts-expect-error The value must match the InjectionKey payload.
22
+ provide(key, state(false))
23
+ }
24
+ expectTypeOf(check).toBeFunction()
25
+ })
26
+ })
package/dist/index.d.ts DELETED
@@ -1,7 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vobs
4
- */
5
- export * from '@vobs/reactivity';
6
- export * from '@vobs/runtime-dom';
7
- export { RouterCapability, collectRouteNavigation, createRouteLoaderCache, createRouteNavigationScope, createRouteResolver, createRouter, loadRouteComponent, resolveActiveRouteNavigationId, resolveRoute, resolveRouteGuard, resolveRouteInitialState, } from '@vobs/router';
package/dist/index.js DELETED
@@ -1,7 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vobs
4
- */
5
- export * from '@vobs/reactivity';
6
- export * from '@vobs/runtime-dom';
7
- export { RouterCapability, collectRouteNavigation, createRouteLoaderCache, createRouteNavigationScope, createRouteResolver, createRouter, loadRouteComponent, resolveActiveRouteNavigationId, resolveRoute, resolveRouteGuard, resolveRouteInitialState, } from '@vobs/router';
@@ -1,5 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vobs
4
- */
5
- export * from '@vobs/reactivity';
@@ -1,5 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vobs
4
- */
5
- export * from '@vobs/reactivity';
package/dist/router.d.ts DELETED
@@ -1,5 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vobs
4
- */
5
- export * from '@vobs/router';
package/dist/router.js DELETED
@@ -1,5 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vobs
4
- */
5
- export * from '@vobs/router';
@@ -1,5 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vobs
4
- */
5
- export * from '@vobs/runtime-dom';
@@ -1,5 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vobs
4
- */
5
- export * from '@vobs/runtime-dom';