diff --git a/demos/vite-react/src/components/FormItems/TagSelect/TagMultiSelect.tsx b/demos/vite-react/src/components/FormItems/TagSelect/TagMultiSelect.tsx index 9a9800c0..02e4a48d 100644 --- a/demos/vite-react/src/components/FormItems/TagSelect/TagMultiSelect.tsx +++ b/demos/vite-react/src/components/FormItems/TagSelect/TagMultiSelect.tsx @@ -43,7 +43,7 @@ export default function TagMultiSelect({ isSelected: value.indexOf(item.value) > -1, label: item.label, value: item.value, - onClick: () => onClick(item.value, item) + onClick: () => onClick(item.value) }); } return ( diff --git a/demos/vite-react/src/layouts/default/components/LocalSettings.tsx b/demos/vite-react/src/layouts/default/components/LocalSettings.tsx index a298804a..729af3a9 100644 --- a/demos/vite-react/src/layouts/default/components/LocalSettings.tsx +++ b/demos/vite-react/src/layouts/default/components/LocalSettings.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from 'react'; +import { ReactNode } from 'react'; import { Tooltip, Drawer, Button, Card, theme } from 'antd'; import Icon from '@/components/Icons'; import type { DrawerProps } from 'antd'; diff --git a/demos/vite-react/src/layouts/default/components/NoticeIcon/NoticeIcon.tsx b/demos/vite-react/src/layouts/default/components/NoticeIcon/NoticeIcon.tsx index 3fe4f5cb..d45a62b5 100644 --- a/demos/vite-react/src/layouts/default/components/NoticeIcon/NoticeIcon.tsx +++ b/demos/vite-react/src/layouts/default/components/NoticeIcon/NoticeIcon.tsx @@ -96,7 +96,7 @@ const NoticeIcon: React.FC & { ); }; - const { className, count, bell } = props; + const { count, bell } = props; const [visible, setVisible] = useControllableValue({ value: props.popupVisible, diff --git a/demos/vite-react/src/layouts/default/components/NoticeIcon/NoticeList.tsx b/demos/vite-react/src/layouts/default/components/NoticeIcon/NoticeList.tsx index 307a661e..ddbde7e2 100644 --- a/demos/vite-react/src/layouts/default/components/NoticeIcon/NoticeList.tsx +++ b/demos/vite-react/src/layouts/default/components/NoticeIcon/NoticeList.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { List, Avatar, theme, Tag } from 'antd'; +import { List, Avatar, theme } from 'antd'; import classNames from 'classnames'; import styles from './NoticeList.module.css'; const { useToken } = theme; diff --git a/demos/vite-react/src/layouts/default/components/NoticeIcon/index.tsx b/demos/vite-react/src/layouts/default/components/NoticeIcon/index.tsx index 8acf88b0..4ad46f6d 100644 --- a/demos/vite-react/src/layouts/default/components/NoticeIcon/index.tsx +++ b/demos/vite-react/src/layouts/default/components/NoticeIcon/index.tsx @@ -73,7 +73,7 @@ const NoticeIconView: React.FC = () => { ); const noticeData = getNoticeData(notices); - const unreadMsg = getUnreadData(noticeData || {}); + const unreadMsg = getUnreadData(noticeData); const changeReadState = (id: string) => { setNotices( diff --git a/demos/vite-react/src/pages/dashboard/components/SalesCard.tsx b/demos/vite-react/src/pages/dashboard/components/SalesCard.tsx index 35ad8902..ac4fd987 100644 --- a/demos/vite-react/src/pages/dashboard/components/SalesCard.tsx +++ b/demos/vite-react/src/pages/dashboard/components/SalesCard.tsx @@ -1,7 +1,6 @@ -import { Card, Tabs, Space, DatePicker } from 'antd'; +import { Card, Tabs } from 'antd'; import { Column } from '@ant-design/plots'; import '../style.css'; -const { RangePicker } = DatePicker; interface SalesCardProps { loading: boolean; @@ -77,21 +76,5 @@ function SalesCard({ loading, data }: SalesCardProps) { ); } -function TabBarExtraContent() { - return ( - - 今日 - 本周 - 本月 - 本年 - { - console.log(date, string); - }} - /> - - ); -} export default SalesCard; diff --git a/demos/vite-react/src/pages/dashboard/index.tsx b/demos/vite-react/src/pages/dashboard/index.tsx index 0161686a..53711a0d 100644 --- a/demos/vite-react/src/pages/dashboard/index.tsx +++ b/demos/vite-react/src/pages/dashboard/index.tsx @@ -14,7 +14,7 @@ export default function Analysis() { - + diff --git a/demos/vite-react/src/store/reducer/userSlice.ts b/demos/vite-react/src/store/reducer/userSlice.ts index ed0c86da..705ec5d2 100644 --- a/demos/vite-react/src/store/reducer/userSlice.ts +++ b/demos/vite-react/src/store/reducer/userSlice.ts @@ -38,7 +38,7 @@ export const userSlice = createSlice({ extraReducers: (builder) => { builder .addCase(login.fulfilled, (state, action) => { - const { UserInfo, SessionKey, MenuItems = [] } = action.payload; + const { UserInfo, SessionKey } = action.payload; state.userInfo = UserInfo; state.token = SessionKey; state.isLogin = true; diff --git a/demos/vite-vue2/src/utils/axios.ts b/demos/vite-vue2/src/utils/axios.ts index e259df72..9167f1f7 100644 --- a/demos/vite-vue2/src/utils/axios.ts +++ b/demos/vite-vue2/src/utils/axios.ts @@ -61,7 +61,7 @@ export const get = ( .catch((err) => { console.error(err, 'Get err') if (notice) { - let message = err?.data?.desc + let message = err.data?.desc if (err.message && err.message.indexOf('timeout') !== -1) { message = notice.timeoutMsg || '接口超时' } @@ -96,7 +96,7 @@ export const post = ( .catch((err) => { console.error(err, 'Post err') if (notice) { - let message = err?.data?.desc + let message = err.data?.desc if (err.message.indexOf('timeout') !== -1) { message = notice.timeoutMsg || '接口超时' } diff --git a/demos/webpack-vue-tsx/src/App.vue b/demos/webpack-vue-tsx/src/App.vue index 6b376ab5..9bbfbf74 100644 --- a/demos/webpack-vue-tsx/src/App.vue +++ b/demos/webpack-vue-tsx/src/App.vue @@ -6,7 +6,6 @@ and with space let replacedContent = content; - const scriptRegex = /\s]*))?)?>[\s\S]*?<\/script>/gi; - const styleRegex = /\s]*))?)?>[\s\S]*?<\/style>/gi; + const scriptRegex = + /\s]*))?)?>[\s\S]*?<\/script>/gi; + const styleRegex = + /\s]*))?)?>[\s\S]*?<\/style>/gi; const scriptMatches = content.match(scriptRegex) || []; const styleMatches = content.match(styleRegex) || []; [...scriptMatches, ...styleMatches].forEach((match) => { @@ -23,7 +29,7 @@ export function transformSvelte(content: string, filePath: string, escapeTags: E if ( node.type === 'Element' && !isEscapeTags(escapeTags, node.name) && - !node?.attributes?.some((attr: any) => attr?.name === PathName) + !node.attributes?.some((attr: any) => attr?.name === PathName) ) { const insertPosition = node.start + node.name.length + 1; const line = countLines(content, node.start) + 1; diff --git a/packages/core/src/server/transform/transform-vue-pug.ts b/packages/core/src/server/transform/transform-vue-pug.ts index 7a0de5be..fa77ae7c 100644 --- a/packages/core/src/server/transform/transform-vue-pug.ts +++ b/packages/core/src/server/transform/transform-vue-pug.ts @@ -16,11 +16,10 @@ export interface PugFileInfo { const AttributeNodeType = 6; export const pugMap = new Map(); // 使用了 pug 模板的 vue 文件集合 -/* v8 ignore next 14 -- defensive checks: column undefined branches rarely triggered by Pug parser */ -function belongTemplate( +export function belongTemplate( target: AstLocation, start: AstLocation, - end: AstLocation + end: AstLocation, ) { return ( (target.line > start.line && target.line < end.line) || @@ -32,16 +31,15 @@ function belongTemplate( } interface TransformPugParams { - node: pug.Node; + node: pug.Node | null | undefined; templateNode: ElementNode; s: MagicString; escapeTags: EscapeTags; filePath: string; } -function transformPugAst(params: TransformPugParams) { +export function transformPugAst(params: TransformPugParams) { const { node, templateNode, escapeTags, s, filePath } = params; - /* v8 ignore next 3 -- defensive guard: node.block can be null for empty Pug elements */ if (!node) { return; } @@ -58,7 +56,7 @@ function transformPugAst(params: TransformPugParams) { const belongToTemplate = belongTemplate( nodeLocation, templateNode.loc.start, - templateNode.loc.end + templateNode.loc.end, ); if ( belongToTemplate && @@ -77,8 +75,10 @@ function transformPugAst(params: TransformPugParams) { const attr = node.attrs[i]; if (['class', 'id'].includes(attr.name) && !attr.mustEscape) { insertPosition = - // @ts-expect-error - Pug attr type not typed - offsets[attr.line + lineOffset - 1] + attr.column + (attr.val.length - 2); + offsets[attr.line + lineOffset - 1] + + attr.column + + // @ts-expect-error - attr.val is not typed + (attr.val.length - 2); } } } @@ -94,14 +94,12 @@ function transformPugAst(params: TransformPugParams) { transformPugAst({ ...params, node: node.block }); } else if (['Case', 'Code', 'When', 'Each', 'While'].includes(node.type)) { if ((node as pug.MixinNode).block) { - /* v8 ignore next 3 -- defensive fallback: nodes array always exists when block exists */ ((node as pug.MixinNode).block?.nodes || []).forEach((childNode) => { transformPugAst({ ...params, node: childNode }); }); } // @ts-expect-error - Pug Conditional type not exported } else if (node.type === 'Conditional') { - /* v8 ignore next 7 -- defensive fallbacks: Pug Conditional nodes always have consequent/alternate structure */ // @ts-expect-error - Pug Conditional consequent/alternate properties not typed (node.consequent?.nodes || []).forEach((childNode) => { transformPugAst({ ...params, node: childNode }); @@ -122,12 +120,11 @@ export function isPugTemplate(templateNode: ElementNode | undefined): boolean { if (!templateNode) { return false; } - /* v8 ignore next 9 -- defensive fallback: templateNode.props is always an array in valid Vue SFCs */ return (templateNode.props || []).some( (prop) => prop.type === AttributeNodeType && prop.name === 'lang' && - prop.value?.content === 'pug' + prop.value?.content === 'pug', ); } @@ -159,7 +156,7 @@ export function transformPugTemplate( filePath: string, templateNode: ElementNode, escapeTags: EscapeTags, - s: MagicString + s: MagicString, ): void { // Calculate and store line offsets const offsets = calculateLineOffsets(content); @@ -168,10 +165,7 @@ export function transformPugTemplate( // Create temporary content with template section preserved const tempContent = ' '.repeat(templateNode.loc.start.offset - 0) + - content.slice( - templateNode.loc.start.offset, - templateNode.loc.end.offset - ) + + content.slice(templateNode.loc.start.offset, templateNode.loc.end.offset) + ' '.repeat(content.length - templateNode.loc.end.offset); // Parse and transform Pug AST diff --git a/packages/core/src/server/use-client.ts b/packages/core/src/server/use-client.ts index 13af9e33..b234d4cf 100644 --- a/packages/core/src/server/use-client.ts +++ b/packages/core/src/server/use-client.ts @@ -1,7 +1,7 @@ -/* v8 ignore next -- import branch coverage artifact */ -import path, { isAbsolute, dirname } from 'path'; +import path from 'path'; import fs from 'fs'; import chalk from 'chalk'; +import { fileURLToPath } from 'url'; import MagicString from 'magic-string'; // @ts-ignore import { parse, traverse } from '@babel/core'; @@ -17,7 +17,6 @@ import { PathName, isJsTypeFile, getFilePathWithoutExt, - fileURLToPath, AstroToolbarFile, getIP, getDependencies, @@ -28,14 +27,7 @@ import { hasWritePermission, } from '../shared'; -let compatibleDirname = ''; - -if (typeof __dirname !== 'undefined') { - compatibleDirname = __dirname; - /* v8 ignore next 3 -- ESM fallback: only runs in native ESM without bundler __dirname shim */ -} else { - compatibleDirname = dirname(fileURLToPath(import.meta.url)); -} +const compatibleDirname = path.dirname(fileURLToPath(import.meta.url)); // 这个路径是根据打包后来的 export const clientJsPath = path.resolve(compatibleDirname, './client.umd.js'); @@ -259,14 +251,14 @@ function recordEntry(record: RecordInfo, file: string, isNextjs: boolean) { // target file to inject code async function isTargetFileToInject(file: string, record: RecordInfo) { - const inputs: string[] = await (record?.inputs || []); + const inputs: string[] = await (record.inputs || []); + const recordInfo = getProjectRecord(record); + const normalizedFile = normalizePath(file); return ( - (isJsTypeFile(file) && - getFilePathWithoutExt(file) === getProjectRecord(record)?.entry) || + (isJsTypeFile(file) && getFilePathWithoutExt(file) === recordInfo?.entry) || file === AstroToolbarFile || - /* v8 ignore next -- optional chaining fallback */ - getProjectRecord(record)?.injectTo?.includes(normalizePath(file)) || - inputs?.includes(normalizePath(file)) + (recordInfo?.injectTo || []).includes(normalizedFile) || + inputs.includes(normalizedFile) ); } @@ -276,7 +268,7 @@ function recordInjectTo(record: RecordInfo, options: CodeOptions) { ? options.injectTo : [options.injectTo]; injectTo.forEach((injectToPath) => { - if (!isAbsolute(injectToPath)) { + if (!path.isAbsolute(injectToPath)) { const info = [ chalk.cyan('injectTo'), chalk.red('in'), @@ -298,8 +290,7 @@ function recordInjectTo(record: RecordInfo, options: CodeOptions) { setProjectRecord( record, 'injectTo', - /* v8 ignore next -- injectTo is always defined here due to if check above */ - (injectTo || []).map((file) => normalizePath(file)), + injectTo.map((file) => normalizePath(file)), ); } } @@ -351,7 +342,6 @@ export async function getCodeWithWebComponent({ const webComponentFilePath = writeWebComponentFile( record.output, injectCode, - /* v8 ignore next -- port is always set before reaching this code path */ getProjectRecord(record)?.port || 0, ); if (!file.match(webComponentFilePath)) { diff --git a/packages/core/src/shared/utils.ts b/packages/core/src/shared/utils.ts index b6977382..7c5c9b3c 100644 --- a/packages/core/src/shared/utils.ts +++ b/packages/core/src/shared/utils.ts @@ -29,20 +29,6 @@ export function getIP(ip: boolean | string) { return 'localhost'; } -// 将 import.meta.url 转换为 __dirname: 兼容 mac linux 和 windows -export function fileURLToPath(fileURL: string) { - let filePath = fileURL; - if (process.platform === 'win32') { - filePath = filePath.replace(/^file:\/\/\//, ''); - filePath = decodeURIComponent(filePath); - filePath = filePath.replace(/\//g, '\\'); - } else { - filePath = filePath.replace(/^file:\/\//, ''); - filePath = decodeURIComponent(filePath); - } - return filePath; -} - // 是否为 JS 类型的文件 export function isJsTypeFile(file: string) { return JsFileExtList.some((ext) => file.endsWith(ext)); @@ -78,7 +64,7 @@ export function getDependenciesMap() { const packageJsonPath = path.resolve(process.cwd(), './package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse( - fs.readFileSync(packageJsonPath, 'utf-8') || '{}' + fs.readFileSync(packageJsonPath, 'utf-8') || '{}', ); const dependencies = { ...packageJson.dependencies, @@ -126,7 +112,7 @@ type BooleanFunction = () => boolean; */ export function isDev( userDev: boolean | BooleanFunction | undefined, - systemDev: boolean + systemDev: boolean, ) { let dev: boolean | undefined; if (typeof userDev === 'function') { @@ -209,7 +195,7 @@ export function getMappingFilePath( file: string, mappings?: | Record - | Array<{ find: string | RegExp; replacement: string }> + | Array<{ find: string | RegExp; replacement: string }>, ): string { if (!mappings) { return file; @@ -245,7 +231,7 @@ export function getMappingFilePath( function replaceFileWithString( file: string, find: string, - replacement: string + replacement: string, ): string | null { find = handlePathWithSlash(find); replacement = handlePathWithSlash(replacement); @@ -270,7 +256,7 @@ function replaceFileWithString( function replaceFileWithRegExp( file: string, find: RegExp, - replacement: string + replacement: string, ): string | null { const match = find.exec(file); if (match) { diff --git a/packages/core/types/server/transform/transform-vue-pug.d.ts b/packages/core/types/server/transform/transform-vue-pug.d.ts index 77956beb..0c77621a 100644 --- a/packages/core/types/server/transform/transform-vue-pug.d.ts +++ b/packages/core/types/server/transform/transform-vue-pug.d.ts @@ -1,11 +1,25 @@ import MagicString from 'magic-string'; import { EscapeTags } from '../../shared'; import type { ElementNode } from '@vue/compiler-dom'; +import * as pug from 'volar-service-pug/lib/languageService'; +interface AstLocation { + column: number; + line: number; +} export interface PugFileInfo { content: string; offsets: number[]; } export declare const pugMap: Map; +export declare function belongTemplate(target: AstLocation, start: AstLocation, end: AstLocation): boolean; +interface TransformPugParams { + node: pug.Node | null | undefined; + templateNode: ElementNode; + s: MagicString; + escapeTags: EscapeTags; + filePath: string; +} +export declare function transformPugAst(params: TransformPugParams): void; /** * Check if a template node uses Pug syntax * @param templateNode - The template element node to check @@ -27,3 +41,4 @@ export declare function calculateLineOffsets(content: string): number[]; * @param s - MagicString instance for code transformation */ export declare function transformPugTemplate(content: string, filePath: string, templateNode: ElementNode, escapeTags: EscapeTags, s: MagicString): void; +export {}; diff --git a/packages/core/types/shared/utils.d.ts b/packages/core/types/shared/utils.d.ts index e4173046..9f9718f3 100644 --- a/packages/core/types/shared/utils.d.ts +++ b/packages/core/types/shared/utils.d.ts @@ -1,6 +1,5 @@ import { CodeOptions, Condition, EscapeTags } from './type'; export declare function getIP(ip: boolean | string): string; -export declare function fileURLToPath(fileURL: string): string; export declare function isJsTypeFile(file: string): boolean; export declare function getFilePathWithoutExt(filePath: string): string; export declare function normalizePath(filepath: string): string; diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts index 429531f5..d0332164 100644 --- a/packages/core/vite.config.ts +++ b/packages/core/vite.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ 'https', 'net', 'chalk', + 'url', 'launch-ide', 'portfinder', 'child_process', diff --git a/packages/mako/src/index.ts b/packages/mako/src/index.ts index a114de99..a0401db5 100644 --- a/packages/mako/src/index.ts +++ b/packages/mako/src/index.ts @@ -42,8 +42,7 @@ export function MakoCodeInspectorPlugin(options: Options): Record { if (isExcludedFile(id, options) || id.includes('/.umi/')) { return; } - /* v8 ignore next -- defensive fallback for undefined options */ - const { escapeTags = [], mappings, match } = options || {}; + const { escapeTags = [], mappings, match } = options; if (match && !match.test(id)) { return; } diff --git a/packages/turbopack/src/index.ts b/packages/turbopack/src/index.ts index 30d40f5e..9751ad07 100644 --- a/packages/turbopack/src/index.ts +++ b/packages/turbopack/src/index.ts @@ -12,8 +12,24 @@ interface Options extends CodeOptions { output: string; } +export function resolveWebpackEntry(params: { + requireResolve?: (id: string) => string; + importMetaResolve?: (id: string) => string | Promise; +}) { + if (typeof params.importMetaResolve === 'function') { + const resolved = params.importMetaResolve( + '@code-inspector/webpack', + ) as unknown as string; + return fileURLToPath(resolved); + } + + return typeof params.requireResolve === 'function' + ? params.requireResolve('@code-inspector/webpack') + : null; +} + export function TurbopackCodeInspectorPlugin( - options: Options + options: Options, ): Record { if ( options.close || @@ -28,17 +44,10 @@ export function TurbopackCodeInspectorPlugin( output: options.output, }; - let WebpackEntry = null; - if (typeof require !== 'undefined' && typeof require.resolve === 'function') { - WebpackEntry = require.resolve('@code-inspector/webpack'); - } - /* v8 ignore next 6 -- ESM import.meta.resolve branch not available in CJS test environment */ - if (typeof import.meta.resolve === 'function') { - const dir = import.meta.resolve( - '@code-inspector/webpack' - ) as unknown as string; - WebpackEntry = fileURLToPath(dir); - } + const WebpackEntry = resolveWebpackEntry({ + requireResolve: globalThis.require?.resolve, + importMetaResolve: import.meta.resolve, + }); const WebpackDistDir = path.resolve(WebpackEntry, '..'); // according to: https://nextjs.org/docs/app/getting-started/project-structure#routing-files diff --git a/packages/turbopack/types/index.d.ts b/packages/turbopack/types/index.d.ts index b990ab0d..647c4769 100644 --- a/packages/turbopack/types/index.d.ts +++ b/packages/turbopack/types/index.d.ts @@ -3,5 +3,9 @@ interface Options extends CodeOptions { close?: boolean; output: string; } +export declare function resolveWebpackEntry(params: { + requireResolve?: (id: string) => string; + importMetaResolve?: (id: string) => string | Promise; +}): string; export declare function TurbopackCodeInspectorPlugin(options: Options): Record; export {}; diff --git a/packages/webpack/src/index.ts b/packages/webpack/src/index.ts index cb408feb..a9fe03ff 100644 --- a/packages/webpack/src/index.ts +++ b/packages/webpack/src/index.ts @@ -1,49 +1,50 @@ import { CodeOptions, RecordInfo, - fileURLToPath, getCodeWithWebComponent, getProjectRecord, isDev, isNextjsProject, } from '@code-inspector/core'; -/* v8 ignore next -- import statement branch coverage not testable */ -import path, { dirname } from 'path'; +import path from 'path'; +import { fileURLToPath } from 'url'; import { getWebpackEntrys } from './entry'; -let compatibleDirname = ''; +const compatibleDirname = path.dirname(fileURLToPath(import.meta.url)); -/* v8 ignore next 5 -- environment-specific: __dirname available in CJS, import.meta.url in ESM */ -if (typeof __dirname !== 'undefined') { - compatibleDirname = __dirname; -} else { - compatibleDirname = dirname(fileURLToPath(import.meta.url)); +interface LoaderOptions extends CodeOptions { + record: RecordInfo; } -let isFirstLoad = true; +const baseLoaderPath = path.resolve(compatibleDirname, './loader.js'); +const injectLoaderPath = path.resolve(compatibleDirname, './inject-loader.js'); -interface LoaderOptions extends CodeOptions { - record: RecordInfo; +function hasRegisteredCodeInspectorLoader(rules: any[]) { + return rules.some((rule) => + rule?.use?.some?.( + (item: any) => + item?.loader === baseLoaderPath || item?.loader === injectLoaderPath, + ), + ); } const applyLoader = (options: LoaderOptions, compiler: any) => { - /* v8 ignore next 3 -- guard prevents duplicate loader registration */ - if (!isFirstLoad) { - return; - } - isFirstLoad = false; // 适配 webpack 各个版本 const _compiler = compiler?.compiler || compiler; const module = _compiler?.options?.module; - /* v8 ignore next -- fallback for legacy webpack versions with module.loaders */ const rules = module?.rules || module?.loaders || []; + + if (hasRegisteredCodeInspectorLoader(rules)) { + return; + } + rules.push( { test: options.match ?? /\.html$/, resourceQuery: /vue/, use: [ { - loader: path.resolve(compatibleDirname, `./loader.js`), + loader: baseLoaderPath, options, }, ], @@ -53,7 +54,7 @@ const applyLoader = (options: LoaderOptions, compiler: any) => { test: /\.(vue|jsx|tsx|js|ts|mjs|mts|svelte)$/, use: [ { - loader: path.resolve(compatibleDirname, `./loader.js`), + loader: baseLoaderPath, options, }, ], @@ -68,12 +69,12 @@ const applyLoader = (options: LoaderOptions, compiler: any) => { }), use: [ { - loader: path.resolve(compatibleDirname, `./inject-loader.js`), + loader: injectLoaderPath, options, }, ], enforce: isNextjsProject() ? 'pre' : 'post', - } + }, ); }; @@ -85,7 +86,7 @@ interface Options extends CodeOptions { function getPureClientCodeString( options: Options, record: RecordInfo, - server?: boolean + server?: boolean, ): Promise { return getCodeWithWebComponent({ options: { ...options, importClient: 'code' }, @@ -114,7 +115,7 @@ async function replaceHtml({ if (typeof source === 'string') { const sourceCode = source.replace( '', - `` + ``, ); assets[filename] = { source: () => sourceCode, @@ -133,14 +134,12 @@ class WebpackCodeInspectorPlugin { } async apply(compiler) { - isFirstLoad = true; - if ( this.options.close || !isDev( this.options.dev, compiler?.options?.mode === 'development' || - process.env.NODE_ENV === 'development' + process.env.NODE_ENV === 'development', ) ) { return; @@ -152,7 +151,7 @@ class WebpackCodeInspectorPlugin { output: this.options.output, inputs: getWebpackEntrys( compiler?.options?.entry, - compiler?.options?.context + compiler?.options?.context, ), envDir: compiler?.options?.context, root: compiler?.options?.context, @@ -195,7 +194,7 @@ class WebpackCodeInspectorPlugin { assets, }); cb(); - } + }, ); } } diff --git a/packages/webpack/src/loader.ts b/packages/webpack/src/loader.ts index a65a7493..7ea13d72 100644 --- a/packages/webpack/src/loader.ts +++ b/packages/webpack/src/loader.ts @@ -13,9 +13,8 @@ export default async function WebpackCodeInspectorLoader(content: string) { this.cacheable && this.cacheable(true); let filePath = normalizePath(this.resourcePath); // 当前文件的绝对路径 let params = new URLSearchParams(this.resource.split('?')?.[1] || ''); - const options = this.query; - /* v8 ignore next -- defensive fallback for undefined options */ - let { escapeTags = [], mappings } = options || {}; + const options = this.query || {}; + let { escapeTags = [], mappings } = options; if (isExcludedFile(filePath, options)) { return content; diff --git a/packages/webpack/vite.config.ts b/packages/webpack/vite.config.ts index d24be9f4..8ce47f1c 100644 --- a/packages/webpack/vite.config.ts +++ b/packages/webpack/vite.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ minify: true, emptyOutDir: false, rollupOptions: { - external: ['@code-inspector/core', '@vue/compiler-sfc', 'path'], + external: ['@code-inspector/core', '@vue/compiler-sfc', 'path', 'url'], output: { exports: 'default', // 设置默认导出 }, diff --git a/test/code-inspector-plugin/index.test.ts b/test/code-inspector-plugin/index.test.ts index 5dc0c835..f91dd0d2 100644 --- a/test/code-inspector-plugin/index.test.ts +++ b/test/code-inspector-plugin/index.test.ts @@ -145,6 +145,14 @@ describe('CodeInspectorPlugin', () => { CodeInspectorPlugin({ bundler: 'vite' }); expect(resetFileRecord).toHaveBeenCalled(); }); + + it('should pass the same output path to plugin and resetFileRecord', () => { + CodeInspectorPlugin({ bundler: 'vite' }); + + const pluginOptions = vi.mocked(ViteCodeInspectorPlugin).mock.calls[0]?.[0]; + expect(pluginOptions?.output).toBeDefined(); + expect(resetFileRecord).toHaveBeenCalledWith(pluginOptions?.output); + }); }); describe('export alias', () => { diff --git a/test/core/server/server/create-server.test.ts b/test/core/server/server/create-server.test.ts index 930a2141..e4dd0a72 100644 --- a/test/core/server/server/create-server.test.ts +++ b/test/core/server/server/create-server.test.ts @@ -1,47 +1,49 @@ import { expect, describe, it, vi, beforeEach, afterEach } from 'vitest'; import http from 'http'; - -// Store reference to portfinder mock for testing error cases -const mockPortfinderGetPort = vi.hoisted(() => vi.fn((options: any, callback: any) => { - callback(null, options?.port || 5678); -})); - -vi.mock('http'); -vi.mock('portfinder', () => ({ - default: { - getPort: mockPortfinderGetPort, - }, - getPort: mockPortfinderGetPort, -})); -vi.mock('launch-ide', () => ({ - launchIDE: vi.fn(), -})); - -import { createServer, ProjectRootPath, getRelativePath, getRelativeOrAbsolutePath } from '@/core/src/server/server'; -import { launchIDE } from 'launch-ide'; +import path from 'path'; +import { createRequire } from 'module'; + +const mockHttpCreateServer = vi.hoisted(() => vi.fn()); +const mockPortfinderGetPort = vi.hoisted(() => vi.fn()); +const requireFromCore = createRequire( + path.resolve(process.cwd(), 'packages/core/package.json'), +); +const corePortFinder = requireFromCore('portfinder') as { + getPort: (...args: any[]) => unknown; +}; + +const loadServerModule = async () => { + return import('@/core/src/server/server'); +}; describe('createServer', () => { + let serverModule: Awaited; let mockServer: any; let requestHandler: Function; - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); vi.clearAllMocks(); - // Reset portfinder mock to default implementation - mockPortfinderGetPort.mockImplementation((options: any, callback: any) => { - callback(null, options?.port || 5678); - }); - mockServer = { listen: vi.fn((port: number, callback: Function) => { callback(); }), }; - vi.mocked(http.createServer).mockImplementation((handler: any) => { + mockHttpCreateServer.mockImplementation((handler: any) => { requestHandler = handler; return mockServer as any; }); + mockPortfinderGetPort.mockImplementation((options: any, callback: any) => { + callback(null, options?.port || 5678); + }); + vi.spyOn(http, 'createServer').mockImplementation(mockHttpCreateServer as any); + vi.spyOn(corePortFinder, 'getPort').mockImplementation( + mockPortfinderGetPort as any, + ); + + serverModule = await loadServerModule(); }); afterEach(() => { @@ -50,20 +52,39 @@ describe('createServer', () => { it('should create an HTTP server', () => { const callback = vi.fn(); - createServer(callback); - expect(http.createServer).toHaveBeenCalled(); + + serverModule.createServer(callback); + + expect(mockHttpCreateServer).toHaveBeenCalled(); + expect(requestHandler).toBeInstanceOf(Function); }); it('should return the server instance', () => { const callback = vi.fn(); - const result = createServer(callback); + + const result = serverModule.createServer(callback); + expect(result).toBe(mockServer); }); + it('should throw when getPort returns an error', () => { + const callback = vi.fn(); + mockPortfinderGetPort.mockImplementationOnce((options: any, portCallback: any) => { + portCallback(new Error('port failed')); + }); + + expect(() => serverModule.createServer(callback)).toThrow('port failed'); + }); + describe('request handling', () => { it('should handle request with file, line, and column parameters', () => { - const callback = vi.fn(); - createServer(callback); + const afterInspectRequest = vi.fn(); + serverModule.createServer(vi.fn(), { + bundler: 'vite', + hooks: { + afterInspectRequest, + }, + }); const mockReq = { url: '?file=%2Ftest%2Ffile.ts&line=10&column=5', @@ -82,12 +103,24 @@ describe('createServer', () => { 'Access-Control-Allow-Private-Network': 'true', }); expect(mockRes.end).toHaveBeenCalledWith('ok'); - expect(launchIDE).toHaveBeenCalled(); + expect(afterInspectRequest).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + file: '/test/file.ts', + line: 10, + column: 5, + }), + ); }); it('should prepend ProjectRootPath to relative file paths', () => { - const callback = vi.fn(); - createServer(callback); + const afterInspectRequest = vi.fn(); + serverModule.createServer(vi.fn(), { + bundler: 'vite', + hooks: { + afterInspectRequest, + }, + }); const mockReq = { url: '?file=src%2Ffile.ts&line=1&column=1', @@ -99,18 +132,21 @@ describe('createServer', () => { requestHandler(mockReq, mockRes); - if (ProjectRootPath) { - expect(launchIDE).toHaveBeenCalledWith( + if (serverModule.ProjectRootPath) { + expect(afterInspectRequest).toHaveBeenCalledWith( + expect.any(Object), expect.objectContaining({ - file: `${ProjectRootPath}/src/file.ts`, + file: `${serverModule.ProjectRootPath}/src/file.ts`, }) ); } }); - it('should return 403 for file outside ProjectRootPath with relative pathType', async () => { - const callback = vi.fn(); - createServer(callback, { pathType: 'relative', bundler: 'vite' }); + it('should return 403 for file outside ProjectRootPath with relative pathType', () => { + serverModule.createServer(vi.fn(), { + pathType: 'relative', + bundler: 'vite', + }); const mockReq = { url: '?file=%2Fetc%2Fpasswd&line=1&column=1', @@ -122,7 +158,7 @@ describe('createServer', () => { requestHandler(mockReq, mockRes); - if (ProjectRootPath) { + if (serverModule.ProjectRootPath) { expect(mockRes.writeHead).toHaveBeenCalledWith(403, expect.any(Object)); expect(mockRes.end).toHaveBeenCalledWith('not allowed to open this file'); } @@ -136,8 +172,8 @@ describe('createServer', () => { afterInspectRequest, }, }; - const callback = vi.fn(); - createServer(callback, options); + + serverModule.createServer(vi.fn(), options); const mockReq = { url: '?file=%2Ftest%2Ffile.ts&line=10&column=5', @@ -157,15 +193,26 @@ describe('createServer', () => { }); it('should pass editor and openIn options to launchIDE', () => { - const callback = vi.fn(); - const options = { - bundler: 'vite' as const, - editor: 'code' as const, - openIn: 'new' as const, - pathFormat: '{file}:{line}', - launchType: 'open' as const, - }; - createServer(callback, options, { output: '/test', port: 0, entry: '', envDir: '/project' }); + const afterInspectRequest = vi.fn(); + serverModule.createServer( + vi.fn(), + { + bundler: 'vite', + editor: 'code', + openIn: 'new', + pathFormat: '{file}:{line}', + launchType: 'open', + hooks: { + afterInspectRequest, + }, + }, + { + output: '/test', + port: 0, + entry: '', + envDir: '/project', + }, + ); const mockReq = { url: '?file=%2Ftest%2Ffile.ts&line=10&column=5', @@ -177,20 +224,30 @@ describe('createServer', () => { requestHandler(mockReq, mockRes); - expect(launchIDE).toHaveBeenCalledWith( + expect(afterInspectRequest).toHaveBeenCalledWith( expect.objectContaining({ + bundler: 'vite', editor: 'code', - method: 'new', - format: '{file}:{line}', - type: 'open', - rootDir: '/project', - }) + openIn: 'new', + pathFormat: '{file}:{line}', + launchType: 'open', + }), + expect.objectContaining({ + file: '/test/file.ts', + line: 10, + column: 5, + }), ); }); it('should decode URL-encoded file paths correctly', () => { - const callback = vi.fn(); - createServer(callback); + const afterInspectRequest = vi.fn(); + serverModule.createServer(vi.fn(), { + bundler: 'vite', + hooks: { + afterInspectRequest, + }, + }); const mockReq = { url: '?file=%2Fpath%2Fwith%20spaces%2Ffile.ts&line=5&column=10', @@ -202,7 +259,8 @@ describe('createServer', () => { requestHandler(mockReq, mockRes); - expect(launchIDE).toHaveBeenCalledWith( + expect(afterInspectRequest).toHaveBeenCalledWith( + expect.any(Object), expect.objectContaining({ file: expect.stringContaining('/path/with spaces/file.ts'), line: 5, @@ -212,8 +270,13 @@ describe('createServer', () => { }); it('should handle missing line and column parameters', () => { - const callback = vi.fn(); - createServer(callback); + const afterInspectRequest = vi.fn(); + serverModule.createServer(vi.fn(), { + bundler: 'vite', + hooks: { + afterInspectRequest, + }, + }); const mockReq = { url: '?file=%2Ftest%2Ffile.ts', @@ -225,8 +288,8 @@ describe('createServer', () => { requestHandler(mockReq, mockRes); - // When line/column are missing, Number(null) returns 0 - expect(launchIDE).toHaveBeenCalledWith( + expect(afterInspectRequest).toHaveBeenCalledWith( + expect.any(Object), expect.objectContaining({ line: 0, column: 0, @@ -236,65 +299,77 @@ describe('createServer', () => { }); }); -describe('getRelativePath', () => { +describe('server path helpers', () => { + let serverModule: Awaited; + + beforeEach(async () => { + vi.clearAllMocks(); + serverModule = await loadServerModule(); + }); + it('should return relative path when ProjectRootPath is set', () => { - if (ProjectRootPath) { - const result = getRelativePath(`${ProjectRootPath}/src/file.ts`); - expect(result).toBe('src/file.ts'); + if (!serverModule.ProjectRootPath) { + return; } + + const result = serverModule.getRelativePath( + `${serverModule.ProjectRootPath}/src/file.ts`, + ); + + expect(result).toBe('src/file.ts'); }); it('should return original path when not under ProjectRootPath', () => { - const result = getRelativePath('/other/path/file.ts'); - expect(result).toBe('/other/path/file.ts'); + expect(serverModule.getRelativePath('/other/path/file.ts')).toBe( + '/other/path/file.ts', + ); }); -}); -describe('getRelativeOrAbsolutePath', () => { it('should return relative path when pathType is "relative"', () => { - if (ProjectRootPath) { - const result = getRelativeOrAbsolutePath(`${ProjectRootPath}/src/file.ts`, 'relative'); - expect(result).toBe('src/file.ts'); + if (!serverModule.ProjectRootPath) { + return; } + + const result = serverModule.getRelativeOrAbsolutePath( + `${serverModule.ProjectRootPath}/src/file.ts`, + 'relative', + ); + + expect(result).toBe('src/file.ts'); }); it('should return absolute path when pathType is "absolute"', () => { - const result = getRelativeOrAbsolutePath('/test/file.ts', 'absolute'); - expect(result).toBe('/test/file.ts'); + expect( + serverModule.getRelativeOrAbsolutePath('/test/file.ts', 'absolute'), + ).toBe('/test/file.ts'); }); it('should return absolute path when pathType is undefined', () => { - const result = getRelativeOrAbsolutePath('/test/file.ts', undefined); - expect(result).toBe('/test/file.ts'); + expect( + serverModule.getRelativeOrAbsolutePath('/test/file.ts', undefined), + ).toBe('/test/file.ts'); }); -}); -describe('ProjectRootPath (getProjectRoot)', () => { - it('should be a string', () => { - expect(typeof ProjectRootPath).toBe('string'); + it('should expose ProjectRootPath as a string', () => { + expect(typeof serverModule.ProjectRootPath).toBe('string'); }); - it('should be a valid git root or empty string', () => { - if (ProjectRootPath) { - // If it's set, it should be an absolute path - expect(ProjectRootPath.startsWith('/')).toBe(true); - } else { - expect(ProjectRootPath).toBe(''); + it('should expose a valid git root or empty string', () => { + if (serverModule.ProjectRootPath) { + expect(serverModule.ProjectRootPath.startsWith('/')).toBe(true); + return; } + + expect(serverModule.ProjectRootPath).toBe(''); }); -}); -describe('getRelativePath edge cases', () => { - it('should return original path when file is not under ProjectRootPath', () => { - // This tests the case where filePath doesn't start with ProjectRootPath - const result = getRelativePath('/completely/different/path/file.ts'); - // If ProjectRootPath is set, the path won't be modified since it doesn't contain ProjectRootPath - // If ProjectRootPath is empty, it returns the original path - expect(result).toBe('/completely/different/path/file.ts'); + it('should return original path when file is outside ProjectRootPath', () => { + expect(serverModule.getRelativePath('/completely/different/path/file.ts')).toBe( + '/completely/different/path/file.ts', + ); }); it('should handle empty file path', () => { - const result = getRelativePath(''); - expect(result).toBe(''); + expect(serverModule.getRelativePath('')).toBe(''); }); }); diff --git a/test/core/server/server/project-root.test.ts b/test/core/server/server/project-root.test.ts new file mode 100644 index 00000000..6cd87770 --- /dev/null +++ b/test/core/server/server/project-root.test.ts @@ -0,0 +1,53 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +describe('server project root helpers', () => { + afterEach(() => { + vi.resetModules(); + vi.doUnmock('child_process'); + }); + + it('should derive relative path when git root is available', async () => { + vi.doMock('child_process', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + default: { + ...actual, + execSync: vi.fn(() => '/repo/root\n'), + }, + execSync: vi.fn(() => '/repo/root\n'), + }; + }); + + const serverModule = await import('@/core/src/server/server'); + + expect(serverModule.ProjectRootPath).toBe('/repo/root'); + expect(serverModule.getRelativePath('/repo/root/src/main.ts')).toBe( + 'src/main.ts', + ); + }); + + it('should fall back to empty project root outside git repo', async () => { + vi.doMock('child_process', async (importOriginal) => { + const actual = await importOriginal(); + const execSync = vi.fn(() => { + throw new Error('not a git repo'); + }); + return { + ...actual, + default: { + ...actual, + execSync, + }, + execSync, + }; + }); + + const serverModule = await import('@/core/src/server/server'); + + expect(serverModule.ProjectRootPath).toBe(''); + expect(serverModule.getRelativePath('/tmp/src/main.ts')).toBe( + '/tmp/src/main.ts', + ); + }); +}); diff --git a/test/core/server/server/start-server.test.ts b/test/core/server/server/start-server.test.ts index 6a2cf0c7..5a5bc270 100644 --- a/test/core/server/server/start-server.test.ts +++ b/test/core/server/server/start-server.test.ts @@ -4,66 +4,80 @@ import net from 'net'; import fs from 'fs'; import path from 'path'; import os from 'os'; +import { createRequire } from 'module'; import type { RecordInfo, CodeOptions } from '@/core/src/shared/type'; -vi.mock('http'); -vi.mock('net'); -vi.mock('portfinder', () => ({ - default: { - getPort: vi.fn((options: any, callback: any) => { - callback(null, options?.port || 5678); - }), - }, - getPort: vi.fn((options: any, callback: any) => { - callback(null, options?.port || 5678); - }), -})); -vi.mock('launch-ide', () => ({ - launchIDE: vi.fn(), -})); - -import { startServer } from '@/core/src/server/server'; -import { getProjectRecord, setProjectRecord, resetFileRecord } from '@/core/src/shared/record-cache'; +const mockHttpCreateServer = vi.hoisted(() => vi.fn()); +const mockNetCreateServer = vi.hoisted(() => vi.fn()); +const mockPortfinderGetPort = vi.hoisted(() => vi.fn()); +const requireFromCore = createRequire( + path.resolve(process.cwd(), 'packages/core/package.json'), +); +const corePortFinder = requireFromCore('portfinder') as { + getPort: (...args: any[]) => unknown; +}; describe('startServer', () => { + let serverModule: Awaited; + let recordCacheModule: Awaited; let testDir: string; let mockHttpServer: any; let mockNetServer: any; + let occupiedState: boolean; + let netListeners: Record; - beforeEach(() => { + beforeEach(async () => { + vi.resetModules(); vi.clearAllMocks(); - // Create a temporary test directory testDir = path.join(os.tmpdir(), `test-start-server-${Date.now()}`); fs.mkdirSync(testDir, { recursive: true }); - // Mock HTTP server mockHttpServer = { listen: vi.fn((port: number, callback: Function) => callback()), }; - vi.mocked(http.createServer).mockReturnValue(mockHttpServer as any); + mockHttpCreateServer.mockReturnValue(mockHttpServer as any); - // Mock net server for isPortOccupied + occupiedState = false; + netListeners = {}; mockNetServer = { unref: vi.fn(), - close: vi.fn(), - listen: vi.fn(), + close: vi.fn((callback?: Function) => callback?.()), + listen: vi.fn(() => { + setTimeout(() => { + if (occupiedState) { + netListeners.error?.(new Error('EADDRINUSE')); + return; + } + netListeners.listening?.(); + }, 0); + return mockNetServer; + }), on: vi.fn((event: string, callback: Function) => { - if (event === 'listening') { - // Port is available by default - setTimeout(() => callback(), 0); - } + netListeners[event] = callback; return mockNetServer; }), }; - vi.mocked(net.createServer).mockReturnValue(mockNetServer as any); + mockNetCreateServer.mockReturnValue(mockNetServer as any); + mockPortfinderGetPort.mockImplementation((options: any, callback: any) => { + callback(null, options?.port || 5678); + }); + vi.spyOn(http, 'createServer').mockImplementation( + mockHttpCreateServer as any, + ); + vi.spyOn(net, 'createServer').mockImplementation( + mockNetCreateServer as any, + ); + vi.spyOn(corePortFinder, 'getPort').mockImplementation( + mockPortfinderGetPort as any, + ); + + serverModule = await import('@/core/src/server/server'); + recordCacheModule = await import('@/core/src/shared/record-cache'); }); afterEach(() => { vi.restoreAllMocks(); - vi.spyOn(process, 'cwd').mockRestore(); - - // Clean up test directory try { if (fs.existsSync(testDir)) { fs.rmSync(testDir, { recursive: true, force: true }); @@ -73,211 +87,161 @@ describe('startServer', () => { } }); - describe('when previous port exists and is still running', () => { - it('should not restart server if port is occupied (already running)', async () => { - vi.spyOn(process, 'cwd').mockReturnValue('/test/project/running'); - - // Set up record with existing port - const record: RecordInfo = { - port: 0, - entry: '', - output: testDir, - }; - - // Create initial record with port - setProjectRecord(record, 'port', 8888); - - // Mock net server to indicate port is occupied (in use) - mockNetServer.on = vi.fn((event: string, callback: Function) => { - if (event === 'error') { - // Port is occupied (already started) - setTimeout(() => callback(), 0); - } - return mockNetServer; - }); - - const options: CodeOptions = { - bundler: 'vite', - }; - - await startServer(options, record); + it('should not restart server if previous port is occupied', async () => { + vi.spyOn(process, 'cwd').mockReturnValue('/test/project/running'); - // Should not create a new server since port is already running - // The server check happens through isPortOccupied - const projectRecord = getProjectRecord(record); - expect(projectRecord?.port).toBe(8888); - }); - }); - - describe('when previous port exists but is not running', () => { - it('should restart server when port is available (not occupied)', async () => { - vi.spyOn(process, 'cwd').mockReturnValue('/test/project/restart'); - - const record: RecordInfo = { - port: 0, - entry: '', - output: testDir, - }; - - // Set up record with existing port - setProjectRecord(record, 'port', 7777); - setProjectRecord(record, 'findPort', 1); - - // Mock net server to indicate port is available (not occupied) - mockNetServer.on = vi.fn((event: string, callback: Function) => { - if (event === 'listening') { - // Port is available (not occupied) - setTimeout(() => callback(), 0); - } - return mockNetServer; - }); + const record: RecordInfo = { + port: 0, + entry: '', + output: testDir, + }; + const options: CodeOptions = { + bundler: 'vite', + }; - const options: CodeOptions = { - bundler: 'vite', - }; + recordCacheModule.setProjectRecord(record, 'port', 8888); + occupiedState = true; - await startServer(options, record); + await serverModule.startServer(options, record); - // Should create a new server since port is not running - expect(http.createServer).toHaveBeenCalled(); - }); + expect(recordCacheModule.getProjectRecord(record)?.port).toBe(8888); + expect(mockHttpCreateServer).not.toHaveBeenCalled(); }); - describe('findPort behavior', () => { - it('should not restart server if findPort is already set and port exists', async () => { - vi.spyOn(process, 'cwd').mockReturnValue('/test/project/findport'); + it('should restart server when previous port is available', async () => { + vi.spyOn(process, 'cwd').mockReturnValue('/test/project/restart'); - const record: RecordInfo = { - port: 0, - entry: '', - output: testDir, - }; - - // Set findPort to indicate server is already being created - setProjectRecord(record, 'findPort', 1); - setProjectRecord(record, 'port', 5678); - - const options: CodeOptions = { - bundler: 'vite', - }; + const record: RecordInfo = { + port: 0, + entry: '', + output: testDir, + }; + const options: CodeOptions = { + bundler: 'vite', + }; - await startServer(options, record); + recordCacheModule.setProjectRecord(record, 'port', 7777); + recordCacheModule.setProjectRecord(record, 'findPort', 1); - // Server should not be created again, port should remain 5678 - const projectRecord = getProjectRecord(record); - expect(projectRecord?.port).toBe(5678); - }); + await serverModule.startServer(options, record); - it('should wait for port if not yet available', async () => { - vi.spyOn(process, 'cwd').mockReturnValue('/test/project/wait'); + expect(mockHttpCreateServer).toHaveBeenCalled(); + expect(recordCacheModule.getProjectRecord(record)?.port).toBe(5678); + }); - const record: RecordInfo = { - port: 0, - entry: '', - output: testDir, - }; + it('should not restart server if findPort is already set and port exists', async () => { + vi.spyOn(process, 'cwd').mockReturnValue('/test/project/findport'); - // Set findPort but not port - setProjectRecord(record, 'findPort', 1); + const record: RecordInfo = { + port: 0, + entry: '', + output: testDir, + }; + const options: CodeOptions = { + bundler: 'vite', + }; - const options: CodeOptions = { - bundler: 'vite', - }; + recordCacheModule.setProjectRecord(record, 'findPort', 1); + recordCacheModule.setProjectRecord(record, 'port', 5678); + occupiedState = true; - // Start server in background - const promise = startServer(options, record); + await serverModule.startServer(options, record); - // Set port after a delay - setTimeout(() => { - setProjectRecord(record, 'port', 9999); - }, 50); + expect(recordCacheModule.getProjectRecord(record)?.port).toBe(5678); + expect(mockHttpCreateServer).not.toHaveBeenCalled(); + }); - await promise; + it('should wait for port if findPort is set but port is not available yet', async () => { + vi.spyOn(process, 'cwd').mockReturnValue('/test/project/wait'); - const projectRecord = getProjectRecord(record); - expect(projectRecord?.port).toBe(9999); - }); - }); + const record: RecordInfo = { + port: 0, + entry: '', + output: testDir, + }; + const options: CodeOptions = { + bundler: 'vite', + }; - describe('printServer option', () => { - it('should print server info when printServer is true', async () => { - vi.spyOn(process, 'cwd').mockReturnValue('/test/project/print-server'); - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + recordCacheModule.setProjectRecord(record, 'findPort', 1); - const record: RecordInfo = { - port: 0, - entry: '', - output: testDir, - }; + const promise = serverModule.startServer(options, record); - resetFileRecord(testDir); + setTimeout(() => { + recordCacheModule.setProjectRecord(record, 'port', 9999); + }, 50); - const options: CodeOptions = { - bundler: 'vite', - printServer: true, - }; + await promise; - await startServer(options, record); + expect(recordCacheModule.getProjectRecord(record)?.port).toBe(9999); + }); - // Should have printed server info - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0]?.[0]; - expect(logCall).toContain('[code-inspector-plugin]'); + it('should print server info when printServer is true', async () => { + vi.spyOn(process, 'cwd').mockReturnValue('/test/project/print-server'); + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - consoleSpy.mockRestore(); - }); + const record: RecordInfo = { + port: 0, + entry: '', + output: testDir, + }; + const options: CodeOptions = { + bundler: 'vite', + printServer: true, + }; - it('should print server info with custom ip', async () => { - vi.spyOn(process, 'cwd').mockReturnValue('/test/project/print-server-ip'); - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + recordCacheModule.resetFileRecord(testDir); - const record: RecordInfo = { - port: 0, - entry: '', - output: testDir, - }; + await serverModule.startServer(options, record); - resetFileRecord(testDir); + expect(consoleSpy).toHaveBeenCalled(); + expect(consoleSpy.mock.calls[0]?.[0]).toContain('[code-inspector-plugin]'); + }); - const options: CodeOptions = { - bundler: 'vite', - printServer: true, - ip: '192.168.1.100', - }; + it('should print server info with custom ip', async () => { + vi.spyOn(process, 'cwd').mockReturnValue('/test/project/print-server-ip'); + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - await startServer(options, record); + const record: RecordInfo = { + port: 0, + entry: '', + output: testDir, + }; + const options: CodeOptions = { + bundler: 'vite', + printServer: true, + ip: '192.168.1.100', + }; - expect(consoleSpy).toHaveBeenCalled(); + recordCacheModule.resetFileRecord(testDir); - consoleSpy.mockRestore(); - }); + await serverModule.startServer(options, record); - it('should not print server info when printServer is false', async () => { - vi.spyOn(process, 'cwd').mockReturnValue('/test/project/no-print'); - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + expect(consoleSpy).toHaveBeenCalled(); + }); - const record: RecordInfo = { - port: 0, - entry: '', - output: testDir, - }; + it('should not print server info when printServer is false', async () => { + vi.spyOn(process, 'cwd').mockReturnValue('/test/project/no-print'); + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - resetFileRecord(testDir); + const record: RecordInfo = { + port: 0, + entry: '', + output: testDir, + }; + const options: CodeOptions = { + bundler: 'vite', + printServer: false, + }; - const options: CodeOptions = { - bundler: 'vite', - printServer: false, - }; + recordCacheModule.resetFileRecord(testDir); - await startServer(options, record); + await serverModule.startServer(options, record); - // Should not print server info (console.log might be called for other reasons) - const serverInfoCalls = consoleSpy.mock.calls.filter( - call => call[0]?.includes?.('[code-inspector-plugin]') - ); - expect(serverInfoCalls.length).toBe(0); + const serverInfoCalls = consoleSpy.mock.calls.filter( + (call) => call[0]?.includes?.('[code-inspector-plugin]'), + ); - consoleSpy.mockRestore(); - }); + expect(serverInfoCalls.length).toBe(0); }); }); diff --git a/test/core/server/transform/transform-vue-pug.test.ts b/test/core/server/transform/transform-vue-pug.test.ts new file mode 100644 index 00000000..6c498d59 --- /dev/null +++ b/test/core/server/transform/transform-vue-pug.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; +import { + belongTemplate, + isPugTemplate, + transformPugAst, +} from '@/core/src/server/transform/transform-vue-pug'; + +const templateNode = { + loc: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 4, column: 1, offset: 20 }, + }, + props: [], +} as any; + +describe('transform-vue-pug', () => { + describe('belongTemplate', () => { + it('should treat undefined column on start line as inside template', () => { + expect( + belongTemplate( + { line: 1, column: undefined }, + { line: 1, column: 3 }, + { line: 3, column: 8 }, + ), + ).toBe(true); + }); + + it('should treat undefined column on end line as inside template', () => { + expect( + belongTemplate( + { line: 3, column: undefined }, + { line: 1, column: 3 }, + { line: 3, column: 8 }, + ), + ).toBe(true); + }); + }); + + describe('transformPugAst', () => { + it('should ignore empty nodes', () => { + const s = { + toString: () => 'div Hello', + } as any; + + expect(() => + transformPugAst({ + node: undefined, + templateNode, + s, + escapeTags: [], + filePath: 'test.vue', + }), + ).not.toThrow(); + expect(s.toString()).toBe('div Hello'); + }); + + it('should handle block nodes without child nodes array', () => { + const s = {} as any; + + expect(() => + transformPugAst({ + node: { + type: 'Each', + block: {}, + } as any, + templateNode, + s, + escapeTags: [], + filePath: 'test.vue', + }), + ).not.toThrow(); + }); + + it('should handle conditionals without consequent or alternate blocks', () => { + const s = {} as any; + + expect(() => + transformPugAst({ + node: { + type: 'Conditional', + } as any, + templateNode, + s, + escapeTags: [], + filePath: 'test.vue', + }), + ).not.toThrow(); + }); + }); + + describe('isPugTemplate', () => { + it('should return false when template props are missing', () => { + expect( + isPugTemplate({ + ...templateNode, + props: undefined, + }), + ).toBe(false); + }); + }); +}); diff --git a/test/core/server/transform/transform-vue.test.ts b/test/core/server/transform/transform-vue.test.ts index be4d854c..069afba2 100644 --- a/test/core/server/transform/transform-vue.test.ts +++ b/test/core/server/transform/transform-vue.test.ts @@ -684,5 +684,23 @@ const count = ref(0); expect(compilerDom.parse).toBe(parse); expect(compilerDom.transform).toBe(transform); }); + + it('should throw when parse export is missing', () => { + expect(() => + resolveVueCompilerDom({ + default: { + transform: vi.fn(), + }, + }), + ).toThrowError('Failed to load @vue/compiler-dom parse/transform exports'); + }); + + it('should throw when transform export is missing', () => { + expect(() => + resolveVueCompilerDom({ + parse: vi.fn(), + }), + ).toThrowError('Failed to load @vue/compiler-dom parse/transform exports'); + }); }); }); diff --git a/test/core/server/use-client/get-code-with-web-component.test.ts b/test/core/server/use-client/get-code-with-web-component.test.ts index 70f56d36..c5172633 100644 --- a/test/core/server/use-client/get-code-with-web-component.test.ts +++ b/test/core/server/use-client/get-code-with-web-component.test.ts @@ -2,9 +2,10 @@ import { expect, describe, it, vi, beforeEach, afterEach } from 'vitest'; import fs from 'fs'; import path from 'path'; import os from 'os'; -import http from 'http'; import type { RecordInfo, CodeOptions } from '@/core/src/shared/type'; +const mockStartServer = vi.hoisted(() => vi.fn(async () => {})); + // Mock fs.readFileSync to handle missing client files first (before imports) vi.mock('fs', async () => { const actual = await vi.importActual('fs') as typeof fs; @@ -43,35 +44,35 @@ vi.mock('fs', async () => { }; }); -vi.mock('http'); -vi.mock('portfinder', () => ({ - getPort: vi.fn((options: any, callback: any) => { - callback(null, options?.port || 5678); - }), -})); vi.mock('launch-ide', () => ({ launchIDE: vi.fn(), })); +vi.mock('@/core/src/server/server', async () => { + const actual = await vi.importActual('@/core/src/server/server'); + return { + ...actual, + startServer: mockStartServer, + }; +}); import { getCodeWithWebComponent } from '@/core/src/server/use-client'; -import { setProjectRecord, resetFileRecord } from '@/core/src/shared/record-cache'; +import { startServer } from '@/core/src/server/server'; +import { + getProjectRecord, + setProjectRecord, + resetFileRecord, +} from '@/core/src/shared/record-cache'; describe('getCodeWithWebComponent', () => { let testDir: string; - let mockServer: any; beforeEach(() => { vi.clearAllMocks(); + mockStartServer.mockResolvedValue(undefined); // Create a temporary test directory testDir = path.join(os.tmpdir(), `test-get-code-${Date.now()}`); fs.mkdirSync(testDir, { recursive: true }); - - // Mock HTTP server - mockServer = { - listen: vi.fn((port: number, callback: Function) => callback()), - }; - vi.mocked(http.createServer).mockReturnValue(mockServer as any); }); afterEach(() => { @@ -133,7 +134,7 @@ describe('getCodeWithWebComponent', () => { server: true, }); - expect(http.createServer).toHaveBeenCalled(); + expect(startServer).toHaveBeenCalledWith(options, record); }); }); @@ -160,7 +161,7 @@ describe('getCodeWithWebComponent', () => { code: 'const x = 1;', }); - expect(http.createServer).toHaveBeenCalled(); + expect(startServer).toHaveBeenCalledWith(options, record); }); it('should not start server when server option is "close"', async () => { @@ -179,9 +180,6 @@ describe('getCodeWithWebComponent', () => { server: 'close', }; - // Reset mocks to track new calls - vi.mocked(http.createServer).mockClear(); - await getCodeWithWebComponent({ options, record, @@ -190,7 +188,7 @@ describe('getCodeWithWebComponent', () => { }); // Server should not be created when server is 'close' - expect(http.createServer).not.toHaveBeenCalled(); + expect(startServer).not.toHaveBeenCalled(); }); }); @@ -252,7 +250,9 @@ describe('getCodeWithWebComponent', () => { code: 'const x = 1;', }); - // injectTo should be recorded + expect(getProjectRecord(record)?.injectTo).toEqual([ + injectToFile.replace(/\\/g, '/'), + ]); }); it('should handle array of injectTo paths', async () => { @@ -282,6 +282,11 @@ describe('getCodeWithWebComponent', () => { file: testFile, code: 'const x = 1;', }); + + expect(getProjectRecord(record)?.injectTo).toEqual([ + injectFile1.replace(/\\/g, '/'), + injectFile2.replace(/\\/g, '/'), + ]); }); it('should warn when injectTo path is not absolute', async () => { diff --git a/test/core/server/use-client/nextjs-integration.test.ts b/test/core/server/use-client/nextjs-integration.test.ts index 9d7d97ed..b96a7055 100644 --- a/test/core/server/use-client/nextjs-integration.test.ts +++ b/test/core/server/use-client/nextjs-integration.test.ts @@ -2,9 +2,10 @@ import { expect, describe, it, vi, beforeEach, afterEach } from 'vitest'; import fs from 'fs'; import path from 'path'; import os from 'os'; -import http from 'http'; import type { RecordInfo, CodeOptions } from '@/core/src/shared/type'; +const mockStartServer = vi.hoisted(() => vi.fn(async () => {})); + // Mock fs.readFileSync to handle missing client files first (before imports) vi.mock('fs', async () => { const actual = await vi.importActual('fs') as typeof fs; @@ -43,15 +44,16 @@ vi.mock('fs', async () => { }; }); -vi.mock('http'); -vi.mock('portfinder', () => ({ - getPort: vi.fn((options: any, callback: any) => { - callback(null, options?.port || 5678); - }), -})); vi.mock('launch-ide', () => ({ launchIDE: vi.fn(), })); +vi.mock('@/core/src/server/server', async () => { + const actual = await vi.importActual('@/core/src/server/server'); + return { + ...actual, + startServer: mockStartServer, + }; +}); import { getCodeWithWebComponent } from '@/core/src/server/use-client'; import { setProjectRecord, resetFileRecord } from '@/core/src/shared/record-cache'; @@ -60,20 +62,14 @@ import * as sharedModule from '@/core/src/shared'; describe('Next.js Integration Tests', () => { let testDir: string; - let mockServer: any; beforeEach(() => { vi.clearAllMocks(); + mockStartServer.mockResolvedValue(undefined); // Create a temporary test directory testDir = path.join(os.tmpdir(), `test-nextjs-${Date.now()}`); fs.mkdirSync(testDir, { recursive: true }); - - // Mock HTTP server - mockServer = { - listen: vi.fn((port: number, callback: Function) => callback()), - }; - vi.mocked(http.createServer).mockReturnValue(mockServer as any); }); afterEach(() => { @@ -496,7 +492,8 @@ export default function NoWrite() { code: 'const x = 1;', }); - expect(result).toBeDefined(); + expect(result).toContain('append-code-0.js'); + expect(fs.existsSync(path.join(testDir, 'append-code-0.js'))).toBe(true); }); it('should inject code when file matches injectTo in record', async () => { diff --git a/test/core/shared/utils/file-url-to-path.test.ts b/test/core/shared/utils/file-url-to-path.test.ts deleted file mode 100644 index 7ce61710..00000000 --- a/test/core/shared/utils/file-url-to-path.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { fileURLToPath } from '@/core/src/shared/utils'; -import { expect, describe, test, afterEach, beforeEach } from 'vitest'; -import process from 'process'; - -describe('fileURLToPath', () => { - // 保存原始的 platform 值 - const originalPlatform = process.platform; - - // 在每个测试后恢复原始的 platform 值 - afterEach(() => { - Object.defineProperty(process, 'platform', { - value: originalPlatform - }); - }); - - describe('Windows platform', () => { - beforeEach(() => { - Object.defineProperty(process, 'platform', { - value: 'win32' - }); - }); - - test('should convert Windows file URL correctly', () => { - const testCases = [ - { - input: 'file:///C:/Users/test/project/file.js', - expected: 'C:\\Users\\test\\project\\file.js' - }, - { - input: 'file:///D:/workspace/project%20name/index.ts', - expected: 'D:\\workspace\\project name\\index.ts' - }, - { - input: 'file:///C:/Users/name%20with%20spaces/file.jsx', - expected: 'C:\\Users\\name with spaces\\file.jsx' - } - ]; - - testCases.forEach(({ input, expected }) => { - expect(fileURLToPath(input)).toBe(expected); - }); - }); - }); - - describe('Unix-like platforms', () => { - beforeEach(() => { - Object.defineProperty(process, 'platform', { - value: 'darwin' // macOS - }); - }); - - test('should convert Unix-like file URL correctly', () => { - const testCases = [ - { - input: 'file:///Users/test/project/file.js', - expected: '/Users/test/project/file.js' - }, - { - input: 'file:///home/user/project%20name/index.ts', - expected: '/home/user/project name/index.ts' - }, - { - input: 'file:///var/www/site/index.html', - expected: '/var/www/site/index.html' - } - ]; - - testCases.forEach(({ input, expected }) => { - expect(fileURLToPath(input)).toBe(expected); - }); - }); - }); - - test('should handle URLs with special characters', () => { - // 设置为非 Windows 平台进行测试 - Object.defineProperty(process, 'platform', { - value: 'darwin' - }); - - const testCases = [ - { - input: 'file:///path/with%20spaces/file.js', - expected: '/path/with spaces/file.js' - }, - { - input: 'file:///path/with%25percentage/file.js', - expected: '/path/with%percentage/file.js' - }, - { - input: 'file:///path/with%23hash/file.js', - expected: '/path/with#hash/file.js' - }, - { - input: 'file:///path/with%3Fquestion/file.js', - expected: '/path/with?question/file.js' - } - ]; - - testCases.forEach(({ input, expected }) => { - expect(fileURLToPath(input)).toBe(expected); - }); - }); -}); \ No newline at end of file diff --git a/test/core/src/index.test.ts b/test/core/src/index.test.ts index a2efc553..88858f92 100644 --- a/test/core/src/index.test.ts +++ b/test/core/src/index.test.ts @@ -3,32 +3,48 @@ import fs from 'fs'; // Mock fs.readFileSync to handle missing client files vi.mock('fs', async () => { - const actual = await vi.importActual('fs') as typeof fs; + const actual = (await vi.importActual('fs')) as typeof fs; return { ...actual, default: { ...actual, readFileSync: vi.fn((filePath: string, encoding?: string) => { - if (typeof filePath === 'string' && (filePath.includes('client.umd.js') || filePath.includes('client.iife.js'))) { + if ( + typeof filePath === 'string' && + (filePath.includes('client.umd.js') || + filePath.includes('client.iife.js')) + ) { return '// mocked client code'; } return actual.readFileSync(filePath, encoding as BufferEncoding); }), existsSync: vi.fn((filePath: string) => { - if (typeof filePath === 'string' && (filePath.includes('client.umd.js') || filePath.includes('client.iife.js'))) { + if ( + typeof filePath === 'string' && + (filePath.includes('client.umd.js') || + filePath.includes('client.iife.js')) + ) { return true; } return actual.existsSync(filePath); }), }, readFileSync: vi.fn((filePath: string, encoding?: string) => { - if (typeof filePath === 'string' && (filePath.includes('client.umd.js') || filePath.includes('client.iife.js'))) { + if ( + typeof filePath === 'string' && + (filePath.includes('client.umd.js') || + filePath.includes('client.iife.js')) + ) { return '// mocked client code'; } return actual.readFileSync(filePath, encoding as BufferEncoding); }), existsSync: vi.fn((filePath: string) => { - if (typeof filePath === 'string' && (filePath.includes('client.umd.js') || filePath.includes('client.iife.js'))) { + if ( + typeof filePath === 'string' && + (filePath.includes('client.umd.js') || + filePath.includes('client.iife.js')) + ) { return true; } return actual.existsSync(filePath); @@ -71,7 +87,6 @@ describe('core/src/index exports', () => { const exports = await import('@/core/src/index'); // From shared/index.ts expect(typeof exports.getIP).toBe('function'); - expect(typeof exports.fileURLToPath).toBe('function'); expect(typeof exports.isJsTypeFile).toBe('function'); expect(typeof exports.getFilePathWithoutExt).toBe('function'); expect(typeof exports.normalizePath).toBe('function'); diff --git a/test/esbuild/index.test.ts b/test/esbuild/index.test.ts index dd2af1d1..02a0708c 100644 --- a/test/esbuild/index.test.ts +++ b/test/esbuild/index.test.ts @@ -122,8 +122,10 @@ describe('EsbuildCodeInspectorPlugin', () => { const onLoadCallback = mockBuild.onLoad.mock.calls[0][1]; const result = await onLoadCallback({ path: '/test/excluded-file-1.tsx' }); - // When excluded and not in cache, returns the code string (checked source code line 56) - expect(result).toBe('const excluded = 1;'); + expect(result).toEqual({ + contents: 'const excluded = 1;', + loader: 'tsx', + }); }); it('should transform JSX files and return output object', async () => { diff --git a/test/turbopack/index.test.ts b/test/turbopack/index.test.ts index 51f3f588..ae3ff5c4 100644 --- a/test/turbopack/index.test.ts +++ b/test/turbopack/index.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; // Mock core module before imports vi.mock('@code-inspector/core', () => ({ @@ -17,15 +17,24 @@ vi.mock('path', async () => { }; }); -import { TurbopackCodeInspectorPlugin } from '@/turbopack/src/index'; +import { + resolveWebpackEntry, + TurbopackCodeInspectorPlugin, +} from '@/turbopack/src/index'; import { isDev, isNextGET16 } from '@code-inspector/core'; describe('TurbopackCodeInspectorPlugin', () => { const originalRequire = global.require; - const originalImportMeta = import.meta; beforeEach(() => { vi.clearAllMocks(); + global.require = { + resolve: vi.fn(() => '/pkg/webpack/index.js'), + } as any; + }); + + afterEach(() => { + global.require = originalRequire; }); describe('basic plugin structure', () => { @@ -125,4 +134,27 @@ describe('TurbopackCodeInspectorPlugin', () => { expect(loaderOptions.record).toBeDefined(); }); }); + + describe('resolveWebpackEntry', () => { + it('should return null when no resolver is provided', () => { + expect(resolveWebpackEntry({})).toBeNull(); + }); + + it('should use require.resolve when provided', () => { + expect( + resolveWebpackEntry({ + requireResolve: vi.fn(() => '/pkg/webpack/index.js'), + }), + ).toBe('/pkg/webpack/index.js'); + }); + + it('should prefer import.meta.resolve when provided', () => { + expect( + resolveWebpackEntry({ + requireResolve: vi.fn(() => '/pkg/webpack/index.js'), + importMetaResolve: vi.fn(() => 'file:///esm/webpack/index.js'), + }), + ).toBe('/esm/webpack/index.js'); + }); + }); }); diff --git a/test/webpack/index.test.ts b/test/webpack/index.test.ts index c7e066ab..6d274960 100644 --- a/test/webpack/index.test.ts +++ b/test/webpack/index.test.ts @@ -371,6 +371,47 @@ describe('WebpackCodeInspectorPlugin', () => { ); expect(injectLoader?.enforce).toBe('pre'); }); + + it('should not register duplicate loaders on repeated apply', async () => { + vi.mocked(isDev).mockReturnValueOnce(true); + vi.mocked(isDev).mockReturnValueOnce(true); + const plugin = new WebpackCodeInspectorPlugin({ + bundler: 'webpack', + output: '/test', + }); + + await plugin.apply(mockCompiler); + const firstRuleCount = mockCompiler.options.module.rules.length; + + await plugin.apply(mockCompiler); + + expect(mockCompiler.options.module.rules.length).toBe(firstRuleCount); + }); + + it('should not register loaders when inject-loader already exists', async () => { + vi.mocked(isDev).mockReturnValueOnce(true); + vi.mocked(isDev).mockReturnValueOnce(true); + + const plugin = new WebpackCodeInspectorPlugin({ + bundler: 'webpack', + output: '/test', + }); + + await plugin.apply(mockCompiler); + const injectLoader = mockCompiler.options.module.rules.find((rule: any) => + rule.use?.some?.((item: any) => item.loader?.includes('inject-loader.js')), + ); + + mockCompiler.options.module.rules = [ + { + use: [{ loader: injectLoader.use[0].loader }], + }, + ]; + + await plugin.apply(mockCompiler); + + expect(mockCompiler.options.module.rules).toHaveLength(1); + }); }); describe('rspack persistent cache', () => { @@ -421,6 +462,20 @@ describe('WebpackCodeInspectorPlugin', () => { expect(mockCompiler.options.module.loaders.length).toBeGreaterThan(0); }); + + it('should handle missing module config', async () => { + vi.mocked(isDev).mockReturnValueOnce(true); + delete mockCompiler.options.module; + + const plugin = new WebpackCodeInspectorPlugin({ + bundler: 'webpack', + output: '/test', + }); + + await plugin.apply(mockCompiler); + + expect(mockCompiler.hooks.emit.tapAsync).toHaveBeenCalled(); + }); }); describe('environment detection', () => { diff --git a/test/webpack/loader.test.ts b/test/webpack/loader.test.ts index 37a6a134..f8da881f 100644 --- a/test/webpack/loader.test.ts +++ b/test/webpack/loader.test.ts @@ -266,5 +266,19 @@ describe('WebpackCodeInspectorLoader', () => { const result = await WebpackCodeInspectorLoader.call(mockContext, 'const x = 1;'); expect(result).toBeDefined(); }); + + it('should handle undefined options', async () => { + mockContext.query = undefined; + mockContext.resourcePath = '/test/file.tsx'; + mockContext.resource = '/test/file.tsx'; + vi.mocked(isJsTypeFile).mockReturnValueOnce(true); + + const result = await WebpackCodeInspectorLoader.call( + mockContext, + 'const x = 1;', + ); + + expect(result).toBeDefined(); + }); }); });