@vobs/vite-plugin 1.2.2 → 1.3.1

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/dist/index.cjs CHANGED
@@ -176,11 +176,13 @@ function vobsPlugin(options = {}) {
176
176
  const include = options.include ?? /\.tsx(?:$|\?)/;
177
177
  const htmlModules = /* @__PURE__ */ new Set();
178
178
  let productionBuild = false;
179
+ let hmrStateEnabled = (options.hmr ?? true) && (options.hmrState ?? true);
179
180
  return {
180
181
  name: "vobs",
181
182
  enforce: "pre",
182
183
  configResolved(config) {
183
184
  productionBuild = config.command === "build";
185
+ hmrStateEnabled = (options.hmr ?? true) && !productionBuild && (options.hmrState ?? true);
184
186
  },
185
187
  resolveId(source, importer) {
186
188
  if (!importer || !isHtmlComponent(source, options.html) || !isRelativeModule(source)) return null;
@@ -195,14 +197,24 @@ function vobsPlugin(options = {}) {
195
197
  return compileHtmlComponent(await (0, import_promises.readFile)(id, "utf8"), { filename: id });
196
198
  },
197
199
  transform(code, id) {
198
- include.lastIndex = 0;
199
- if (!include.test(id)) return null;
200
+ const cleanId = id.split(/[?#]/u, 1)[0];
201
+ const isTsx = cleanId.endsWith(".tsx");
202
+ const isStateTs = isStateModulePath(cleanId);
203
+ if (!isTsx && !isStateTs) return null;
204
+ if (options.include) {
205
+ include.lastIndex = 0;
206
+ if (!include.test(id)) return null;
207
+ }
208
+ if (!isTsx && (!hmrStateEnabled || !isStatefulModule(code))) return null;
200
209
  const extractor = options.extractI18n ? (0, import_compiler.createI18nExtractor)({ onKey: options.extractI18n }) : void 0;
210
+ const hmr = options.hmr ?? !productionBuild;
201
211
  const result = (0, import_compiler.compileWithSourceMap)(code, {
202
212
  ...options.compiler,
203
213
  // 生产构建默认剔除组件源码位置(错误定位走 source map);显式配置优先。
204
214
  sourceLocation: options.compiler?.sourceLocation ?? !productionBuild,
205
215
  filename: id,
216
+ // HMR 模块标识必须跨 ?t= 查询稳定(registry 复用语义依赖它),用干净路径。
217
+ hmrModuleId: hmr ? cleanId : options.compiler?.hmrModuleId,
206
218
  plugins: [
207
219
  ...options.compiler?.plugins ?? [],
208
220
  ...extractor ? [extractor.plugin] : []
@@ -219,8 +231,7 @@ function vobsPlugin(options = {}) {
219
231
  fix: diagnostic.fix
220
232
  });
221
233
  }
222
- const hmr = options.hmr ?? !productionBuild;
223
- const hmrCode = hmr ? createHmrCode(id) : "";
234
+ const hmrCode = hmr ? createHmrCode(cleanId) : "";
224
235
  return {
225
236
  code: `${result.code}${hmrCode}`,
226
237
  map: result.map
@@ -229,6 +240,17 @@ function vobsPlugin(options = {}) {
229
240
  };
230
241
  }
231
242
  __name(vobsPlugin, "vobsPlugin");
243
+ function isStatefulModule(code) {
244
+ return /\bimport\s+(?:type\s+)?\{[^}]*\bstate\b[^}]*\}\s*from\s*['"]@vobs\/(?:reactivity|vobs)['"]/u.test(code) && /(?<![\w$.])state\s*\(/u.test(code);
245
+ }
246
+ __name(isStatefulModule, "isStatefulModule");
247
+ function isStateModulePath(cleanId) {
248
+ if (!cleanId.endsWith(".ts")) return false;
249
+ if (cleanId.endsWith(".d.ts")) return false;
250
+ if (cleanId.includes("node_modules")) return false;
251
+ return true;
252
+ }
253
+ __name(isStateModulePath, "isStateModulePath");
232
254
  function isRelativeModule(source) {
233
255
  return source.startsWith("./") || source.startsWith("../");
234
256
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/html-component.ts"],"sourcesContent":["// Vite 插件:集成 Vobs 编译器\n\nimport { readFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport type { Plugin } from 'vite'\nimport { compileWithSourceMap, createI18nExtractor, type CompileOptions } from '@vobs/compiler'\nimport { VobsError } from '@vobs/runtime/error'\nimport { compileHtmlComponent } from './html-component.ts'\n\nexport interface VobsVitePluginOptions {\n include?: RegExp\n compiler?: CompileOptions\n hmr?: boolean\n extractI18n?: (key: string, filename: string) => void\n html?: boolean | { readonly extensions?: readonly string[] }\n}\n\nexport function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {\n const include = options.include ?? /\\.tsx(?:$|\\?)/\n const htmlModules = new Set<string>()\n let productionBuild = false\n\n return {\n name: 'vobs',\n\n enforce: 'pre',\n\n configResolved(config) {\n productionBuild = config.command === 'build'\n },\n\n resolveId(source: string, importer: string | undefined) {\n if (!importer || !isHtmlComponent(source, options.html) || !isRelativeModule(source)) return null\n const cleanImporter = importer.split(/[?#]/u, 1)[0]\n const cleanSource = source.split(/[?#]/u, 1)[0]\n const resolved = path.resolve(path.dirname(cleanImporter), cleanSource)\n htmlModules.add(resolved)\n return resolved\n },\n\n async load(id: string) {\n if (!htmlModules.has(id)) return null\n return compileHtmlComponent(await readFile(id, 'utf8'), { filename: id })\n },\n\n transform(code: string, id: string) {\n include.lastIndex = 0\n if (!include.test(id)) return null\n\n const extractor = options.extractI18n\n ? createI18nExtractor({ onKey: options.extractI18n })\n : undefined\n const result = compileWithSourceMap(code, {\n ...options.compiler,\n // 生产构建默认剔除组件源码位置(错误定位走 source map);显式配置优先。\n sourceLocation: options.compiler?.sourceLocation ?? !productionBuild,\n filename: id,\n plugins: [\n ...(options.compiler?.plugins ?? []),\n ...(extractor ? [extractor.plugin] : [])\n ]\n })\n const diagnostic = result.diagnostics.find(item => item.severity === 'error')\n if (diagnostic) {\n throw new VobsError({\n code: diagnostic.code,\n layer: 'compiler',\n message: diagnostic.message,\n location: diagnostic.location,\n codeFrame: diagnostic.codeFrame,\n fix: diagnostic.fix\n })\n }\n const hmr = options.hmr ?? !productionBuild\n const hmrCode = hmr ? createHmrCode(id) : ''\n return {\n code: `${result.code}${hmrCode}`,\n map: result.map\n }\n }\n }\n}\n\nfunction isRelativeModule(source: string): boolean {\n return source.startsWith('./') || source.startsWith('../')\n}\n\nfunction isHtmlComponent(id: string, option: VobsVitePluginOptions['html']): boolean {\n if (option === false) return false\n const extensions = typeof option === 'object' && option.extensions?.length ? option.extensions : ['.html', '.htm']\n const cleanId = id.split(/[?#]/u, 1)[0]\n return extensions.some(extension => cleanId.endsWith(extension))\n}\n\n\nfunction createHmrCode(moduleId: string): string {\n const encodedId = JSON.stringify(moduleId)\n return `\nimport { disposeHmrModule, updateHmrModule } from '@vobs/vobs'\n\nif (import.meta.hot) {\n import.meta.hot.accept((module) => {\n if (module) updateHmrModule(${encodedId}, module)\n })\n import.meta.hot.dispose(() => disposeHmrModule(${encodedId}))\n}\n`\n}\n","import { parseFragment, type DefaultTreeAdapterMap, type DefaultTreeAdapterTypes } from 'parse5'\n\ntype HtmlNode = DefaultTreeAdapterTypes.Node\ntype HtmlDocumentFragment = DefaultTreeAdapterTypes.DocumentFragment\ntype HtmlElement = DefaultTreeAdapterTypes.Element\n\nconst blockedTags = new Set(['script', 'iframe', 'object', 'embed', 'base', 'meta', 'link', 'style'])\nconst blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu\nconst urlAttributes = new Set(['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'])\nconst safeProtocols = new Set(['http:', 'https:', 'mailto:', 'tel:'])\nconst directivePattern = /^data-vobs-(text|slot|on|bind|prop)(?:-(.+))?$/iu\nconst safePropertyNames = new Set([\n 'value', 'checked', 'selected', 'disabled', 'multiple', 'readonly', 'required',\n 'autofocus', 'hidden', 'tabindex'\n])\n\nexport interface HtmlComponentOptions {\n readonly filename?: string\n}\n\n/** Converts a trusted static HTML fragment to Vobs runtime node creation code. */\nexport function compileHtmlComponent(source: string, options: HtmlComponentOptions = {}): string {\n const fragment = parseFragment(source)\n const body = compileChildren(fragment, options.filename ?? 'component.html')\n const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map(code => `insertBefore(parent, ${code}, anchor);`).join('')}})`\n\n 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(() => {})\"}; }`\n}\n\nfunction compileChildren(parent: HtmlDocumentFragment | HtmlElement, filename: string): string[] {\n return parent.childNodes.flatMap(node => compileNode(node, filename))\n}\n\nfunction compileNode(node: HtmlNode, filename: string): string[] {\n if (node.nodeName === '#text') {\n const value = (node as DefaultTreeAdapterMap['textNode']).value\n return value ? [`createText(${JSON.stringify(value)})`] : []\n }\n\n if (node.nodeName === '#comment') return []\n if (node.nodeName !== '#document-fragment' && !isElement(node)) return []\n\n if (isElement(node)) {\n const tag = node.tagName.toLowerCase()\n if (blockedTags.has(tag)) throw new Error(`Vobs HTML component: 禁止使用 <${tag}> (${filename})`)\n\n const children = compileChildren(node, filename)\n const statements = [`const element = createElement(${JSON.stringify(tag)})`]\n const dynamicText = [] as string[]\n for (const attribute of node.attrs) {\n const name = attribute.name.toLowerCase()\n const value = attribute.value\n const directive = parseDirective(name, value, filename)\n if (directive) {\n if (directive.kind === 'text') {\n dynamicText.push(directive.prop)\n } else if (directive.kind === 'slot') {\n statements.push(`insertDynamic(element, null, () => resolveHtmlSlot(props[${JSON.stringify(directive.prop)}]))`)\n } else if (directive.kind === 'event') {\n statements.push(`addEventListener(element, ${JSON.stringify(directive.name)}, (event) => { const handler = props[${JSON.stringify(directive.prop)}]; if (typeof handler === 'function') handler(event); })`)\n } else if (directive.kind === 'attribute') {\n statements.push(`bindAttribute(element, ${JSON.stringify(directive.name)}, () => sanitizeHtmlAttribute(${JSON.stringify(directive.name)}, props[${JSON.stringify(directive.prop)}] ?? ''))`)\n } else if (directive.kind === 'property') {\n statements.push(`bindProperty(element, ${JSON.stringify(directive.name)}, () => props[${JSON.stringify(directive.prop)}])`)\n }\n continue\n }\n if (blockedAttributes.test(name)) throw new Error(`Vobs HTML component: 禁止使用危险属性 ${attribute.name} (${filename})`)\n if (urlAttributes.has(name) && !isSafeUrl(value)) {\n throw new Error(`Vobs HTML component: 禁止使用危险 URL 属性 ${attribute.name} (${filename})`)\n }\n statements.push(`setAttribute(element, ${JSON.stringify(attribute.name)}, ${JSON.stringify(value)})`)\n }\n if (dynamicText.length > 1) throw new Error(`Vobs HTML component: 一个元素只能使用一个 data-vobs-text 指令 (${filename})`)\n if (dynamicText.length === 1) {\n const text = 'createText(\"\")'\n statements.push(`const text = ${text}`)\n statements.push('insertBefore(element, text, null)')\n statements.push(`bindText(text, () => props[${JSON.stringify(dynamicText[0])}] ?? '')`)\n } else {\n for (const child of children) statements.push(`insertBefore(element, ${child}, null)`)\n }\n statements.push('return element')\n return [`(() => {${statements.join(';')};})()`]\n }\n\n return compileChildren(node as HtmlDocumentFragment, filename)\n}\n\nfunction isElement(node: HtmlNode): node is HtmlElement {\n return !node.nodeName.startsWith('#')\n}\n\nfunction isSafeUrl(value: string): boolean {\n const normalized = value.trim().toLowerCase()\n if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return true\n try {\n return safeProtocols.has(new URL(normalized, 'https://vobs.invalid/').protocol)\n } catch {\n return false\n }\n}\n\ntype HtmlDirective =\n | { readonly kind: 'text' | 'slot'; readonly prop: string }\n | { readonly kind: 'event' | 'attribute' | 'property'; readonly name: string; readonly prop: string }\n\nfunction parseDirective(name: string, value: string, filename: string): HtmlDirective | null {\n const match = directivePattern.exec(name)\n if (!match) return null\n const kind = match[1].toLowerCase()\n const namePart = match[2]\n const prop = value.trim()\n if (!prop || !/^[A-Za-z_$][\\w$]*$/u.test(prop)) {\n throw new Error(`Vobs HTML component: ${name} 必须引用有效的 props 名称 (${filename})`)\n }\n if (kind === 'text' || kind === 'slot') {\n if (namePart) throw new Error(`Vobs HTML component: ${name} 不接受额外名称 (${filename})`)\n return { kind, prop }\n }\n if (!namePart || !/^[a-z][a-z0-9:-]*$/iu.test(namePart)) {\n throw new Error(`Vobs HTML component: ${name} 必须包含有效名称 (${filename})`)\n }\n if (kind === 'on') return { kind: 'event', name: namePart.toLowerCase(), prop }\n if (kind === 'bind') {\n if (namePart.toLowerCase() === 'style') throw new Error(`Vobs HTML component: 不允许动态绑定 style (${filename})`)\n return { kind: 'attribute', name: namePart, prop }\n }\n if (!safePropertyNames.has(namePart.toLowerCase())) {\n throw new Error(`Vobs HTML component: 不允许动态绑定 property ${namePart} (${filename})`)\n }\n return { kind: 'property', name: normalizePropertyName(namePart), prop }\n}\n\nfunction normalizePropertyName(name: string): string {\n return name.toLowerCase() === 'readonly' ? 'readOnly' : name.toLowerCase() === 'tabindex' ? 'tabIndex' : name\n}\n"],"mappings":"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","names":["path"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/html-component.ts"],"sourcesContent":["// Vite 插件:集成 Vobs 编译器\n\nimport { readFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport type { Plugin } from 'vite'\nimport { compileWithSourceMap, createI18nExtractor, type CompileOptions } from '@vobs/compiler'\nimport { VobsError } from '@vobs/runtime/error'\nimport { compileHtmlComponent } from './html-component.ts'\n\nexport interface VobsVitePluginOptions {\n include?: RegExp\n compiler?: CompileOptions\n hmr?: boolean\n /**\n * 模块级状态 HMR 保鲜(默认开启,仅 dev 生效)。开启后:\n * - 模块顶层 state() 声明编译为 hmrStateRef(...),热更新重执行模块时复用既有信号,\n * 消除\"新旧两份模块实例、两份状态\"导致的编辑不生效/页面半边失灵;\n * - 声明了模块级 state 的 .ts 文件(store 类模块)也纳入 HMR 处理。\n */\n hmrState?: boolean\n extractI18n?: (key: string, filename: string) => void\n html?: boolean | { readonly extensions?: readonly string[] }\n}\n\nexport function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {\n const include = options.include ?? /\\.tsx(?:$|\\?)/\n const htmlModules = new Set<string>()\n let productionBuild = false\n let hmrStateEnabled = (options.hmr ?? true) && (options.hmrState ?? true)\n\n return {\n name: 'vobs',\n\n enforce: 'pre',\n\n configResolved(config) {\n productionBuild = config.command === 'build'\n hmrStateEnabled = (options.hmr ?? true) && !productionBuild && (options.hmrState ?? true)\n },\n\n resolveId(source: string, importer: string | undefined) {\n if (!importer || !isHtmlComponent(source, options.html) || !isRelativeModule(source)) return null\n const cleanImporter = importer.split(/[?#]/u, 1)[0]\n const cleanSource = source.split(/[?#]/u, 1)[0]\n const resolved = path.resolve(path.dirname(cleanImporter), cleanSource)\n htmlModules.add(resolved)\n return resolved\n },\n\n async load(id: string) {\n if (!htmlModules.has(id)) return null\n return compileHtmlComponent(await readFile(id, 'utf8'), { filename: id })\n },\n\n transform(code: string, id: string) {\n const cleanId = id.split(/[?#]/u, 1)[0]\n const isTsx = cleanId.endsWith('.tsx')\n const isStateTs = isStateModulePath(cleanId)\n if (!isTsx && !isStateTs) return null\n if (options.include) {\n include.lastIndex = 0\n if (!include.test(id)) return null\n }\n\n // .ts 状态模块(store 类):声明了模块级 state 才纳入编译与 HMR,\n // 避免对普通 .ts 全量重印。\n if (!isTsx && (!hmrStateEnabled || !isStatefulModule(code))) return null\n\n const extractor = options.extractI18n\n ? createI18nExtractor({ onKey: options.extractI18n })\n : undefined\n const hmr = options.hmr ?? !productionBuild\n const result = compileWithSourceMap(code, {\n ...options.compiler,\n // 生产构建默认剔除组件源码位置(错误定位走 source map);显式配置优先。\n sourceLocation: options.compiler?.sourceLocation ?? !productionBuild,\n filename: id,\n // HMR 模块标识必须跨 ?t= 查询稳定(registry 复用语义依赖它),用干净路径。\n hmrModuleId: hmr ? cleanId : options.compiler?.hmrModuleId,\n plugins: [\n ...(options.compiler?.plugins ?? []),\n ...(extractor ? [extractor.plugin] : [])\n ]\n })\n const diagnostic = result.diagnostics.find(item => item.severity === 'error')\n if (diagnostic) {\n throw new VobsError({\n code: diagnostic.code,\n layer: 'compiler',\n message: diagnostic.message,\n location: diagnostic.location,\n codeFrame: diagnostic.codeFrame,\n fix: diagnostic.fix\n })\n }\n const hmrCode = hmr ? createHmrCode(cleanId) : ''\n return {\n code: `${result.code}${hmrCode}`,\n map: result.map\n }\n }\n }\n}\n\n/** 模块级 state 的 store 类 .ts 模块判定:从 @vobs 导入 state 且实际调用。 */\nfunction isStatefulModule(code: string): boolean {\n return /\\bimport\\s+(?:type\\s+)?\\{[^}]*\\bstate\\b[^}]*\\}\\s*from\\s*['\"]@vobs\\/(?:reactivity|vobs)['\"]/u.test(code)\n && /(?<![\\w$.])state\\s*\\(/u.test(code)\n}\n\nfunction isStateModulePath(cleanId: string): boolean {\n if (!cleanId.endsWith('.ts')) return false\n if (cleanId.endsWith('.d.ts')) return false\n if (cleanId.includes('node_modules')) return false\n return true\n}\n\nfunction isRelativeModule(source: string): boolean {\n return source.startsWith('./') || source.startsWith('../')\n}\n\nfunction isHtmlComponent(id: string, option: VobsVitePluginOptions['html']): boolean {\n if (option === false) return false\n const extensions = typeof option === 'object' && option.extensions?.length ? option.extensions : ['.html', '.htm']\n const cleanId = id.split(/[?#]/u, 1)[0]\n return extensions.some(extension => cleanId.endsWith(extension))\n}\n\n\nfunction createHmrCode(moduleId: string): string {\n const encodedId = JSON.stringify(moduleId)\n return `\nimport { disposeHmrModule, updateHmrModule } from '@vobs/vobs'\n\nif (import.meta.hot) {\n import.meta.hot.accept((module) => {\n if (module) updateHmrModule(${encodedId}, module)\n })\n import.meta.hot.dispose(() => disposeHmrModule(${encodedId}))\n}\n`\n}\n","import { parseFragment, type DefaultTreeAdapterMap, type DefaultTreeAdapterTypes } from 'parse5'\n\ntype HtmlNode = DefaultTreeAdapterTypes.Node\ntype HtmlDocumentFragment = DefaultTreeAdapterTypes.DocumentFragment\ntype HtmlElement = DefaultTreeAdapterTypes.Element\n\nconst blockedTags = new Set(['script', 'iframe', 'object', 'embed', 'base', 'meta', 'link', 'style'])\nconst blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu\nconst urlAttributes = new Set(['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'])\nconst safeProtocols = new Set(['http:', 'https:', 'mailto:', 'tel:'])\nconst directivePattern = /^data-vobs-(text|slot|on|bind|prop)(?:-(.+))?$/iu\nconst safePropertyNames = new Set([\n 'value', 'checked', 'selected', 'disabled', 'multiple', 'readonly', 'required',\n 'autofocus', 'hidden', 'tabindex'\n])\n\nexport interface HtmlComponentOptions {\n readonly filename?: string\n}\n\n/** Converts a trusted static HTML fragment to Vobs runtime node creation code. */\nexport function compileHtmlComponent(source: string, options: HtmlComponentOptions = {}): string {\n const fragment = parseFragment(source)\n const body = compileChildren(fragment, options.filename ?? 'component.html')\n const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map(code => `insertBefore(parent, ${code}, anchor);`).join('')}})`\n\n 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(() => {})\"}; }`\n}\n\nfunction compileChildren(parent: HtmlDocumentFragment | HtmlElement, filename: string): string[] {\n return parent.childNodes.flatMap(node => compileNode(node, filename))\n}\n\nfunction compileNode(node: HtmlNode, filename: string): string[] {\n if (node.nodeName === '#text') {\n const value = (node as DefaultTreeAdapterMap['textNode']).value\n return value ? [`createText(${JSON.stringify(value)})`] : []\n }\n\n if (node.nodeName === '#comment') return []\n if (node.nodeName !== '#document-fragment' && !isElement(node)) return []\n\n if (isElement(node)) {\n const tag = node.tagName.toLowerCase()\n if (blockedTags.has(tag)) throw new Error(`Vobs HTML component: 禁止使用 <${tag}> (${filename})`)\n\n const children = compileChildren(node, filename)\n const statements = [`const element = createElement(${JSON.stringify(tag)})`]\n const dynamicText = [] as string[]\n for (const attribute of node.attrs) {\n const name = attribute.name.toLowerCase()\n const value = attribute.value\n const directive = parseDirective(name, value, filename)\n if (directive) {\n if (directive.kind === 'text') {\n dynamicText.push(directive.prop)\n } else if (directive.kind === 'slot') {\n statements.push(`insertDynamic(element, null, () => resolveHtmlSlot(props[${JSON.stringify(directive.prop)}]))`)\n } else if (directive.kind === 'event') {\n statements.push(`addEventListener(element, ${JSON.stringify(directive.name)}, (event) => { const handler = props[${JSON.stringify(directive.prop)}]; if (typeof handler === 'function') handler(event); })`)\n } else if (directive.kind === 'attribute') {\n statements.push(`bindAttribute(element, ${JSON.stringify(directive.name)}, () => sanitizeHtmlAttribute(${JSON.stringify(directive.name)}, props[${JSON.stringify(directive.prop)}] ?? ''))`)\n } else if (directive.kind === 'property') {\n statements.push(`bindProperty(element, ${JSON.stringify(directive.name)}, () => props[${JSON.stringify(directive.prop)}])`)\n }\n continue\n }\n if (blockedAttributes.test(name)) throw new Error(`Vobs HTML component: 禁止使用危险属性 ${attribute.name} (${filename})`)\n if (urlAttributes.has(name) && !isSafeUrl(value)) {\n throw new Error(`Vobs HTML component: 禁止使用危险 URL 属性 ${attribute.name} (${filename})`)\n }\n statements.push(`setAttribute(element, ${JSON.stringify(attribute.name)}, ${JSON.stringify(value)})`)\n }\n if (dynamicText.length > 1) throw new Error(`Vobs HTML component: 一个元素只能使用一个 data-vobs-text 指令 (${filename})`)\n if (dynamicText.length === 1) {\n const text = 'createText(\"\")'\n statements.push(`const text = ${text}`)\n statements.push('insertBefore(element, text, null)')\n statements.push(`bindText(text, () => props[${JSON.stringify(dynamicText[0])}] ?? '')`)\n } else {\n for (const child of children) statements.push(`insertBefore(element, ${child}, null)`)\n }\n statements.push('return element')\n return [`(() => {${statements.join(';')};})()`]\n }\n\n return compileChildren(node as HtmlDocumentFragment, filename)\n}\n\nfunction isElement(node: HtmlNode): node is HtmlElement {\n return !node.nodeName.startsWith('#')\n}\n\nfunction isSafeUrl(value: string): boolean {\n const normalized = value.trim().toLowerCase()\n if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return true\n try {\n return safeProtocols.has(new URL(normalized, 'https://vobs.invalid/').protocol)\n } catch {\n return false\n }\n}\n\ntype HtmlDirective =\n | { readonly kind: 'text' | 'slot'; readonly prop: string }\n | { readonly kind: 'event' | 'attribute' | 'property'; readonly name: string; readonly prop: string }\n\nfunction parseDirective(name: string, value: string, filename: string): HtmlDirective | null {\n const match = directivePattern.exec(name)\n if (!match) return null\n const kind = match[1].toLowerCase()\n const namePart = match[2]\n const prop = value.trim()\n if (!prop || !/^[A-Za-z_$][\\w$]*$/u.test(prop)) {\n throw new Error(`Vobs HTML component: ${name} 必须引用有效的 props 名称 (${filename})`)\n }\n if (kind === 'text' || kind === 'slot') {\n if (namePart) throw new Error(`Vobs HTML component: ${name} 不接受额外名称 (${filename})`)\n return { kind, prop }\n }\n if (!namePart || !/^[a-z][a-z0-9:-]*$/iu.test(namePart)) {\n throw new Error(`Vobs HTML component: ${name} 必须包含有效名称 (${filename})`)\n }\n if (kind === 'on') return { kind: 'event', name: namePart.toLowerCase(), prop }\n if (kind === 'bind') {\n if (namePart.toLowerCase() === 'style') throw new Error(`Vobs HTML component: 不允许动态绑定 style (${filename})`)\n return { kind: 'attribute', name: namePart, prop }\n }\n if (!safePropertyNames.has(namePart.toLowerCase())) {\n throw new Error(`Vobs HTML component: 不允许动态绑定 property ${namePart} (${filename})`)\n }\n return { kind: 'property', name: normalizePropertyName(namePart), prop }\n}\n\nfunction normalizePropertyName(name: string): string {\n return name.toLowerCase() === 'readonly' ? 'readOnly' : name.toLowerCase() === 'tabindex' ? 'tabIndex' : name\n}\n"],"mappings":"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","names":["path"]}
package/dist/index.d.cts CHANGED
@@ -5,6 +5,13 @@ interface VobsVitePluginOptions {
5
5
  include?: RegExp;
6
6
  compiler?: CompileOptions;
7
7
  hmr?: boolean;
8
+ /**
9
+ * 模块级状态 HMR 保鲜(默认开启,仅 dev 生效)。开启后:
10
+ * - 模块顶层 state() 声明编译为 hmrStateRef(...),热更新重执行模块时复用既有信号,
11
+ * 消除"新旧两份模块实例、两份状态"导致的编辑不生效/页面半边失灵;
12
+ * - 声明了模块级 state 的 .ts 文件(store 类模块)也纳入 HMR 处理。
13
+ */
14
+ hmrState?: boolean;
8
15
  extractI18n?: (key: string, filename: string) => void;
9
16
  html?: boolean | {
10
17
  readonly extensions?: readonly string[];
package/dist/index.d.ts CHANGED
@@ -5,6 +5,13 @@ interface VobsVitePluginOptions {
5
5
  include?: RegExp;
6
6
  compiler?: CompileOptions;
7
7
  hmr?: boolean;
8
+ /**
9
+ * 模块级状态 HMR 保鲜(默认开启,仅 dev 生效)。开启后:
10
+ * - 模块顶层 state() 声明编译为 hmrStateRef(...),热更新重执行模块时复用既有信号,
11
+ * 消除"新旧两份模块实例、两份状态"导致的编辑不生效/页面半边失灵;
12
+ * - 声明了模块级 state 的 .ts 文件(store 类模块)也纳入 HMR 处理。
13
+ */
14
+ hmrState?: boolean;
8
15
  extractI18n?: (key: string, filename: string) => void;
9
16
  html?: boolean | {
10
17
  readonly extensions?: readonly string[];
package/dist/index.js CHANGED
@@ -143,11 +143,13 @@ function vobsPlugin(options = {}) {
143
143
  const include = options.include ?? /\.tsx(?:$|\?)/;
144
144
  const htmlModules = /* @__PURE__ */ new Set();
145
145
  let productionBuild = false;
146
+ let hmrStateEnabled = (options.hmr ?? true) && (options.hmrState ?? true);
146
147
  return {
147
148
  name: "vobs",
148
149
  enforce: "pre",
149
150
  configResolved(config) {
150
151
  productionBuild = config.command === "build";
152
+ hmrStateEnabled = (options.hmr ?? true) && !productionBuild && (options.hmrState ?? true);
151
153
  },
152
154
  resolveId(source, importer) {
153
155
  if (!importer || !isHtmlComponent(source, options.html) || !isRelativeModule(source)) return null;
@@ -162,14 +164,24 @@ function vobsPlugin(options = {}) {
162
164
  return compileHtmlComponent(await readFile(id, "utf8"), { filename: id });
163
165
  },
164
166
  transform(code, id) {
165
- include.lastIndex = 0;
166
- if (!include.test(id)) return null;
167
+ const cleanId = id.split(/[?#]/u, 1)[0];
168
+ const isTsx = cleanId.endsWith(".tsx");
169
+ const isStateTs = isStateModulePath(cleanId);
170
+ if (!isTsx && !isStateTs) return null;
171
+ if (options.include) {
172
+ include.lastIndex = 0;
173
+ if (!include.test(id)) return null;
174
+ }
175
+ if (!isTsx && (!hmrStateEnabled || !isStatefulModule(code))) return null;
167
176
  const extractor = options.extractI18n ? createI18nExtractor({ onKey: options.extractI18n }) : void 0;
177
+ const hmr = options.hmr ?? !productionBuild;
168
178
  const result = compileWithSourceMap(code, {
169
179
  ...options.compiler,
170
180
  // 生产构建默认剔除组件源码位置(错误定位走 source map);显式配置优先。
171
181
  sourceLocation: options.compiler?.sourceLocation ?? !productionBuild,
172
182
  filename: id,
183
+ // HMR 模块标识必须跨 ?t= 查询稳定(registry 复用语义依赖它),用干净路径。
184
+ hmrModuleId: hmr ? cleanId : options.compiler?.hmrModuleId,
173
185
  plugins: [
174
186
  ...options.compiler?.plugins ?? [],
175
187
  ...extractor ? [extractor.plugin] : []
@@ -186,8 +198,7 @@ function vobsPlugin(options = {}) {
186
198
  fix: diagnostic.fix
187
199
  });
188
200
  }
189
- const hmr = options.hmr ?? !productionBuild;
190
- const hmrCode = hmr ? createHmrCode(id) : "";
201
+ const hmrCode = hmr ? createHmrCode(cleanId) : "";
191
202
  return {
192
203
  code: `${result.code}${hmrCode}`,
193
204
  map: result.map
@@ -196,6 +207,17 @@ function vobsPlugin(options = {}) {
196
207
  };
197
208
  }
198
209
  __name(vobsPlugin, "vobsPlugin");
210
+ function isStatefulModule(code) {
211
+ return /\bimport\s+(?:type\s+)?\{[^}]*\bstate\b[^}]*\}\s*from\s*['"]@vobs\/(?:reactivity|vobs)['"]/u.test(code) && /(?<![\w$.])state\s*\(/u.test(code);
212
+ }
213
+ __name(isStatefulModule, "isStatefulModule");
214
+ function isStateModulePath(cleanId) {
215
+ if (!cleanId.endsWith(".ts")) return false;
216
+ if (cleanId.endsWith(".d.ts")) return false;
217
+ if (cleanId.includes("node_modules")) return false;
218
+ return true;
219
+ }
220
+ __name(isStateModulePath, "isStateModulePath");
199
221
  function isRelativeModule(source) {
200
222
  return source.startsWith("./") || source.startsWith("../");
201
223
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/html-component.ts"],"sourcesContent":["// Vite 插件:集成 Vobs 编译器\n\nimport { readFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport type { Plugin } from 'vite'\nimport { compileWithSourceMap, createI18nExtractor, type CompileOptions } from '@vobs/compiler'\nimport { VobsError } from '@vobs/runtime/error'\nimport { compileHtmlComponent } from './html-component.ts'\n\nexport interface VobsVitePluginOptions {\n include?: RegExp\n compiler?: CompileOptions\n hmr?: boolean\n extractI18n?: (key: string, filename: string) => void\n html?: boolean | { readonly extensions?: readonly string[] }\n}\n\nexport function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {\n const include = options.include ?? /\\.tsx(?:$|\\?)/\n const htmlModules = new Set<string>()\n let productionBuild = false\n\n return {\n name: 'vobs',\n\n enforce: 'pre',\n\n configResolved(config) {\n productionBuild = config.command === 'build'\n },\n\n resolveId(source: string, importer: string | undefined) {\n if (!importer || !isHtmlComponent(source, options.html) || !isRelativeModule(source)) return null\n const cleanImporter = importer.split(/[?#]/u, 1)[0]\n const cleanSource = source.split(/[?#]/u, 1)[0]\n const resolved = path.resolve(path.dirname(cleanImporter), cleanSource)\n htmlModules.add(resolved)\n return resolved\n },\n\n async load(id: string) {\n if (!htmlModules.has(id)) return null\n return compileHtmlComponent(await readFile(id, 'utf8'), { filename: id })\n },\n\n transform(code: string, id: string) {\n include.lastIndex = 0\n if (!include.test(id)) return null\n\n const extractor = options.extractI18n\n ? createI18nExtractor({ onKey: options.extractI18n })\n : undefined\n const result = compileWithSourceMap(code, {\n ...options.compiler,\n // 生产构建默认剔除组件源码位置(错误定位走 source map);显式配置优先。\n sourceLocation: options.compiler?.sourceLocation ?? !productionBuild,\n filename: id,\n plugins: [\n ...(options.compiler?.plugins ?? []),\n ...(extractor ? [extractor.plugin] : [])\n ]\n })\n const diagnostic = result.diagnostics.find(item => item.severity === 'error')\n if (diagnostic) {\n throw new VobsError({\n code: diagnostic.code,\n layer: 'compiler',\n message: diagnostic.message,\n location: diagnostic.location,\n codeFrame: diagnostic.codeFrame,\n fix: diagnostic.fix\n })\n }\n const hmr = options.hmr ?? !productionBuild\n const hmrCode = hmr ? createHmrCode(id) : ''\n return {\n code: `${result.code}${hmrCode}`,\n map: result.map\n }\n }\n }\n}\n\nfunction isRelativeModule(source: string): boolean {\n return source.startsWith('./') || source.startsWith('../')\n}\n\nfunction isHtmlComponent(id: string, option: VobsVitePluginOptions['html']): boolean {\n if (option === false) return false\n const extensions = typeof option === 'object' && option.extensions?.length ? option.extensions : ['.html', '.htm']\n const cleanId = id.split(/[?#]/u, 1)[0]\n return extensions.some(extension => cleanId.endsWith(extension))\n}\n\n\nfunction createHmrCode(moduleId: string): string {\n const encodedId = JSON.stringify(moduleId)\n return `\nimport { disposeHmrModule, updateHmrModule } from '@vobs/vobs'\n\nif (import.meta.hot) {\n import.meta.hot.accept((module) => {\n if (module) updateHmrModule(${encodedId}, module)\n })\n import.meta.hot.dispose(() => disposeHmrModule(${encodedId}))\n}\n`\n}\n","import { parseFragment, type DefaultTreeAdapterMap, type DefaultTreeAdapterTypes } from 'parse5'\n\ntype HtmlNode = DefaultTreeAdapterTypes.Node\ntype HtmlDocumentFragment = DefaultTreeAdapterTypes.DocumentFragment\ntype HtmlElement = DefaultTreeAdapterTypes.Element\n\nconst blockedTags = new Set(['script', 'iframe', 'object', 'embed', 'base', 'meta', 'link', 'style'])\nconst blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu\nconst urlAttributes = new Set(['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'])\nconst safeProtocols = new Set(['http:', 'https:', 'mailto:', 'tel:'])\nconst directivePattern = /^data-vobs-(text|slot|on|bind|prop)(?:-(.+))?$/iu\nconst safePropertyNames = new Set([\n 'value', 'checked', 'selected', 'disabled', 'multiple', 'readonly', 'required',\n 'autofocus', 'hidden', 'tabindex'\n])\n\nexport interface HtmlComponentOptions {\n readonly filename?: string\n}\n\n/** Converts a trusted static HTML fragment to Vobs runtime node creation code. */\nexport function compileHtmlComponent(source: string, options: HtmlComponentOptions = {}): string {\n const fragment = parseFragment(source)\n const body = compileChildren(fragment, options.filename ?? 'component.html')\n const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map(code => `insertBefore(parent, ${code}, anchor);`).join('')}})`\n\n 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(() => {})\"}; }`\n}\n\nfunction compileChildren(parent: HtmlDocumentFragment | HtmlElement, filename: string): string[] {\n return parent.childNodes.flatMap(node => compileNode(node, filename))\n}\n\nfunction compileNode(node: HtmlNode, filename: string): string[] {\n if (node.nodeName === '#text') {\n const value = (node as DefaultTreeAdapterMap['textNode']).value\n return value ? [`createText(${JSON.stringify(value)})`] : []\n }\n\n if (node.nodeName === '#comment') return []\n if (node.nodeName !== '#document-fragment' && !isElement(node)) return []\n\n if (isElement(node)) {\n const tag = node.tagName.toLowerCase()\n if (blockedTags.has(tag)) throw new Error(`Vobs HTML component: 禁止使用 <${tag}> (${filename})`)\n\n const children = compileChildren(node, filename)\n const statements = [`const element = createElement(${JSON.stringify(tag)})`]\n const dynamicText = [] as string[]\n for (const attribute of node.attrs) {\n const name = attribute.name.toLowerCase()\n const value = attribute.value\n const directive = parseDirective(name, value, filename)\n if (directive) {\n if (directive.kind === 'text') {\n dynamicText.push(directive.prop)\n } else if (directive.kind === 'slot') {\n statements.push(`insertDynamic(element, null, () => resolveHtmlSlot(props[${JSON.stringify(directive.prop)}]))`)\n } else if (directive.kind === 'event') {\n statements.push(`addEventListener(element, ${JSON.stringify(directive.name)}, (event) => { const handler = props[${JSON.stringify(directive.prop)}]; if (typeof handler === 'function') handler(event); })`)\n } else if (directive.kind === 'attribute') {\n statements.push(`bindAttribute(element, ${JSON.stringify(directive.name)}, () => sanitizeHtmlAttribute(${JSON.stringify(directive.name)}, props[${JSON.stringify(directive.prop)}] ?? ''))`)\n } else if (directive.kind === 'property') {\n statements.push(`bindProperty(element, ${JSON.stringify(directive.name)}, () => props[${JSON.stringify(directive.prop)}])`)\n }\n continue\n }\n if (blockedAttributes.test(name)) throw new Error(`Vobs HTML component: 禁止使用危险属性 ${attribute.name} (${filename})`)\n if (urlAttributes.has(name) && !isSafeUrl(value)) {\n throw new Error(`Vobs HTML component: 禁止使用危险 URL 属性 ${attribute.name} (${filename})`)\n }\n statements.push(`setAttribute(element, ${JSON.stringify(attribute.name)}, ${JSON.stringify(value)})`)\n }\n if (dynamicText.length > 1) throw new Error(`Vobs HTML component: 一个元素只能使用一个 data-vobs-text 指令 (${filename})`)\n if (dynamicText.length === 1) {\n const text = 'createText(\"\")'\n statements.push(`const text = ${text}`)\n statements.push('insertBefore(element, text, null)')\n statements.push(`bindText(text, () => props[${JSON.stringify(dynamicText[0])}] ?? '')`)\n } else {\n for (const child of children) statements.push(`insertBefore(element, ${child}, null)`)\n }\n statements.push('return element')\n return [`(() => {${statements.join(';')};})()`]\n }\n\n return compileChildren(node as HtmlDocumentFragment, filename)\n}\n\nfunction isElement(node: HtmlNode): node is HtmlElement {\n return !node.nodeName.startsWith('#')\n}\n\nfunction isSafeUrl(value: string): boolean {\n const normalized = value.trim().toLowerCase()\n if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return true\n try {\n return safeProtocols.has(new URL(normalized, 'https://vobs.invalid/').protocol)\n } catch {\n return false\n }\n}\n\ntype HtmlDirective =\n | { readonly kind: 'text' | 'slot'; readonly prop: string }\n | { readonly kind: 'event' | 'attribute' | 'property'; readonly name: string; readonly prop: string }\n\nfunction parseDirective(name: string, value: string, filename: string): HtmlDirective | null {\n const match = directivePattern.exec(name)\n if (!match) return null\n const kind = match[1].toLowerCase()\n const namePart = match[2]\n const prop = value.trim()\n if (!prop || !/^[A-Za-z_$][\\w$]*$/u.test(prop)) {\n throw new Error(`Vobs HTML component: ${name} 必须引用有效的 props 名称 (${filename})`)\n }\n if (kind === 'text' || kind === 'slot') {\n if (namePart) throw new Error(`Vobs HTML component: ${name} 不接受额外名称 (${filename})`)\n return { kind, prop }\n }\n if (!namePart || !/^[a-z][a-z0-9:-]*$/iu.test(namePart)) {\n throw new Error(`Vobs HTML component: ${name} 必须包含有效名称 (${filename})`)\n }\n if (kind === 'on') return { kind: 'event', name: namePart.toLowerCase(), prop }\n if (kind === 'bind') {\n if (namePart.toLowerCase() === 'style') throw new Error(`Vobs HTML component: 不允许动态绑定 style (${filename})`)\n return { kind: 'attribute', name: namePart, prop }\n }\n if (!safePropertyNames.has(namePart.toLowerCase())) {\n throw new Error(`Vobs HTML component: 不允许动态绑定 property ${namePart} (${filename})`)\n }\n return { kind: 'property', name: normalizePropertyName(namePart), prop }\n}\n\nfunction normalizePropertyName(name: string): string {\n return name.toLowerCase() === 'readonly' ? 'readOnly' : name.toLowerCase() === 'tabindex' ? 'tabIndex' : name\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/html-component.ts"],"sourcesContent":["// Vite 插件:集成 Vobs 编译器\n\nimport { readFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport type { Plugin } from 'vite'\nimport { compileWithSourceMap, createI18nExtractor, type CompileOptions } from '@vobs/compiler'\nimport { VobsError } from '@vobs/runtime/error'\nimport { compileHtmlComponent } from './html-component.ts'\n\nexport interface VobsVitePluginOptions {\n include?: RegExp\n compiler?: CompileOptions\n hmr?: boolean\n /**\n * 模块级状态 HMR 保鲜(默认开启,仅 dev 生效)。开启后:\n * - 模块顶层 state() 声明编译为 hmrStateRef(...),热更新重执行模块时复用既有信号,\n * 消除\"新旧两份模块实例、两份状态\"导致的编辑不生效/页面半边失灵;\n * - 声明了模块级 state 的 .ts 文件(store 类模块)也纳入 HMR 处理。\n */\n hmrState?: boolean\n extractI18n?: (key: string, filename: string) => void\n html?: boolean | { readonly extensions?: readonly string[] }\n}\n\nexport function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {\n const include = options.include ?? /\\.tsx(?:$|\\?)/\n const htmlModules = new Set<string>()\n let productionBuild = false\n let hmrStateEnabled = (options.hmr ?? true) && (options.hmrState ?? true)\n\n return {\n name: 'vobs',\n\n enforce: 'pre',\n\n configResolved(config) {\n productionBuild = config.command === 'build'\n hmrStateEnabled = (options.hmr ?? true) && !productionBuild && (options.hmrState ?? true)\n },\n\n resolveId(source: string, importer: string | undefined) {\n if (!importer || !isHtmlComponent(source, options.html) || !isRelativeModule(source)) return null\n const cleanImporter = importer.split(/[?#]/u, 1)[0]\n const cleanSource = source.split(/[?#]/u, 1)[0]\n const resolved = path.resolve(path.dirname(cleanImporter), cleanSource)\n htmlModules.add(resolved)\n return resolved\n },\n\n async load(id: string) {\n if (!htmlModules.has(id)) return null\n return compileHtmlComponent(await readFile(id, 'utf8'), { filename: id })\n },\n\n transform(code: string, id: string) {\n const cleanId = id.split(/[?#]/u, 1)[0]\n const isTsx = cleanId.endsWith('.tsx')\n const isStateTs = isStateModulePath(cleanId)\n if (!isTsx && !isStateTs) return null\n if (options.include) {\n include.lastIndex = 0\n if (!include.test(id)) return null\n }\n\n // .ts 状态模块(store 类):声明了模块级 state 才纳入编译与 HMR,\n // 避免对普通 .ts 全量重印。\n if (!isTsx && (!hmrStateEnabled || !isStatefulModule(code))) return null\n\n const extractor = options.extractI18n\n ? createI18nExtractor({ onKey: options.extractI18n })\n : undefined\n const hmr = options.hmr ?? !productionBuild\n const result = compileWithSourceMap(code, {\n ...options.compiler,\n // 生产构建默认剔除组件源码位置(错误定位走 source map);显式配置优先。\n sourceLocation: options.compiler?.sourceLocation ?? !productionBuild,\n filename: id,\n // HMR 模块标识必须跨 ?t= 查询稳定(registry 复用语义依赖它),用干净路径。\n hmrModuleId: hmr ? cleanId : options.compiler?.hmrModuleId,\n plugins: [\n ...(options.compiler?.plugins ?? []),\n ...(extractor ? [extractor.plugin] : [])\n ]\n })\n const diagnostic = result.diagnostics.find(item => item.severity === 'error')\n if (diagnostic) {\n throw new VobsError({\n code: diagnostic.code,\n layer: 'compiler',\n message: diagnostic.message,\n location: diagnostic.location,\n codeFrame: diagnostic.codeFrame,\n fix: diagnostic.fix\n })\n }\n const hmrCode = hmr ? createHmrCode(cleanId) : ''\n return {\n code: `${result.code}${hmrCode}`,\n map: result.map\n }\n }\n }\n}\n\n/** 模块级 state 的 store 类 .ts 模块判定:从 @vobs 导入 state 且实际调用。 */\nfunction isStatefulModule(code: string): boolean {\n return /\\bimport\\s+(?:type\\s+)?\\{[^}]*\\bstate\\b[^}]*\\}\\s*from\\s*['\"]@vobs\\/(?:reactivity|vobs)['\"]/u.test(code)\n && /(?<![\\w$.])state\\s*\\(/u.test(code)\n}\n\nfunction isStateModulePath(cleanId: string): boolean {\n if (!cleanId.endsWith('.ts')) return false\n if (cleanId.endsWith('.d.ts')) return false\n if (cleanId.includes('node_modules')) return false\n return true\n}\n\nfunction isRelativeModule(source: string): boolean {\n return source.startsWith('./') || source.startsWith('../')\n}\n\nfunction isHtmlComponent(id: string, option: VobsVitePluginOptions['html']): boolean {\n if (option === false) return false\n const extensions = typeof option === 'object' && option.extensions?.length ? option.extensions : ['.html', '.htm']\n const cleanId = id.split(/[?#]/u, 1)[0]\n return extensions.some(extension => cleanId.endsWith(extension))\n}\n\n\nfunction createHmrCode(moduleId: string): string {\n const encodedId = JSON.stringify(moduleId)\n return `\nimport { disposeHmrModule, updateHmrModule } from '@vobs/vobs'\n\nif (import.meta.hot) {\n import.meta.hot.accept((module) => {\n if (module) updateHmrModule(${encodedId}, module)\n })\n import.meta.hot.dispose(() => disposeHmrModule(${encodedId}))\n}\n`\n}\n","import { parseFragment, type DefaultTreeAdapterMap, type DefaultTreeAdapterTypes } from 'parse5'\n\ntype HtmlNode = DefaultTreeAdapterTypes.Node\ntype HtmlDocumentFragment = DefaultTreeAdapterTypes.DocumentFragment\ntype HtmlElement = DefaultTreeAdapterTypes.Element\n\nconst blockedTags = new Set(['script', 'iframe', 'object', 'embed', 'base', 'meta', 'link', 'style'])\nconst blockedAttributes = /^(?:on[a-z]+|srcdoc|style)$/iu\nconst urlAttributes = new Set(['href', 'src', 'action', 'formaction', 'poster', 'xlink:href'])\nconst safeProtocols = new Set(['http:', 'https:', 'mailto:', 'tel:'])\nconst directivePattern = /^data-vobs-(text|slot|on|bind|prop)(?:-(.+))?$/iu\nconst safePropertyNames = new Set([\n 'value', 'checked', 'selected', 'disabled', 'multiple', 'readonly', 'required',\n 'autofocus', 'hidden', 'tabindex'\n])\n\nexport interface HtmlComponentOptions {\n readonly filename?: string\n}\n\n/** Converts a trusted static HTML fragment to Vobs runtime node creation code. */\nexport function compileHtmlComponent(source: string, options: HtmlComponentOptions = {}): string {\n const fragment = parseFragment(source)\n const body = compileChildren(fragment, options.filename ?? 'component.html')\n const result = body.length === 1 ? body[0] : `createFragment((parent, anchor) => {${body.map(code => `insertBefore(parent, ${code}, anchor);`).join('')}})`\n\n 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(() => {})\"}; }`\n}\n\nfunction compileChildren(parent: HtmlDocumentFragment | HtmlElement, filename: string): string[] {\n return parent.childNodes.flatMap(node => compileNode(node, filename))\n}\n\nfunction compileNode(node: HtmlNode, filename: string): string[] {\n if (node.nodeName === '#text') {\n const value = (node as DefaultTreeAdapterMap['textNode']).value\n return value ? [`createText(${JSON.stringify(value)})`] : []\n }\n\n if (node.nodeName === '#comment') return []\n if (node.nodeName !== '#document-fragment' && !isElement(node)) return []\n\n if (isElement(node)) {\n const tag = node.tagName.toLowerCase()\n if (blockedTags.has(tag)) throw new Error(`Vobs HTML component: 禁止使用 <${tag}> (${filename})`)\n\n const children = compileChildren(node, filename)\n const statements = [`const element = createElement(${JSON.stringify(tag)})`]\n const dynamicText = [] as string[]\n for (const attribute of node.attrs) {\n const name = attribute.name.toLowerCase()\n const value = attribute.value\n const directive = parseDirective(name, value, filename)\n if (directive) {\n if (directive.kind === 'text') {\n dynamicText.push(directive.prop)\n } else if (directive.kind === 'slot') {\n statements.push(`insertDynamic(element, null, () => resolveHtmlSlot(props[${JSON.stringify(directive.prop)}]))`)\n } else if (directive.kind === 'event') {\n statements.push(`addEventListener(element, ${JSON.stringify(directive.name)}, (event) => { const handler = props[${JSON.stringify(directive.prop)}]; if (typeof handler === 'function') handler(event); })`)\n } else if (directive.kind === 'attribute') {\n statements.push(`bindAttribute(element, ${JSON.stringify(directive.name)}, () => sanitizeHtmlAttribute(${JSON.stringify(directive.name)}, props[${JSON.stringify(directive.prop)}] ?? ''))`)\n } else if (directive.kind === 'property') {\n statements.push(`bindProperty(element, ${JSON.stringify(directive.name)}, () => props[${JSON.stringify(directive.prop)}])`)\n }\n continue\n }\n if (blockedAttributes.test(name)) throw new Error(`Vobs HTML component: 禁止使用危险属性 ${attribute.name} (${filename})`)\n if (urlAttributes.has(name) && !isSafeUrl(value)) {\n throw new Error(`Vobs HTML component: 禁止使用危险 URL 属性 ${attribute.name} (${filename})`)\n }\n statements.push(`setAttribute(element, ${JSON.stringify(attribute.name)}, ${JSON.stringify(value)})`)\n }\n if (dynamicText.length > 1) throw new Error(`Vobs HTML component: 一个元素只能使用一个 data-vobs-text 指令 (${filename})`)\n if (dynamicText.length === 1) {\n const text = 'createText(\"\")'\n statements.push(`const text = ${text}`)\n statements.push('insertBefore(element, text, null)')\n statements.push(`bindText(text, () => props[${JSON.stringify(dynamicText[0])}] ?? '')`)\n } else {\n for (const child of children) statements.push(`insertBefore(element, ${child}, null)`)\n }\n statements.push('return element')\n return [`(() => {${statements.join(';')};})()`]\n }\n\n return compileChildren(node as HtmlDocumentFragment, filename)\n}\n\nfunction isElement(node: HtmlNode): node is HtmlElement {\n return !node.nodeName.startsWith('#')\n}\n\nfunction isSafeUrl(value: string): boolean {\n const normalized = value.trim().toLowerCase()\n if (normalized.startsWith('#') || normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) return true\n try {\n return safeProtocols.has(new URL(normalized, 'https://vobs.invalid/').protocol)\n } catch {\n return false\n }\n}\n\ntype HtmlDirective =\n | { readonly kind: 'text' | 'slot'; readonly prop: string }\n | { readonly kind: 'event' | 'attribute' | 'property'; readonly name: string; readonly prop: string }\n\nfunction parseDirective(name: string, value: string, filename: string): HtmlDirective | null {\n const match = directivePattern.exec(name)\n if (!match) return null\n const kind = match[1].toLowerCase()\n const namePart = match[2]\n const prop = value.trim()\n if (!prop || !/^[A-Za-z_$][\\w$]*$/u.test(prop)) {\n throw new Error(`Vobs HTML component: ${name} 必须引用有效的 props 名称 (${filename})`)\n }\n if (kind === 'text' || kind === 'slot') {\n if (namePart) throw new Error(`Vobs HTML component: ${name} 不接受额外名称 (${filename})`)\n return { kind, prop }\n }\n if (!namePart || !/^[a-z][a-z0-9:-]*$/iu.test(namePart)) {\n throw new Error(`Vobs HTML component: ${name} 必须包含有效名称 (${filename})`)\n }\n if (kind === 'on') return { kind: 'event', name: namePart.toLowerCase(), prop }\n if (kind === 'bind') {\n if (namePart.toLowerCase() === 'style') throw new Error(`Vobs HTML component: 不允许动态绑定 style (${filename})`)\n return { kind: 'attribute', name: namePart, prop }\n }\n if (!safePropertyNames.has(namePart.toLowerCase())) {\n throw new Error(`Vobs HTML component: 不允许动态绑定 property ${namePart} (${filename})`)\n }\n return { kind: 'property', name: normalizePropertyName(namePart), prop }\n}\n\nfunction normalizePropertyName(name: string): string {\n return name.toLowerCase() === 'readonly' ? 'readOnly' : name.toLowerCase() === 'tabindex' ? 'tabIndex' : name\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "type": "git",
12
12
  "url": "git+https://github.com/vobsjs/vobs.git"
13
13
  },
14
- "version": "1.2.2",
14
+ "version": "1.3.1",
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
@@ -33,8 +33,8 @@
33
33
  "./source/*": "./src/*"
34
34
  },
35
35
  "dependencies": {
36
- "@vobs/compiler": "1.2.2",
37
- "@vobs/runtime": "1.2.2",
38
- "parse5": "^8.0.1"
36
+ "parse5": "^8.0.1",
37
+ "@vobs/compiler": "1.3.1",
38
+ "@vobs/runtime": "1.3.1"
39
39
  }
40
40
  }
package/src/index.test.ts CHANGED
@@ -141,6 +141,54 @@ describe('vobsPlugin', () => {
141
141
  expect(keys).toEqual(['page.title', 'common.ok'])
142
142
  })
143
143
 
144
+ it('为 .ts 状态模块注入 HMR 保鲜与接受器(dev 默认开启)', () => {
145
+ const plugin = vobsPlugin()
146
+ const transform = plugin.transform
147
+ if (typeof transform !== 'function') throw new Error('Vobs Vite Plugin: 缺少 transform 钩子')
148
+
149
+ const result = transform.call({} as ThisParameterType<typeof transform>,
150
+ `import { state } from '@vobs/reactivity'\nexport const count = state(0)\n`,
151
+ 'src/stores/counter.ts'
152
+ ) as { code: string }
153
+
154
+ expect(result.code).toContain('hmrStateRef("src/stores/counter.ts#count"')
155
+ expect(result.code).toContain('import.meta.hot.accept')
156
+ })
157
+
158
+ it('普通 .ts 模块与 .d.ts 不参与编译', () => {
159
+ const plugin = vobsPlugin()
160
+ const transform = plugin.transform
161
+ if (typeof transform !== 'function') throw new Error('Vobs Vite Plugin: 缺少 transform 钩子')
162
+
163
+ expect(transform.call({} as ThisParameterType<typeof transform>, `export const value = 1`, 'src/utils/math.ts')).toBeNull()
164
+ expect(transform.call({} as ThisParameterType<typeof transform>, `export declare const x: number`, 'src/types.d.ts')).toBeNull()
165
+ })
166
+
167
+ it('hmrState: false 关闭 .ts 状态模块处理', () => {
168
+ const plugin = vobsPlugin({ hmrState: false })
169
+ const transform = plugin.transform
170
+ if (typeof transform !== 'function') throw new Error('Vobs Vite Plugin: 缺少 transform 钩子')
171
+
172
+ const result = transform.call({} as ThisParameterType<typeof transform>,
173
+ `import { state } from '@vobs/reactivity'\nexport const count = state(0)\n`,
174
+ 'src/stores/counter.ts'
175
+ )
176
+ expect(result).toBeNull()
177
+ })
178
+
179
+ it('生产构建不为 .ts 状态模块注入 HMR', () => {
180
+ const plugin = vobsPlugin()
181
+ ;(plugin.configResolved as (config: unknown) => void)?.({ command: 'build' })
182
+ const transform = plugin.transform
183
+ if (typeof transform !== 'function') throw new Error('Vobs Vite Plugin: 缺少 transform 钩子')
184
+
185
+ const result = transform.call({} as ThisParameterType<typeof transform>,
186
+ `import { state } from '@vobs/reactivity'\nexport const count = state(0)\n`,
187
+ 'src/stores/counter.ts'
188
+ )
189
+ expect(result).toBeNull()
190
+ })
191
+
144
192
  it('将 HTML 模块编译为无 innerHTML 的 Vobs 组件', () => {
145
193
  const result = compileHtmlComponent('<article class="copy"><h1>Hello</h1><p>Safe</p></article>', {
146
194
  filename: 'src/content.html'
package/src/index.ts CHANGED
@@ -11,6 +11,13 @@ export interface VobsVitePluginOptions {
11
11
  include?: RegExp
12
12
  compiler?: CompileOptions
13
13
  hmr?: boolean
14
+ /**
15
+ * 模块级状态 HMR 保鲜(默认开启,仅 dev 生效)。开启后:
16
+ * - 模块顶层 state() 声明编译为 hmrStateRef(...),热更新重执行模块时复用既有信号,
17
+ * 消除"新旧两份模块实例、两份状态"导致的编辑不生效/页面半边失灵;
18
+ * - 声明了模块级 state 的 .ts 文件(store 类模块)也纳入 HMR 处理。
19
+ */
20
+ hmrState?: boolean
14
21
  extractI18n?: (key: string, filename: string) => void
15
22
  html?: boolean | { readonly extensions?: readonly string[] }
16
23
  }
@@ -19,6 +26,7 @@ export function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {
19
26
  const include = options.include ?? /\.tsx(?:$|\?)/
20
27
  const htmlModules = new Set<string>()
21
28
  let productionBuild = false
29
+ let hmrStateEnabled = (options.hmr ?? true) && (options.hmrState ?? true)
22
30
 
23
31
  return {
24
32
  name: 'vobs',
@@ -27,6 +35,7 @@ export function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {
27
35
 
28
36
  configResolved(config) {
29
37
  productionBuild = config.command === 'build'
38
+ hmrStateEnabled = (options.hmr ?? true) && !productionBuild && (options.hmrState ?? true)
30
39
  },
31
40
 
32
41
  resolveId(source: string, importer: string | undefined) {
@@ -44,17 +53,30 @@ export function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {
44
53
  },
45
54
 
46
55
  transform(code: string, id: string) {
47
- include.lastIndex = 0
48
- if (!include.test(id)) return null
56
+ const cleanId = id.split(/[?#]/u, 1)[0]
57
+ const isTsx = cleanId.endsWith('.tsx')
58
+ const isStateTs = isStateModulePath(cleanId)
59
+ if (!isTsx && !isStateTs) return null
60
+ if (options.include) {
61
+ include.lastIndex = 0
62
+ if (!include.test(id)) return null
63
+ }
64
+
65
+ // .ts 状态模块(store 类):声明了模块级 state 才纳入编译与 HMR,
66
+ // 避免对普通 .ts 全量重印。
67
+ if (!isTsx && (!hmrStateEnabled || !isStatefulModule(code))) return null
49
68
 
50
69
  const extractor = options.extractI18n
51
70
  ? createI18nExtractor({ onKey: options.extractI18n })
52
71
  : undefined
72
+ const hmr = options.hmr ?? !productionBuild
53
73
  const result = compileWithSourceMap(code, {
54
74
  ...options.compiler,
55
75
  // 生产构建默认剔除组件源码位置(错误定位走 source map);显式配置优先。
56
76
  sourceLocation: options.compiler?.sourceLocation ?? !productionBuild,
57
77
  filename: id,
78
+ // HMR 模块标识必须跨 ?t= 查询稳定(registry 复用语义依赖它),用干净路径。
79
+ hmrModuleId: hmr ? cleanId : options.compiler?.hmrModuleId,
58
80
  plugins: [
59
81
  ...(options.compiler?.plugins ?? []),
60
82
  ...(extractor ? [extractor.plugin] : [])
@@ -71,8 +93,7 @@ export function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {
71
93
  fix: diagnostic.fix
72
94
  })
73
95
  }
74
- const hmr = options.hmr ?? !productionBuild
75
- const hmrCode = hmr ? createHmrCode(id) : ''
96
+ const hmrCode = hmr ? createHmrCode(cleanId) : ''
76
97
  return {
77
98
  code: `${result.code}${hmrCode}`,
78
99
  map: result.map
@@ -81,6 +102,19 @@ export function vobsPlugin(options: VobsVitePluginOptions = {}): Plugin {
81
102
  }
82
103
  }
83
104
 
105
+ /** 模块级 state 的 store 类 .ts 模块判定:从 @vobs 导入 state 且实际调用。 */
106
+ function isStatefulModule(code: string): boolean {
107
+ return /\bimport\s+(?:type\s+)?\{[^}]*\bstate\b[^}]*\}\s*from\s*['"]@vobs\/(?:reactivity|vobs)['"]/u.test(code)
108
+ && /(?<![\w$.])state\s*\(/u.test(code)
109
+ }
110
+
111
+ function isStateModulePath(cleanId: string): boolean {
112
+ if (!cleanId.endsWith('.ts')) return false
113
+ if (cleanId.endsWith('.d.ts')) return false
114
+ if (cleanId.includes('node_modules')) return false
115
+ return true
116
+ }
117
+
84
118
  function isRelativeModule(source: string): boolean {
85
119
  return source.startsWith('./') || source.startsWith('../')
86
120
  }