@vobs/vite-plugin 0.1.0 → 1.0.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,43 @@
1
+ # @vobs/vite-plugin
2
+
3
+ Vite plugin that compiles Vobs TSX, turns imported HTML files into components, and wires up HMR and i18n key extraction.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @vobs/vite-plugin
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ // vite.config.ts
15
+ import { defineConfig } from 'vite'
16
+ import { vobsPlugin } from '@vobs/vite-plugin'
17
+
18
+ const collectedKeys = new Set<string>()
19
+
20
+ export default defineConfig({
21
+ plugins: [
22
+ vobsPlugin({
23
+ html: true,
24
+ extractI18n: key => collectedKeys.add(key)
25
+ })
26
+ ]
27
+ })
28
+ ```
29
+
30
+ ## API
31
+
32
+ | Signature | Description |
33
+ | --- | --- |
34
+ | `vobsPlugin(options?)` | Create the plugin; compiles matched modules with `@vobs/compiler`, returns source maps, and throws compiler errors as `VobsError` with code, location and code frame. |
35
+ | `options.include` | `RegExp` selecting modules to compile; defaults to `/\.tsx(?:$|\?)/`. |
36
+ | `options.compiler` | Extra `CompileOptions` forwarded to the compiler (e.g. custom `plugins`). |
37
+ | `options.hmr` | Inject `import.meta.hot` accept/dispose handlers; defaults to on in dev and off for production builds. |
38
+ | `options.extractI18n` | Callback `(key, filename)` receiving each static translation key collected from `t('...')` and `i18n.t('...')` calls. |
39
+ | `options.html` | Compile imported `.html`/`.htm` modules into Vobs components (scripts, inline event attributes and dangerous URLs are rejected); `false` disables, or pass `{ extensions }` for other file types. |
40
+
41
+ ## Types
42
+
43
+ VobsVitePluginOptions
package/package.json CHANGED
@@ -1,54 +1,21 @@
1
1
  {
2
- "name": "@vobs/vite-plugin",
3
- "version": "0.1.0",
4
- "description": "Vite integration, HTML transforms and file routes for vobs.",
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": "plugins/vite"
15
- },
16
- "bugs": {
17
- "url": "https://github.com/vobsjs/vobs/issues"
18
- },
19
- "homepage": "https://github.com/vobsjs/vobs#readme",
20
- "dependencies": {
21
- "@vobs/compiler-dom": "0.1.0",
22
- "@vobs/runtime-dom": "0.1.0",
23
- "@vobs/runtime-core": "0.1.0",
24
- "@vobs/icons": "0.1.0",
25
- "@vobs/router": "0.1.0"
26
- },
27
- "peerDependencies": {
28
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
29
- },
30
- "devDependencies": {
31
- "vite": "^7.0.0"
32
- },
33
3
  "files": [
34
- "dist"
4
+ "src",
5
+ "README.md",
6
+ "LICENSE"
35
7
  ],
8
+ "name": "@vobs/vite-plugin",
9
+ "version": "1.0.0",
10
+ "type": "module",
11
+ "main": "src/index.ts",
12
+ "types": "src/index.ts",
36
13
  "exports": {
37
- ".": {
38
- "types": "./dist/index.d.ts",
39
- "import": "./dist/index.js"
40
- },
41
- "./client": {
42
- "types": "./dist/client.d.ts",
43
- "import": "./dist/client.js"
44
- },
45
- "./package.json": "./package.json"
14
+ ".": "./src/index.ts"
46
15
  },
47
- "types": "./dist/index.d.ts",
48
- "module": "./dist/index.js",
49
- "main": "./dist/index.js",
50
- "sideEffects": false,
51
- "engines": {
52
- "node": ">=20.19.0"
16
+ "dependencies": {
17
+ "parse5": "^8.0.1",
18
+ "@vobs/runtime": "1.0.0",
19
+ "@vobs/compiler": "1.0.0"
53
20
  }
54
21
  }
@@ -0,0 +1,137 @@
1
+ import { parseFragment, type DefaultTreeAdapterMap, type DefaultTreeAdapterTypes } from 'parse5'
2
+
3
+ type HtmlNode = DefaultTreeAdapterTypes.Node
4
+ type HtmlDocumentFragment = DefaultTreeAdapterTypes.DocumentFragment
5
+ type HtmlElement = DefaultTreeAdapterTypes.Element
6
+
7
+ const blockedTags = new Set(['script', 'iframe', 'object', 'embed', 'base', 'meta', 'link', 'style'])
8
+ const blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu
9
+ const urlAttributes = new Set(['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'])
10
+ const safeProtocols = new Set(['http:', 'https:', 'mailto:', 'tel:'])
11
+ const directivePattern = /^data-vobs-(text|slot|on|bind|prop)(?:-(.+))?$/iu
12
+ const safePropertyNames = new Set([
13
+ 'value', 'checked', 'selected', 'disabled', 'multiple', 'readonly', 'required',
14
+ 'autofocus', 'hidden', 'tabindex'
15
+ ])
16
+
17
+ export interface HtmlComponentOptions {
18
+ readonly filename?: string
19
+ }
20
+
21
+ /** Converts a trusted static HTML fragment to Vobs runtime node creation code. */
22
+ export function compileHtmlComponent(source: string, options: HtmlComponentOptions = {}): string {
23
+ const fragment = parseFragment(source)
24
+ const body = compileChildren(fragment, options.filename ?? 'component.html')
25
+ const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map(code => `insertBefore(parent, ${code}, anchor);`).join('')}})`
26
+
27
+ return `import { addEventListener, bindAttribute, bindProperty, bindText, createElement, createFragment, createText, insertBefore, insertDynamic, setAttribute } from '@vobs/vobs';\nfunction resolveHtmlSlot(value) { const resolved = typeof value === 'function' ? value() : value; if (resolved === undefined || resolved === null || resolved === false) return null; if (typeof resolved === 'string' || typeof resolved === 'number') return createText(String(resolved)); if (Array.isArray(resolved)) return createFragment((parent, anchor) => { for (const item of resolved) { const child = resolveHtmlSlot(item); if (child) insertBefore(parent, child, anchor); } }); return resolved; }\nfunction sanitizeHtmlAttribute(name, value) { const stringValue = String(value ?? ''); if (!['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'].includes(name.toLowerCase())) return stringValue; const normalized = stringValue.trim().toLowerCase(); if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return stringValue; try { const protocol = new URL(normalized, 'https://vobs.invalid/').protocol; return ['http:', 'https:', 'mailto:', 'tel:'].includes(protocol) ? stringValue : ''; } catch { return ''; } }\nexport default function HtmlComponent(props = {}) { return ${result ?? "createFragment(() => {})"}; }`
28
+ }
29
+
30
+ function compileChildren(parent: HtmlDocumentFragment | HtmlElement, filename: string): string[] {
31
+ return parent.childNodes.flatMap(node => compileNode(node, filename))
32
+ }
33
+
34
+ function compileNode(node: HtmlNode, filename: string): string[] {
35
+ if (node.nodeName === '#text') {
36
+ const value = (node as DefaultTreeAdapterMap['textNode']).value
37
+ return value ? [`createText(${JSON.stringify(value)})`] : []
38
+ }
39
+
40
+ if (node.nodeName === '#comment') return []
41
+ if (node.nodeName !== '#document-fragment' && !isElement(node)) return []
42
+
43
+ if (isElement(node)) {
44
+ const tag = node.tagName.toLowerCase()
45
+ if (blockedTags.has(tag)) throw new Error(`Vobs HTML component: 禁止使用 <${tag}> (${filename})`)
46
+
47
+ const children = compileChildren(node, filename)
48
+ const statements = [`const element = createElement(${JSON.stringify(tag)})`]
49
+ const dynamicText = [] as string[]
50
+ for (const attribute of node.attrs) {
51
+ const name = attribute.name.toLowerCase()
52
+ const value = attribute.value
53
+ const directive = parseDirective(name, value, filename)
54
+ if (directive) {
55
+ if (directive.kind === 'text') {
56
+ dynamicText.push(directive.prop)
57
+ } else if (directive.kind === 'slot') {
58
+ statements.push(`insertDynamic(element, null, () => resolveHtmlSlot(props[${JSON.stringify(directive.prop)}]))`)
59
+ } else if (directive.kind === 'event') {
60
+ statements.push(`addEventListener(element, ${JSON.stringify(directive.name)}, (event) => { const handler = props[${JSON.stringify(directive.prop)}]; if (typeof handler === 'function') handler(event); })`)
61
+ } else if (directive.kind === 'attribute') {
62
+ statements.push(`bindAttribute(element, ${JSON.stringify(directive.name)}, () => sanitizeHtmlAttribute(${JSON.stringify(directive.name)}, props[${JSON.stringify(directive.prop)}] ?? ''))`)
63
+ } else if (directive.kind === 'property') {
64
+ statements.push(`bindProperty(element, ${JSON.stringify(directive.name)}, () => props[${JSON.stringify(directive.prop)}])`)
65
+ }
66
+ continue
67
+ }
68
+ if (blockedAttributes.test(name)) throw new Error(`Vobs HTML component: 禁止使用危险属性 ${attribute.name} (${filename})`)
69
+ if (urlAttributes.has(name) && !isSafeUrl(value)) {
70
+ throw new Error(`Vobs HTML component: 禁止使用危险 URL 属性 ${attribute.name} (${filename})`)
71
+ }
72
+ statements.push(`setAttribute(element, ${JSON.stringify(attribute.name)}, ${JSON.stringify(value)})`)
73
+ }
74
+ if (dynamicText.length > 1) throw new Error(`Vobs HTML component: 一个元素只能使用一个 data-vobs-text 指令 (${filename})`)
75
+ if (dynamicText.length === 1) {
76
+ const text = 'createText("")'
77
+ statements.push(`const text = ${text}`)
78
+ statements.push('insertBefore(element, text, null)')
79
+ statements.push(`bindText(text, () => props[${JSON.stringify(dynamicText[0])}] ?? '')`)
80
+ } else {
81
+ for (const child of children) statements.push(`insertBefore(element, ${child}, null)`)
82
+ }
83
+ statements.push('return element')
84
+ return [`(() => {${statements.join(';')};})()`]
85
+ }
86
+
87
+ return compileChildren(node as HtmlDocumentFragment, filename)
88
+ }
89
+
90
+ function isElement(node: HtmlNode): node is HtmlElement {
91
+ return !node.nodeName.startsWith('#')
92
+ }
93
+
94
+ function isSafeUrl(value: string): boolean {
95
+ const normalized = value.trim().toLowerCase()
96
+ if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return true
97
+ try {
98
+ return safeProtocols.has(new URL(normalized, 'https://vobs.invalid/').protocol)
99
+ } catch {
100
+ return false
101
+ }
102
+ }
103
+
104
+ type HtmlDirective =
105
+ | { readonly kind: 'text' | 'slot'; readonly prop: string }
106
+ | { readonly kind: 'event' | 'attribute' | 'property'; readonly name: string; readonly prop: string }
107
+
108
+ function parseDirective(name: string, value: string, filename: string): HtmlDirective | null {
109
+ const match = directivePattern.exec(name)
110
+ if (!match) return null
111
+ const kind = match[1].toLowerCase()
112
+ const namePart = match[2]
113
+ const prop = value.trim()
114
+ if (!prop || !/^[A-Za-z_$][\w$]*$/u.test(prop)) {
115
+ throw new Error(`Vobs HTML component: ${name} 必须引用有效的 props 名称 (${filename})`)
116
+ }
117
+ if (kind === 'text' || kind === 'slot') {
118
+ if (namePart) throw new Error(`Vobs HTML component: ${name} 不接受额外名称 (${filename})`)
119
+ return { kind, prop }
120
+ }
121
+ if (!namePart || !/^[a-z][a-z0-9:-]*$/iu.test(namePart)) {
122
+ throw new Error(`Vobs HTML component: ${name} 必须包含有效名称 (${filename})`)
123
+ }
124
+ if (kind === 'on') return { kind: 'event', name: namePart.toLowerCase(), prop }
125
+ if (kind === 'bind') {
126
+ if (namePart.toLowerCase() === 'style') throw new Error(`Vobs HTML component: 不允许动态绑定 style (${filename})`)
127
+ return { kind: 'attribute', name: namePart, prop }
128
+ }
129
+ if (!safePropertyNames.has(namePart.toLowerCase())) {
130
+ throw new Error(`Vobs HTML component: 不允许动态绑定 property ${namePart} (${filename})`)
131
+ }
132
+ return { kind: 'property', name: normalizePropertyName(namePart), prop }
133
+ }
134
+
135
+ function normalizePropertyName(name: string): string {
136
+ return name.toLowerCase() === 'readonly' ? 'readOnly' : name.toLowerCase() === 'tabindex' ? 'tabIndex' : name
137
+ }
@@ -0,0 +1,164 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import ts from 'typescript'
3
+ import { compileHtmlComponent } from './html-component'
4
+ import { vobsPlugin } from './index'
5
+
6
+ describe('vobsPlugin', () => {
7
+ it('将编译器插件配置透传给 TSX 转换', () => {
8
+ const plugin = vobsPlugin({
9
+ compiler: {
10
+ plugins: [{
11
+ name: 'replace-message',
12
+ transform: {
13
+ node(node, context) {
14
+ if (ts.isIdentifier(node) && node.text === 'message') {
15
+ return context.factory.createIdentifier('title')
16
+ }
17
+ return node
18
+ }
19
+ }
20
+ }]
21
+ }
22
+ })
23
+ const transform = plugin.transform
24
+ if (typeof transform !== 'function') throw new Error('Vobs Vite plugin: 缺少 transform 钩子')
25
+
26
+ const result = transform.call({} as ThisParameterType<typeof transform>,
27
+ `export function App() { return <span>{message}</span> }`,
28
+ 'src/App.tsx'
29
+ )
30
+
31
+ expect(result).toMatchObject({ code: expect.stringContaining('() => title') })
32
+ })
33
+
34
+ it('返回源码映射并注入可关闭的 HMR 接受器', () => {
35
+ const plugin = vobsPlugin()
36
+ const transform = plugin.transform
37
+ if (typeof transform !== 'function') throw new Error('Vobs Vite plugin: 缺少 transform 钩子')
38
+
39
+ const result = transform.call({} as ThisParameterType<typeof transform>,
40
+ `export function App() { return <span>hello</span> }`,
41
+ 'src/App.tsx'
42
+ ) as { code: string; map: { sources: string[] } }
43
+
44
+ expect(result.map.sources).toEqual(['src/App.tsx'])
45
+ expect(result.code).toContain('import.meta.hot.accept')
46
+ expect(result.code).toContain('updateHmrModule')
47
+ })
48
+
49
+ it('可以关闭 HMR 注入', () => {
50
+ const plugin = vobsPlugin({ hmr: false })
51
+ const transform = plugin.transform
52
+ if (typeof transform !== 'function') throw new Error('Vobs Vite plugin: 缺少 transform 钩子')
53
+
54
+ const result = transform.call({} as ThisParameterType<typeof transform>,
55
+ `export function App() { return <span>hello</span> }`,
56
+ 'src/App.tsx'
57
+ ) as { code: string }
58
+
59
+ expect(result.code).not.toContain('import.meta.hot.accept')
60
+ })
61
+
62
+ it('生产构建默认不注入 HMR 代码', () => {
63
+ const plugin = vobsPlugin()
64
+ const configResolved = plugin.configResolved as ((config: unknown) => void) | undefined
65
+ configResolved?.({ command: 'build' })
66
+ const transform = plugin.transform
67
+ if (typeof transform !== 'function') throw new Error('Vobs Vite plugin: 缺少 transform 钩子')
68
+ const result = transform.call({} as ThisParameterType<typeof transform>,
69
+ `export const App = () => <main>hello</main>`, 'src/App.tsx') as { code: string }
70
+ expect(result.code).not.toContain('import.meta.hot.accept')
71
+ })
72
+
73
+ it('转换不含 export function 的合法 TSX 模块', () => {
74
+ const plugin = vobsPlugin({ hmr: false })
75
+ const transform = plugin.transform
76
+ if (typeof transform !== 'function') throw new Error('Vobs Vite plugin: 缺少 transform 钩子')
77
+
78
+ const result = transform.call({} as ThisParameterType<typeof transform>,
79
+ `export const App = () => <main>hello</main>`,
80
+ 'src/App.tsx'
81
+ ) as { code: string }
82
+
83
+ expect(result.code).toContain('createElement("main")')
84
+ })
85
+
86
+ it('转换默认箭头导出和带 query 的 TSX 模块', () => {
87
+ const plugin = vobsPlugin({ hmr: false })
88
+ const transform = plugin.transform
89
+ if (typeof transform !== 'function') throw new Error('Vobs Vite plugin: 缺少 transform 钩子')
90
+
91
+ const result = transform.call({} as ThisParameterType<typeof transform>,
92
+ `export default () => <main>hello</main>`,
93
+ 'src/App.tsx?direct'
94
+ ) as { code: string }
95
+
96
+ expect(result.code).toContain('createElement("main")')
97
+ })
98
+
99
+ it('通过 extractI18n 回调收集静态翻译 key', () => {
100
+ const keys: string[] = []
101
+ const plugin = vobsPlugin({ hmr: false, extractI18n: key => keys.push(key) })
102
+ const transform = plugin.transform
103
+ if (typeof transform !== 'function') throw new Error('Vobs Vite Plugin: 缺少 transform 钩子')
104
+
105
+ transform.call({} as ThisParameterType<typeof transform>,
106
+ `export const Page = () => <main>{t('page.title')} {i18n.t('common.ok')}</main>`,
107
+ 'src/Page.tsx'
108
+ )
109
+
110
+ expect(keys).toEqual(['page.title', 'common.ok'])
111
+ })
112
+
113
+ it('将 HTML 模块编译为无 innerHTML 的 Vobs 组件', () => {
114
+ const result = compileHtmlComponent('<article class="copy"><h1>Hello</h1><p>Safe</p></article>', {
115
+ filename: 'src/content.html'
116
+ })
117
+
118
+ expect(result).toContain("createElement(\"article\")")
119
+ expect(result).toContain("setAttribute(element, \"class\", \"copy\")")
120
+ expect(result).toContain('createText(\"Hello\")')
121
+ expect(result).not.toContain('innerHTML')
122
+ })
123
+
124
+ it('将受限的 Vobs 指令连接到响应式 props 和事件', () => {
125
+ const result = compileHtmlComponent(`
126
+ <button data-vobs-on-click="onSave" data-vobs-prop-disabled="disabled">
127
+ <span data-vobs-text="label"></span>
128
+ <span data-vobs-slot="actions"></span>
129
+ </button>
130
+ `)
131
+
132
+ expect(result).toContain('addEventListener(element, "click"')
133
+ expect(result).toContain('bindProperty(element, "disabled"')
134
+ expect(result).toContain('bindText(text')
135
+ expect(result).toContain('insertDynamic(element, null')
136
+ expect(result).toContain('props["onSave"]')
137
+ expect(result).toContain('bindProperty(element, "disabled"')
138
+ })
139
+
140
+ it('拒绝脚本、事件属性和危险 URL', () => {
141
+ expect(() => compileHtmlComponent('<script>alert(1)</script>')).toThrow('禁止使用 <script>')
142
+ expect(() => compileHtmlComponent('<button onclick="alert(1)">run</button>')).toThrow('禁止使用危险属性')
143
+ expect(() => compileHtmlComponent('<a href="javascript:alert(1)">run</a>')).toThrow('禁止使用危险 URL')
144
+ expect(() => compileHtmlComponent('<button data-vobs-on-click="on-click">run</button>')).toThrow('有效的 props 名称')
145
+ expect(() => compileHtmlComponent('<div data-vobs-bind-style="styleValue"></div>')).toThrow('不允许动态绑定 style')
146
+ expect(() => compileHtmlComponent('<div data-vobs-prop-innerHTML="content"></div>')).toThrow('不允许动态绑定 property')
147
+ })
148
+
149
+ it('忽略注释和文档类型声明', () => {
150
+ const result = compileHtmlComponent('<!doctype html><!-- note --><main>content</main>')
151
+ expect(result).toContain('createElement(\"main\")')
152
+ expect(result).not.toContain('tagName')
153
+ })
154
+
155
+ it('只将被导入的 HTML 文件交给组件加载器', async () => {
156
+ const plugin = vobsPlugin({ hmr: false })
157
+ const resolveId = plugin.resolveId
158
+ const load = plugin.load
159
+ if (typeof resolveId !== 'function' || typeof load !== 'function') throw new Error('Vobs Vite Plugin: 缺少 HTML 模块钩子')
160
+
161
+ expect(resolveId.call({} as ThisParameterType<typeof resolveId>, './content.html', 'src/Page.tsx', { attributes: {}, isEntry: false })).toBeTruthy()
162
+ expect(resolveId.call({} as ThisParameterType<typeof resolveId>, './index.html', undefined, { attributes: {}, isEntry: false })).toBeNull()
163
+ })
164
+ })
package/src/index.ts ADDED
@@ -0,0 +1,106 @@
1
+ // Vite 插件:集成 Vobs 编译器
2
+
3
+ import { readFile } from 'node:fs/promises'
4
+ import path from 'node:path'
5
+ import type { Plugin } from 'vite'
6
+ import { compileWithSourceMap, createI18nExtractor, type CompileOptions } from '@vobs/compiler'
7
+ import { VobsError } from '@vobs/runtime/error'
8
+ import { compileHtmlComponent } from './html-component.ts'
9
+
10
+ export interface VobsVitePluginOptions {
11
+ include?: RegExp
12
+ compiler?: CompileOptions
13
+ hmr?: boolean
14
+ extractI18n?: (key: string, filename: string) => void
15
+ html?: boolean | { readonly extensions?: readonly string[] }
16
+ }
17
+
18
+ export function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {
19
+ const include = options.include ?? /\.tsx(?:$|\?)/
20
+ const htmlModules = new Set<string>()
21
+ let productionBuild = false
22
+
23
+ return {
24
+ name: 'vobs',
25
+
26
+ enforce: 'pre',
27
+
28
+ configResolved(config) {
29
+ productionBuild = config.command === 'build'
30
+ },
31
+
32
+ resolveId(source: string, importer: string | undefined) {
33
+ if (!importer || !isHtmlComponent(source, options.html) || !isRelativeModule(source)) return null
34
+ const cleanImporter = importer.split(/[?#]/u, 1)[0]
35
+ const cleanSource = source.split(/[?#]/u, 1)[0]
36
+ const resolved = path.resolve(path.dirname(cleanImporter), cleanSource)
37
+ htmlModules.add(resolved)
38
+ return resolved
39
+ },
40
+
41
+ async load(id: string) {
42
+ if (!htmlModules.has(id)) return null
43
+ return compileHtmlComponent(await readFile(id, 'utf8'), { filename: id })
44
+ },
45
+
46
+ transform(code: string, id: string) {
47
+ include.lastIndex = 0
48
+ if (!include.test(id)) return null
49
+
50
+ const extractor = options.extractI18n
51
+ ? createI18nExtractor({ onKey: options.extractI18n })
52
+ : undefined
53
+ const result = compileWithSourceMap(code, {
54
+ ...options.compiler,
55
+ filename: id,
56
+ plugins: [
57
+ ...(options.compiler?.plugins ?? []),
58
+ ...(extractor ? [extractor.plugin] : [])
59
+ ]
60
+ })
61
+ const diagnostic = result.diagnostics.find(item => item.severity === 'error')
62
+ if (diagnostic) {
63
+ throw new VobsError({
64
+ code: diagnostic.code,
65
+ layer: 'compiler',
66
+ message: diagnostic.message,
67
+ location: diagnostic.location,
68
+ codeFrame: diagnostic.codeFrame,
69
+ fix: diagnostic.fix
70
+ })
71
+ }
72
+ const hmr = options.hmr ?? !productionBuild
73
+ const hmrCode = hmr ? createHmrCode(id) : ''
74
+ return {
75
+ code: `${result.code}${hmrCode}`,
76
+ map: result.map
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ function isRelativeModule(source: string): boolean {
83
+ return source.startsWith('./') || source.startsWith('../')
84
+ }
85
+
86
+ function isHtmlComponent(id: string, option: VobsVitePluginOptions['html']): boolean {
87
+ if (option === false) return false
88
+ const extensions = typeof option === 'object' && option.extensions?.length ? option.extensions : ['.html', '.htm']
89
+ const cleanId = id.split(/[?#]/u, 1)[0]
90
+ return extensions.some(extension => cleanId.endsWith(extension))
91
+ }
92
+
93
+
94
+ function createHmrCode(moduleId: string): string {
95
+ const encodedId = JSON.stringify(moduleId)
96
+ return `
97
+ import { disposeHmrModule, updateHmrModule } from '@vobs/vobs'
98
+
99
+ if (import.meta.hot) {
100
+ import.meta.hot.accept((module) => {
101
+ if (module) updateHmrModule(${encodedId}, module)
102
+ })
103
+ import.meta.hot.dispose(() => disposeHmrModule(${encodedId}))
104
+ }
105
+ `
106
+ }
package/dist/client.d.ts DELETED
@@ -1,48 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vite-plugin
4
- */
5
- declare module '*.html' {
6
- const template: import('@vobs/runtime-dom').CompiledTemplate<object>;
7
- export default template;
8
- }
9
- declare module '*?vobs-image' {
10
- export interface VobsImageMetadata {
11
- readonly src: string;
12
- readonly width: number;
13
- readonly height: number;
14
- readonly format: 'jpeg' | 'png' | 'svg' | 'webp';
15
- }
16
- const metadata: VobsImageMetadata;
17
- export default metadata;
18
- }
19
- declare module '*.svg?raw' {
20
- const source: string;
21
- export default source;
22
- }
23
- declare module 'vobs:icons' {
24
- const icons: import('@vobs/icons').IconRegistry;
25
- export { icons };
26
- export default icons;
27
- }
28
- declare module 'vobs:routes' {
29
- import type { RouteComponent, RouteRecord, RouterOptions } from '@vobs/router';
30
- import type { RouterPort } from '@vobs/runtime-core';
31
- export interface GeneratedRoutePathMap {
32
- }
33
- export type RoutePath = keyof GeneratedRoutePathMap extends never ? string : Extract<keyof GeneratedRoutePathMap, string>;
34
- const generatedRoutes: readonly RouteRecord[];
35
- const generatedRoutePaths: readonly RoutePath[];
36
- const generatedNotFound: RouteComponent | undefined;
37
- const generatedError: RouteComponent | undefined;
38
- export type GeneratedRoutingManifest = {
39
- readonly routes: typeof generatedRoutes;
40
- readonly routePaths: typeof generatedRoutePaths;
41
- readonly notFound: typeof generatedNotFound;
42
- readonly error: typeof generatedError;
43
- };
44
- export function createRoutingRouter(options?: Omit<RouterOptions, 'routes' | 'notFound' | 'error'>): RouterPort | undefined;
45
- const router: RouterPort | undefined;
46
- export { generatedError as error, generatedNotFound as notFound, generatedRoutePaths as routePaths, generatedRoutes as routes, };
47
- export default router;
48
- }
package/dist/client.js DELETED
@@ -1,5 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vite-plugin
4
- */
5
- "use strict";
package/dist/index.d.ts DELETED
@@ -1,54 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/vite-plugin
4
- */
5
- export interface VobsVitePlugin {
6
- readonly name: 'vobs-vite-plugin';
7
- readonly enforce: 'pre';
8
- /** Vite/Rollup binds this hook helper at call time. */
9
- addWatchFile?(fileName: string): void;
10
- configResolved(config: {
11
- readonly root: string;
12
- }): void;
13
- buildStart(this: VobsBuildStartContext): void;
14
- closeWatcher(): void;
15
- resolveId(id: string, importer?: string): string | null;
16
- load(this: VobsLoadContext, id: string): Promise<string | {
17
- readonly code: string;
18
- readonly map: object;
19
- } | null>;
20
- transform(this: VobsTransformContext, code: string, id: string): {
21
- readonly code: string;
22
- readonly map: object;
23
- } | null;
24
- }
25
- interface VobsTransformContext {
26
- addWatchFile?(fileName: string): void;
27
- }
28
- interface VobsLoadContext {
29
- addWatchFile?(fileName: string): void;
30
- }
31
- interface VobsBuildStartContext {
32
- addWatchFile?(fileName: string): void;
33
- }
34
- export interface VobsVitePluginOptions {
35
- readonly runtimeModule?: string;
36
- readonly check?: boolean | VobsViteCheckOptions;
37
- readonly routes?: boolean | VobsViteRoutesOptions;
38
- readonly images?: boolean;
39
- readonly icons?: boolean | VobsViteIconsOptions;
40
- }
41
- export interface VobsViteCheckOptions {
42
- readonly tsconfigPath?: string;
43
- readonly rootNames?: readonly string[];
44
- readonly viewFiles?: readonly string[];
45
- }
46
- export interface VobsViteRoutesOptions {
47
- readonly pagesDir?: string;
48
- readonly typesFile?: string;
49
- }
50
- export interface VobsViteIconsOptions {
51
- readonly config?: string;
52
- }
53
- export declare function vobs(options?: VobsVitePluginOptions): VobsVitePlugin;
54
- export default vobs;