Skip to content

Commit

Permalink
refactor: move module mocking transforms out of plugins (#2993)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Mar 20, 2023
1 parent 0ddf722 commit 29c4952
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 143 deletions.
6 changes: 5 additions & 1 deletion packages/vite-node/src/server.ts
Expand Up @@ -191,6 +191,10 @@ export class ViteNodeServer {
return result
}

protected async processTransformResult(result: TransformResult) {
return withInlineSourcemap(result)
}

private async _transformRequest(id: string, customTransformMode?: 'web' | 'ssr') {
debugRequest(id)

Expand All @@ -217,7 +221,7 @@ export class ViteNodeServer {

const sourcemap = this.options.sourcemap ?? 'inline'
if (sourcemap === 'inline' && result && !id.includes('node_modules'))
withInlineSourcemap(result)
result = await this.processTransformResult(result)

if (this.options.debug?.dumpModules)
await this.debugger?.dumpFile(id, result)
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-node/src/source-map.ts
Expand Up @@ -13,7 +13,7 @@ const VITE_NODE_SOURCEMAPPING_SOURCE = '//# sourceMappingSource=vite-node'
const VITE_NODE_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8`
const VITE_NODE_SOURCEMAPPING_REGEXP = new RegExp(`//# ${VITE_NODE_SOURCEMAPPING_URL};base64,(.+)`)

export async function withInlineSourcemap(result: TransformResult) {
export function withInlineSourcemap(result: TransformResult) {
const map = result.map
let code = result.code

Expand Down
1 change: 1 addition & 0 deletions packages/vitest/package.json
Expand Up @@ -144,6 +144,7 @@
"why-is-node-running": "^2.2.2"
},
"devDependencies": {
"@ampproject/remapping": "^2.2.0",
"@antfu/install-pkg": "^0.1.1",
"@edge-runtime/vm": "2.0.2",
"@sinonjs/fake-timers": "^10.0.2",
Expand Down
6 changes: 3 additions & 3 deletions packages/vitest/src/node/core.ts
Expand Up @@ -5,7 +5,6 @@ import fg from 'fast-glob'
import mm from 'micromatch'
import c from 'picocolors'
import { ViteNodeRunner } from 'vite-node/client'
import { ViteNodeServer } from 'vite-node/server'
import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig, VitestRunMode } from '../types'
import { SnapshotManager } from '../integrations/snapshot/manager'
import { deepMerge, hasFailed, noop, slash, toArray } from '../utils'
Expand All @@ -18,6 +17,7 @@ import { StateManager } from './state'
import { resolveConfig } from './config'
import { Logger } from './logger'
import { VitestCache } from './cache'
import { VitestServer } from './server'

const WATCHER_DEBOUNCE = 100

Expand All @@ -35,7 +35,7 @@ export class Vitest {
pool: ProcessPool | undefined
typechecker: Typechecker | undefined

vitenode: ViteNodeServer = undefined!
vitenode: VitestServer = undefined!

invalidates: Set<string> = new Set()
changedTests: Set<string> = new Set()
Expand Down Expand Up @@ -76,7 +76,7 @@ export class Vitest {
if (this.config.watch && this.mode !== 'typecheck')
this.registerWatcher()

this.vitenode = new ViteNodeServer(server, this.config)
this.vitenode = new VitestServer(server, this.config)
const node = this.vitenode
this.runner = new ViteNodeRunner({
root: server.config.root,
Expand Down
235 changes: 235 additions & 0 deletions packages/vitest/src/node/mock.ts
@@ -0,0 +1,235 @@
import MagicString from 'magic-string'
import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping'
import type { SourceMap } from 'rollup'
import type { TransformResult } from 'vite'
import remapping from '@ampproject/remapping'
import { getCallLastIndex } from '../utils'

const hoistRegexp = /^[ \t]*\b(?:__vite_ssr_import_\d+__\.)?((?:vitest|vi)\s*.\s*(mock|unmock)\(["`'\s]+(.*[@\w_-]+)["`'\s]+)[),]{1};?/gm

const API_NOT_FOUND_ERROR = `There are some problems in resolving the mocks API.
You may encounter this issue when importing the mocks API from another module other than 'vitest'.
To fix this issue you can either:
- import the mocks API directly from 'vitest'
- enable the 'globals' options`

export function hoistModuleMocks(mod: TransformResult, vitestPath: string): TransformResult {
if (!mod.code)
return mod
const m = hoistCodeMocks(mod.code)

if (m) {
const vitestRegexp = new RegExp(`const __vite_ssr_import_\\d+__ = await __vite_ssr_import__\\("(?:\/@fs\/?)?(?:${vitestPath}|vitest)"\\);`, 'gm')
// hoist vitest imports in case it was used inside vi.mock factory #425
const vitestImports = mod.code.matchAll(vitestRegexp)
let found = false

for (const match of vitestImports) {
const indexStart = match.index!
const indexEnd = match[0].length + indexStart
m.remove(indexStart, indexEnd)
m.prepend(`${match[0]}\n`)
found = true
}

// if no vitest import found, check if the mock API is reachable after the hoisting
if (!found) {
m.prepend('if (typeof globalThis.vi === "undefined" && typeof globalThis.vitest === "undefined") '
+ `{ throw new Error(${JSON.stringify(API_NOT_FOUND_ERROR)}) }\n`)
}

return {
...mod,
code: m.toString(),
map: mod.map
? combineSourcemaps(
mod.map.file,
[
{
...m.generateMap({ hires: true }),
sourcesContent: mod.map.sourcesContent,
} as RawSourceMap,
mod.map as RawSourceMap,
],
) as SourceMap
: null,
}
}

return mod
}

function hoistCodeMocks(code: string) {
let m: MagicString | undefined
const mocks = code.matchAll(hoistRegexp)

for (const mockResult of mocks) {
const lastIndex = getMockLastIndex(code.slice(mockResult.index!))

if (lastIndex === null)
continue

const startIndex = mockResult.index!

const { insideComment, insideString } = getIndexStatus(code, startIndex)

if (insideComment || insideString)
continue

const endIndex = startIndex + lastIndex

m ??= new MagicString(code)

m.prepend(`${m.slice(startIndex, endIndex)}\n`)
m.remove(startIndex, endIndex)
}

return m
}

function escapeToLinuxLikePath(path: string) {
if (/^[A-Z]:/.test(path))
return path.replace(/^([A-Z]):\//, '/windows/$1/')

if (/^\/[^/]/.test(path))
return `/linux${path}`

return path
}

function unescapeToLinuxLikePath(path: string) {
if (path.startsWith('/linux/'))
return path.slice('/linux'.length)

if (path.startsWith('/windows/'))
return path.replace(/^\/windows\/([A-Z])\//, '$1:/')

return path
}

// based on https://github.com/vitejs/vite/blob/6b40f03574cd71a17cbe564bc63adebb156ff06e/packages/vite/src/node/utils.ts#L727
const nullSourceMap: RawSourceMap = {
names: [],
sources: [],
mappings: '',
version: 3,
}
export function combineSourcemaps(
filename: string,
sourcemapList: Array<DecodedSourceMap | RawSourceMap>,
excludeContent = true,
): RawSourceMap {
if (
sourcemapList.length === 0
|| sourcemapList.every(m => m.sources.length === 0)
)
return { ...nullSourceMap }

// hack for parse broken with normalized absolute paths on windows (C:/path/to/something).
// escape them to linux like paths
// also avoid mutation here to prevent breaking plugin's using cache to generate sourcemaps like vue (see #7442)
sourcemapList = sourcemapList.map((sourcemap) => {
const newSourcemaps = { ...sourcemap }
newSourcemaps.sources = sourcemap.sources.map(source =>
source ? escapeToLinuxLikePath(source) : null,
)
if (sourcemap.sourceRoot)
newSourcemaps.sourceRoot = escapeToLinuxLikePath(sourcemap.sourceRoot)

return newSourcemaps
})
const escapedFilename = escapeToLinuxLikePath(filename)

// We don't declare type here so we can convert/fake/map as RawSourceMap
let map // : SourceMap
let mapIndex = 1
const useArrayInterface
= sourcemapList.slice(0, -1).find(m => m.sources.length !== 1) === undefined
if (useArrayInterface) {
map = remapping(sourcemapList, () => null, excludeContent)
}
else {
map = remapping(
sourcemapList[0],
(sourcefile) => {
if (sourcefile === escapedFilename && sourcemapList[mapIndex])
return sourcemapList[mapIndex++]

else
return null
},
excludeContent,
)
}
if (!map.file)
delete map.file

// unescape the previous hack
map.sources = map.sources.map(source =>
source ? unescapeToLinuxLikePath(source) : source,
)
map.file = filename

return map as RawSourceMap
}

function getMockLastIndex(code: string): number | null {
const index = getCallLastIndex(code)
if (index === null)
return null
return code[index + 1] === ';' ? index + 2 : index + 1
}

function getIndexStatus(code: string, from: number) {
let index = 0
let commentStarted = false
let commentEnded = true
let multilineCommentStarted = false
let multilineCommentEnded = true
let inString: string | null = null
let beforeChar: string | null = null

while (index <= from) {
const char = code[index]
const sub = code[index] + code[index + 1]

if (!inString) {
if (sub === '/*') {
multilineCommentStarted = true
multilineCommentEnded = false
}
if (sub === '*/' && multilineCommentStarted) {
multilineCommentStarted = false
multilineCommentEnded = true
}
if (sub === '//') {
commentStarted = true
commentEnded = false
}
if ((char === '\n' || sub === '\r\n') && commentStarted) {
commentStarted = false
commentEnded = true
}
}

if (!multilineCommentStarted && !commentStarted) {
const isCharString = char === '"' || char === '\'' || char === '`'

if (isCharString && beforeChar !== '\\') {
if (inString === char)
inString = null
else if (!inString)
inString = char
}
}

beforeChar = char
index++
}

return {
insideComment: !multilineCommentEnded || !commentEnded,
insideString: inString !== null,
}
}
2 changes: 0 additions & 2 deletions packages/vitest/src/node/plugins/index.ts
Expand Up @@ -12,7 +12,6 @@ import { Vitest } from '../core'
import { generateScopedClassName } from '../../integrations/css/css-modules'
import { EnvReplacerPlugin } from './envReplacer'
import { GlobalSetupPlugin } from './globalSetup'
import { MocksPlugin } from './mock'
import { CSSEnablerPlugin } from './cssEnabler'
import { CoverageTransform } from './coverageTransform'

Expand Down Expand Up @@ -244,7 +243,6 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
},
},
EnvReplacerPlugin(),
MocksPlugin(),
GlobalSetupPlugin(ctx),
...(options.browser
? await BrowserPlugin()
Expand Down

0 comments on commit 29c4952

Please sign in to comment.