UNPKG

vite-plugin-istanbul

Version:

vite-plugin-istanbul ==========================

1 lines 18.4 kB
{"version":3,"file":"index.mjs","names":[],"sources":["../src/source-map.ts","../src/vue-sfc.ts","../src/index.ts"],"sourcesContent":["import * as espree from 'espree';\nimport { SourceMapGenerator, StartOfSourceMap } from 'source-map';\n\nexport type RawSourceMap = {\n version: number;\n sources: string[];\n names: string[];\n sourceRoot?: string;\n sourcesContent?: string[];\n mappings: string;\n file: string;\n};\n\n// Create a source map which always maps to the same line and column\nexport function createIdentitySourceMap(\n file: string,\n source: string,\n option: StartOfSourceMap\n) {\n const gen = new SourceMapGenerator(option);\n const tokens = espree.tokenize(source, { loc: true, ecmaVersion: 'latest' });\n\n tokens.forEach((token: any) => {\n const loc = token.loc.start;\n gen.addMapping({\n source: file,\n original: loc,\n generated: loc,\n });\n });\n\n return JSON.parse(gen.toString());\n}\n\n// Creates a complete source map that maps all lines of compiled code to the original file.\n// This fixes coverage for Vue SFCs where Vite's source map only covers the script portion,\n// leaving template-generated code (render function) unmapped and causing 0% coverage.\nexport function createCompleteSourceMap(\n file: string,\n source: string,\n originalSource: string | null,\n option: StartOfSourceMap\n) {\n const gen = new SourceMapGenerator(option);\n const lines = source.split('\\n');\n const originalLines = originalSource\n ? originalSource.split('\\n').length\n : lines.length;\n\n lines.forEach((line, lineIndex) => {\n const tokens: Array<{ loc: { start: { line: number; column: number } } }> =\n [];\n\n try {\n tokens.push(\n ...espree.tokenize(line, { loc: true, ecmaVersion: 'latest' })\n );\n } catch {\n // If tokenization fails (e.g., partial statement), add a single mapping for the line\n tokens.push({ loc: { start: { line: 1, column: 0 } } });\n }\n\n tokens.forEach((token) => {\n // Map each generated token back to the closest original line\n const originalLine = Math.min(lineIndex + 1, originalLines);\n gen.addMapping({\n source: file,\n original: { line: originalLine, column: 0 },\n generated: { line: lineIndex + 1, column: token.loc.start.column },\n });\n });\n });\n\n if (originalSource) {\n gen.setSourceContent(file, originalSource);\n }\n\n return JSON.parse(gen.toString());\n}\n","/** # Fix for vue Single-File Components instrumentation in build mode. (cf issue #96)\n *\n * ## Option API SFC splits file into 2\n *\n * 1. id: /path/to/file.vue which contains all the code **What we need to instrument**\n * 2. id: /path/to/file.vue?vue&type=style&... which contains no source code\n *\n * ## Composition API SFC splits file into 3 chunks\n *\n * 1. id: /path/to/file.vue which contains only impors and exports but no user's source code\n * 2. id: /path/to/file.vue?vue&type=style&... which contains no source code\n * 3. id: /path/to/file.vue?vue&type=script&... which contains all the user's source code **What we need to instrument**\n *\n * ## Diff of chunk 1\n *\n * - Composition API: starts with `import _sfc_main from '/path/to/file.vue?vue&type=script...'\\n`\n * - Option API: starts with `\\nconst _sfc_main = {\\n`\n *\n */\nexport function canInstrumentChunk(id: string, srcCode: string): boolean {\n const is1stChunk = id.endsWith('.vue');\n const is2ndChunk = /\\?vue&type=style/.test(id);\n const is3rdChunk = /\\?vue&type=script/.test(id);\n const isCompositionAPI = /import _sfc_main from/.test(srcCode);\n if (is2ndChunk) {\n // never instrument type=style\n return false;\n }\n if (is3rdChunk) {\n // always instrument type=script\n return true;\n }\n if (is1stChunk) {\n // instrument 1st chunk only if it's Option API\n return !isCompositionAPI;\n }\n // instrument if not a vue chunk\n return true;\n}\n","import type { GeneratorOptions } from '@babel/generator';\nimport { loadNycConfig } from '@istanbuljs/load-nyc-config';\nimport { createInstrumenter } from 'istanbul-lib-instrument';\nimport picocolors from 'picocolors';\nimport TestExclude from 'test-exclude';\nimport { createLogger, Plugin, TransformResult } from 'vite';\n\nimport {\n createCompleteSourceMap,\n createIdentitySourceMap,\n type RawSourceMap,\n} from './source-map';\nimport { canInstrumentChunk } from './vue-sfc';\n\nconst { yellow } = picocolors;\n\n// Required for typings to work in configureServer()\ndeclare global {\n var __coverage__: any;\n}\n\n/**\n * Custom instrumenter interface. Matches the subset of istanbul-lib-instrument's\n * Instrumenter that this plugin uses. Implement this to use a faster instrumenter\n * (e.g., oxc-coverage-instrument) while keeping the Istanbul coverage format.\n */\nexport interface CustomInstrumenter {\n instrumentSync(\n code: string,\n filename: string,\n inputSourceMap?: RawSourceMap\n ): string;\n lastSourceMap(): RawSourceMap | null;\n fileCoverage: object;\n}\n\nexport interface IstanbulPluginOptions {\n include?: string | string[];\n exclude?: string | string[];\n extension?: string | string[];\n requireEnv?: boolean;\n cypress?: boolean;\n checkProd?: boolean;\n forceBuildInstrument?: boolean;\n cwd?: string;\n nycrcPath?: string;\n generatorOpts?: GeneratorOptions;\n onCover?: (fileName: string, fileCoverage: object) => void;\n /**\n * Custom instrumenter to use instead of istanbul-lib-instrument.\n * Must implement `instrumentSync`, `lastSourceMap`, and `fileCoverage`.\n *\n * @example\n * ```ts\n * import { createOxcInstrumenter } from 'oxc-coverage-instrument/vitest';\n *\n * istanbul({\n * instrumenter: createOxcInstrumenter(),\n * })\n * ```\n */\n instrumenter?: CustomInstrumenter;\n}\n\n// Custom extensions to include .vue files\nconst DEFAULT_EXTENSION = [\n '.js',\n '.cjs',\n '.mjs',\n '.ts',\n '.tsx',\n '.jsx',\n '.vue',\n];\nconst COVERAGE_PUBLIC_PATH = '/__coverage__';\nconst PLUGIN_NAME = 'vite:istanbul';\nconst MODULE_PREFIX = '/@modules/';\nconst NULL_STRING = '\\0';\n\nfunction sanitizeSourceMap(rawSourceMap: RawSourceMap): RawSourceMap {\n // Delete sourcesContent since it is optional and if it contains process.env.NODE_ENV vite will break when trying to replace it\n const { sourcesContent, ...sourceMap } = rawSourceMap;\n\n // JSON parse/stringify trick required for istanbul to accept the SourceMap\n return JSON.parse(JSON.stringify(sourceMap));\n}\n\nfunction getEnvVariable(\n key: string,\n prefix: string | string[],\n env: Record<string, any>\n): string {\n if (Array.isArray(prefix)) {\n const envPrefix = prefix.find((pre) => {\n const prefixedName = `${pre}${key}`;\n\n return env[prefixedName] != null;\n });\n\n prefix = envPrefix ?? '';\n }\n\n return env[`${prefix}${key}`];\n}\n\nasync function createTestExclude(\n opts: IstanbulPluginOptions\n): Promise<TestExclude> {\n const { nycrcPath, include, exclude, extension } = opts;\n const cwd = opts.cwd ?? process.cwd();\n\n const nycConfig = await loadNycConfig({\n cwd,\n nycrcPath,\n });\n\n // Only instrument when we want to, as we only want instrumentation in test\n // By default the plugin is always on\n return new TestExclude({\n cwd,\n include: include ?? nycConfig.include,\n exclude: exclude ?? nycConfig.exclude,\n extension: extension ?? nycConfig.extension ?? DEFAULT_EXTENSION,\n excludeNodeModules: true,\n });\n}\n\nfunction resolveFilename(id: string): string {\n // Fix for @vitejs/plugin-vue in serve mode (#67)\n // To remove the annoying query parameters from the filename\n const [filename] = id.split('?vue');\n\n return filename;\n}\n\nexport default function istanbulPlugin(\n opts: IstanbulPluginOptions = {}\n): Plugin {\n const requireEnv = opts?.requireEnv ?? false;\n const checkProd = opts?.checkProd ?? true;\n const forceBuildInstrument = opts?.forceBuildInstrument ?? false;\n\n const logger = createLogger('warn', { prefix: 'vite-plugin-istanbul' });\n let testExclude: TestExclude;\n const instrumenter: CustomInstrumenter =\n opts.instrumenter ??\n createInstrumenter({\n coverageGlobalScopeFunc: false,\n coverageGlobalScope: 'globalThis',\n preserveComments: true,\n produceSourceMap: true,\n autoWrap: true,\n esModules: true,\n compact: false,\n generatorOpts: { ...opts?.generatorOpts },\n });\n\n // Lazy check the active status of the plugin\n // as this gets fed after config is fully resolved\n let enabled = true;\n\n return {\n name: PLUGIN_NAME,\n apply(_, env) {\n // If forceBuildInstrument is true run for both serve and build\n return forceBuildInstrument ? true : env.command == 'serve';\n },\n // istanbul only knows how to instrument JavaScript,\n // this allows us to wait until the whole code is JavaScript to\n // instrument and sourcemap\n enforce: 'post',\n async config(config) {\n // If sourcemap is not set (either undefined or false)\n if (!config.build?.sourcemap) {\n logger.warn(\n `${PLUGIN_NAME}> ${yellow(`Sourcemaps was automatically enabled for code coverage to be accurate.\nTo hide this message set build.sourcemap to true, 'inline' or 'hidden'.`)}`\n );\n\n // Enforce sourcemapping,\n config.build ??= {};\n config.build.sourcemap = true;\n }\n testExclude = await createTestExclude(opts);\n },\n configResolved(config) {\n // We need to check if the plugin should enable after all configuration is resolved\n // As config can be modified by other plugins and from .env variables\n const { isProduction, env } = config;\n const { CYPRESS_COVERAGE } = process.env;\n const envPrefix = config.envPrefix ?? 'VITE_';\n\n const envCoverage = opts.cypress\n ? CYPRESS_COVERAGE\n : getEnvVariable('COVERAGE', envPrefix, env);\n const envVar = envCoverage?.toLowerCase() ?? '';\n\n if (\n (checkProd && isProduction && !forceBuildInstrument) ||\n (!requireEnv && envVar === 'false') ||\n (requireEnv && envVar !== 'true')\n ) {\n enabled = false;\n }\n },\n configureServer({ middlewares }) {\n if (!enabled) {\n return;\n }\n\n // Returns the current code coverage in the global scope\n // Used if an external endpoint is required to fetch code coverage\n middlewares.use((req, res, next) => {\n if (req.url !== COVERAGE_PUBLIC_PATH) {\n return next();\n }\n\n const coverage = global.__coverage__ ?? null;\n let data: string;\n\n try {\n data = JSON.stringify(coverage, null, 4);\n } catch (ex) {\n return next(ex);\n }\n\n res.setHeader('Content-Type', 'application/json');\n res.statusCode = 200;\n res.end(data);\n });\n },\n transform(srcCode, id, options) {\n if (\n !enabled ||\n options?.ssr ||\n id.startsWith(MODULE_PREFIX) ||\n id.startsWith(NULL_STRING)\n ) {\n // do not transform if this is a dep\n // do not transform if plugin is not enabled\n // do not transform if ssr\n return;\n\n // Fix for Vue SFC\n }\n if (!canInstrumentChunk(id, srcCode)) {\n return;\n }\n const filename = resolveFilename(id);\n\n if (testExclude.shouldInstrument(filename)) {\n // Get the raw combined source map before sanitization to preserve sourcesContent\n const rawCombinedSourceMap = this.getCombinedSourcemap();\n const combinedSourceMap = sanitizeSourceMap(rawCombinedSourceMap);\n\n // For Vue SFC files, create a complete source map that covers all compiled lines.\n // Vite's source map only covers the <script> block, leaving template-generated code\n // (render function) unmapped, which causes Istanbul to report 0% coverage.\n const isVueSFC = id.endsWith('.vue') || /\\?vue&type=script/.test(id);\n\n if (isVueSFC) {\n let originalSource: string | null = null;\n if (\n rawCombinedSourceMap.sourcesContent &&\n rawCombinedSourceMap.sourcesContent[0]\n ) {\n originalSource = rawCombinedSourceMap.sourcesContent[0];\n }\n\n const completeSourceMap = sanitizeSourceMap(\n createCompleteSourceMap(filename, srcCode, originalSource, {\n file: combinedSourceMap.file,\n sourceRoot: combinedSourceMap.sourceRoot,\n })\n );\n\n const code = instrumenter.instrumentSync(\n srcCode,\n filename,\n completeSourceMap\n );\n const map = instrumenter.lastSourceMap();\n\n const fileCoverage = instrumenter.fileCoverage;\n if (opts.onCover) {\n opts.onCover(filename, fileCoverage);\n }\n return { code, map } as TransformResult;\n }\n\n // For non-Vue files, use the two-pass instrumentation approach\n // to ensure proper source map handling\n const code = instrumenter.instrumentSync(\n srcCode,\n filename,\n combinedSourceMap\n );\n\n // Create an identity source map with the same number of fields as the combined source map\n const identitySourceMap = sanitizeSourceMap(\n createIdentitySourceMap(filename, srcCode, {\n file: combinedSourceMap.file,\n sourceRoot: combinedSourceMap.sourceRoot,\n })\n );\n\n // Create a result source map to combine with the source maps of previous plugins\n instrumenter.instrumentSync(srcCode, filename, identitySourceMap);\n const map = instrumenter.lastSourceMap();\n\n const fileCoverage = instrumenter.fileCoverage;\n if (opts.onCover) {\n opts.onCover(filename, fileCoverage);\n }\n // Required to cast to correct mapping value\n return { code, map } as TransformResult;\n }\n },\n };\n}\n"],"mappings":"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"}