🏡 index : quine-global/webdriverio.git

import path from 'node:path'
import { EventEmitter } from 'node:events'

import getPort from 'get-port'
import logger from '@wdio/logger'
import istanbulPlugin from 'vite-plugin-istanbul'
import { deepmerge } from 'deepmerge-ts'
import { createServer } from 'vite'
import type { ViteDevServer, InlineConfig, ConfigEnv } from 'vite'

import { testrunner } from './plugins/testrunner.js'
import { mockHoisting } from './plugins/mockHoisting.js'
import { workerPlugin, type SocketEventHandler } from './plugins/worker.js'
import { userfriendlyImport } from './utils.js'
import { MockHandler } from './mock.js'
import { PRESET_DEPENDENCIES, DEFAULT_VITE_CONFIG } from './constants.js'
import { DEFAULT_INCLUDE, DEFAULT_FILE_EXTENSIONS } from '../constants.js'

const log = logger('@wdio/browser-runner:ViteServer')
const DEFAULT_CONFIG_ENV: ConfigEnv = {
    command: 'serve',
    mode: process.env.NODE_ENV === 'production' ? 'production' : 'development'
}

/**
 * Class that sets up the Vite server with correct configuration based on given WebdriverIO options.
 */
export class ViteServer extends EventEmitter {
    #options: WebdriverIO.BrowserRunnerOptions
    #config: WebdriverIO.Config
    #viteConfig: Partial<InlineConfig>
    #server?: ViteDevServer
    #mockHandler: MockHandler
    #socketEventHandler: SocketEventHandler[] = []

    get config () {
        return this.#viteConfig
    }

    constructor (options: WebdriverIO.BrowserRunnerOptions, config: WebdriverIO.Config, optimizations: InlineConfig) {
        super()
        this.#options = options
        this.#config = config
        this.#mockHandler = new MockHandler(options, config)

        const root = options.rootDir || config.rootDir || process.cwd()
        this.#viteConfig = deepmerge(DEFAULT_VITE_CONFIG, optimizations, {
            root,
            plugins: [
                testrunner(options),
                mockHoisting(this.#mockHandler),
                workerPlugin((payload, client) => (
                    this.#socketEventHandler.forEach(
                        (handler) => handler(payload, client)
                    )
                ))
            ]
        })

        if (options.coverage && options.coverage.enabled) {
            log.info('Capturing test coverage enabled')
            // @ts-expect-error istanbul plugin seems to incorrectly export
            // its type for our setup
            const plugin = istanbulPlugin as typeof istanbulPlugin.default
            this.#viteConfig.plugins?.push(plugin({
                cwd: config.rootDir,
                include: DEFAULT_INCLUDE,
                extension: DEFAULT_FILE_EXTENSIONS,
                forceBuildInstrument: true,
                ...options.coverage
            }))
        }
    }

    onBrowserEvent (handler: SocketEventHandler) {
        this.#socketEventHandler.push(handler)
    }

    async start () {
        /**
         * load additional Vite plugins for framework
         */
        if (this.#options.preset) {
            const [pkg, importProp, opts] = PRESET_DEPENDENCIES[this.#options.preset] || []
            const plugin = (await userfriendlyImport(this.#options.preset, pkg))[importProp || 'default']
            if (plugin) {
                this.#viteConfig.plugins!.push(plugin(opts))
            }
        }

        /**
         * merge custom `viteConfig` last into the object
         */
        if (this.#options.viteConfig) {
            const { plugins, ...configToMerge } = typeof this.#options.viteConfig === 'string'
                ? (
                    await import(path.resolve(this.#config.rootDir || process.cwd(), this.#options.viteConfig))
                ).default as InlineConfig
                : typeof this.#options.viteConfig === 'function'
                    ? await this.#options.viteConfig(DEFAULT_CONFIG_ENV)
                    : this.#options.viteConfig
            this.#viteConfig = deepmerge(this.#viteConfig, configToMerge)
            this.#viteConfig.plugins = [...(plugins || []), ...this.#viteConfig.plugins!]
        }

        /**
         * merge custom port into Vite config last
         */
        const vitePort = await getPort()
        this.#viteConfig = deepmerge(this.#viteConfig, <Partial<InlineConfig>>{
            server: {
                ...this.#viteConfig.server,
                port: vitePort
            }
        })

        /**
         * initialize Vite
         */
        this.#server = await createServer(this.#viteConfig)
        await this.#server.listen()
        log.info(`Vite server started successfully on port ${vitePort}, root directory: ${this.#viteConfig.root}`)

        return vitePort
    }

    async close () {
        await this.#server?.close()
    }
}