diff --git a/packages/core/src/server/transform/index.ts b/packages/core/src/server/transform/index.ts index 2dc09d46..343321c7 100644 --- a/packages/core/src/server/transform/index.ts +++ b/packages/core/src/server/transform/index.ts @@ -1,11 +1,11 @@ import fs from 'fs'; import { transformJsx } from './transform-jsx'; import { transformSvelte } from './transform-svelte'; -import { transformVue } from './transform-vue'; +import { transformVue, transformVueHtml } from './transform-vue'; import { EscapeTags, PathType, isIgnoredFile } from '../../shared'; import { getRelativeOrAbsolutePath } from '../server'; -type FileType = 'vue' | 'jsx' | 'svelte' | unknown; +type FileType = 'vue' | 'vue-html' | 'jsx' | 'svelte' | unknown; type TransformCodeParams = { content: string; @@ -49,6 +49,8 @@ export function transformCode(params: TransformCodeParams) { try { if (fileType === 'vue') { return transformVue(content, filePath, finalEscapeTags); + } else if (fileType === 'vue-html') { + return transformVueHtml(content, filePath, finalEscapeTags); } else if (fileType === 'jsx') { return transformJsx(content, filePath, finalEscapeTags); } else if (fileType === 'svelte') { diff --git a/packages/core/src/server/transform/transform-vue.ts b/packages/core/src/server/transform/transform-vue.ts index 29f54a66..dcdea1ad 100644 --- a/packages/core/src/server/transform/transform-vue.ts +++ b/packages/core/src/server/transform/transform-vue.ts @@ -106,3 +106,27 @@ function transformVueTemplate( ], }); } + +/** + * Transform standalone HTML template files used as Vue templates. + * These are .html files that contain Vue template syntax and are loaded via + * require('./xxx.html') rather than through vue-loader's SFC mechanism. + * + * Unlike transformVue which handles full .vue SFC files, this function + * directly parses the HTML content as a Vue template fragment. + */ +export function transformVueHtml( + content: string, + filePath: string, + escapeTags: EscapeTags +) { + const s = new MagicString(content); + + const ast = parse(content, { + comments: true, + }); + + transformVueTemplate(ast, filePath, escapeTags, s); + + return s.toString(); +} diff --git a/packages/core/src/shared/utils.ts b/packages/core/src/shared/utils.ts index b6977382..dffbb0a0 100644 --- a/packages/core/src/shared/utils.ts +++ b/packages/core/src/shared/utils.ts @@ -366,7 +366,7 @@ export function isIgnoredFile({ fileType, }: { content: string; - fileType: 'vue' | 'jsx' | 'svelte' | unknown; + fileType: 'vue' | 'vue-html' | 'jsx' | 'svelte' | unknown; }): boolean { if (!content) { return false; @@ -374,8 +374,8 @@ export function isIgnoredFile({ const trimmed = content.trimStart(); const directives = ['code-inspector-disable', 'code-inspector-ignore']; - // Vue / Svelte - check HTML comments - if (fileType === 'vue' || fileType === 'svelte') { + // Vue / Vue HTML / Svelte - check HTML comments + if (fileType === 'vue' || fileType === 'vue-html' || fileType === 'svelte') { if (trimmed.startsWith(''); if (endIndex !== -1) { diff --git a/packages/core/types/server/transform/index.d.ts b/packages/core/types/server/transform/index.d.ts index 01065f1b..586edada 100644 --- a/packages/core/types/server/transform/index.d.ts +++ b/packages/core/types/server/transform/index.d.ts @@ -1,5 +1,5 @@ import { EscapeTags, PathType } from '../../shared'; -type FileType = 'vue' | 'jsx' | 'svelte' | unknown; +type FileType = 'vue' | 'vue-html' | 'jsx' | 'svelte' | unknown; type TransformCodeParams = { content: string; filePath: string; diff --git a/packages/core/types/server/transform/transform-vue.d.ts b/packages/core/types/server/transform/transform-vue.d.ts index a56a3bf4..878d8c7a 100644 --- a/packages/core/types/server/transform/transform-vue.d.ts +++ b/packages/core/types/server/transform/transform-vue.d.ts @@ -1,2 +1,11 @@ import { EscapeTags } from '../../shared'; export declare function transformVue(content: string, filePath: string, escapeTags: EscapeTags): string; +/** + * Transform standalone HTML template files used as Vue templates. + * These are .html files that contain Vue template syntax and are loaded via + * require('./xxx.html') rather than through vue-loader's SFC mechanism. + * + * Unlike transformVue which handles full .vue SFC files, this function + * directly parses the HTML content as a Vue template fragment. + */ +export declare function transformVueHtml(content: string, filePath: string, escapeTags: EscapeTags): string; diff --git a/packages/core/types/shared/utils.d.ts b/packages/core/types/shared/utils.d.ts index e4173046..c70ac561 100644 --- a/packages/core/types/shared/utils.d.ts +++ b/packages/core/types/shared/utils.d.ts @@ -133,6 +133,6 @@ export declare function hasWritePermission(filePath: string): boolean; */ export declare function isIgnoredFile({ content, fileType, }: { content: string; - fileType: 'vue' | 'jsx' | 'svelte' | unknown; + fileType: 'vue' | 'vue-html' | 'jsx' | 'svelte' | unknown; }): boolean; export {}; diff --git a/packages/webpack/src/index.ts b/packages/webpack/src/index.ts index cb408feb..29c6f61f 100644 --- a/packages/webpack/src/index.ts +++ b/packages/webpack/src/index.ts @@ -37,6 +37,11 @@ const applyLoader = (options: LoaderOptions, compiler: any) => { const module = _compiler?.options?.module; /* v8 ignore next -- fallback for legacy webpack versions with module.loaders */ const rules = module?.rules || module?.loaders || []; + // Determine the file match pattern, supporting both default and user-configured patterns + const matchPattern = options.match ?? /\.(vue|jsx|tsx|js|ts|mjs|mts|svelte)$/; + // Check if user's match pattern includes .html + const matchIncludesHtml = matchPattern instanceof RegExp && matchPattern.test('.html'); + rules.push( { test: options.match ?? /\.html$/, @@ -49,8 +54,26 @@ const applyLoader = (options: LoaderOptions, compiler: any) => { ], ...(options.enforcePre === false ? {} : { enforce: 'pre' }), }, + // If user's match pattern includes .html, add a separate rule for standalone .html files + // These are HTML template files used as Vue templates via require('./xxx.html') + ...(matchIncludesHtml + ? [ + { + test: /\.html$/, + use: [ + { + loader: path.resolve(compatibleDirname, `./loader.js`), + options, + }, + ], + ...(options.enforcePre === false ? {} : { enforce: 'pre' }), + }, + ] + : []), { - test: /\.(vue|jsx|tsx|js|ts|mjs|mts|svelte)$/, + test: matchIncludesHtml + ? /\.(vue|jsx|tsx|js|ts|mjs|mts|svelte)$/ + : matchPattern, use: [ { loader: path.resolve(compatibleDirname, `./loader.js`), diff --git a/packages/webpack/src/loader.ts b/packages/webpack/src/loader.ts index d6fa73d4..74173cf3 100644 --- a/packages/webpack/src/loader.ts +++ b/packages/webpack/src/loader.ts @@ -76,11 +76,19 @@ export default async function WebpackCodeInspectorLoader(content: string) { filePath.endsWith('.html') && params.get('type') === 'template' && params.has('vue'); - if (isVue || isHtmlVue) { + // Standalone HTML template files (used as Vue templates via require('./xxx.html')) + // These are HTML files that contain Vue template syntax but are not loaded through + // vue-loader's ?vue query mechanism + const isStandaloneHtmlTemplate = + filePath.endsWith('.html') && + !params.has('vue') && + options.match instanceof RegExp && + options.match.test('.html'); + if (isVue || isHtmlVue || isStandaloneHtmlTemplate) { return transformCode({ content, filePath, - fileType: 'vue', + fileType: isStandaloneHtmlTemplate ? 'vue-html' : 'vue', escapeTags, pathType: options.pathType, });