@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 +26 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +26 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/index.test.ts +48 -0
- package/src/index.ts +38 -4
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
|
-
|
|
199
|
-
|
|
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
|
|
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
|
}
|
package/dist/index.cjs.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":["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
|
-
|
|
166
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
37
|
-
"@vobs/
|
|
38
|
-
"
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
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
|
}
|