{"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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"}