Implementation:Webdriverio Webdriverio InitializePlugin Function
Overview
Concrete tool for dynamically resolving and importing WebdriverIO plugins provided by the @wdio/utils package.
Description
The initializePlugin function resolves plugin module names using a convention-based approach. It tries scoped names (@wdio/{name}-{type}), then community names (wdio-{name}-{type}), and supports direct imports for scoped packages and absolute paths.
The function is the core of WDIO's plugin ecosystem, used every time a plugin is specified by string name in the configuration. It works in conjunction with:
initializeServices-- Resolves service entries from the config into initialized service instancesinitializeLauncherService-- Creates launcher-process service instances with proper separation from worker servicesinitializeWorkerService-- Creates worker-process service instances, filtering out launcher-only services
The resolution relies on safeImport, a utility that wraps dynamic import() in a try/catch, returning undefined on failure instead of throwing. This enables the chain-of-responsibility pattern where each naming convention is tried in sequence.
Source Files
| File | Purpose | Lines |
|---|---|---|
packages/wdio-utils/src/initializePlugin.ts |
Plugin resolution function | L13-51 |
packages/wdio-utils/src/initializeServices.ts |
Service initialization pipeline | L23-196 |
Code Reference
initializePlugin Function
// packages/wdio-utils/src/initializePlugin.ts:L13-51
import type { Services } from '@wdio/types'
import { safeImport, isAbsolute, REG_EXP_WINDOWS_ABS_PATH, SLASH } from './utils.js'
const FILE_PROTOCOL = 'file://'
/**
* Initialize WebdriverIO compliant plugins like reporter or services in the following way:
* 1. if package name is scoped (starts with "@"), require scoped package name
* 2. otherwise try to require "@wdio/<name>-<type>"
* 3. otherwise try to require "wdio-<name>-<type>"
*/
export default async function initializePlugin(
name: string,
type?: string
): Promise<Services.ServicePlugin | Services.RunnerPlugin> {
/**
* directly import packages that are scoped or start with an absolute path
*/
if (name[0] === '@' || isAbsolute(name)) {
const fileUrl = name[0] === '@' ? name : ensureFileURL(name)
const service = await safeImport(fileUrl)
if (service) {
return service
}
}
if (typeof type !== 'string') {
throw new Error('No plugin type provided')
}
/**
* check for scoped version of plugin first (e.g. @wdio/sauce-service)
*/
const scopedPlugin = await safeImport(`@wdio/${name.toLowerCase()}-${type}`)
if (scopedPlugin) {
return scopedPlugin
}
/**
* check for community naming convention
*/
const plugin = await safeImport(`wdio-${name.toLowerCase()}-${type}`)
if (plugin) {
return plugin
}
throw new Error(
`Couldn't find plugin "${name}" ${type}, neither as wdio scoped package ` +
`"@wdio/${name.toLowerCase()}-${type}" nor as community package ` +
`"wdio-${name.toLowerCase()}-${type}". Please make sure you have it installed!`
)
}
ensureFileURL Helper
// packages/wdio-utils/src/initializePlugin.ts:L53-69
function ensureFileURL(path: string) {
if (path.startsWith(FILE_PROTOCOL)) {
return path
}
// Windows drive path
if (REG_EXP_WINDOWS_ABS_PATH.test(path)) {
return `${FILE_PROTOCOL}/${path.replace(/\\/g, '/')}`
}
// Unix absolute path
if (path.startsWith(SLASH)) {
return `${FILE_PROTOCOL}${path}`
}
return path
}
initializeLauncherService Function
// packages/wdio-utils/src/initializeServices.ts:L94-151
export async function initializeLauncherService(
config: Omit<WebdriverIO.Config, 'capabilities' | keyof Services.HookFunctions>,
caps: Capabilities.TestrunnerCapabilities
): Promise<{
ignoredWorkerServices: string[];
launcherServices: Services.ServiceInstance[];
}> {
const ignoredWorkerServices = []
const launcherServices: Services.ServiceInstance[] = []
const services = await initializeServices(config.services!.map(sanitizeServiceArray))
for (const [service, serviceConfig, serviceName] of services) {
// Object services: use directly
if (typeof service === 'object' && !serviceName) {
launcherServices.push(service as object)
continue
}
// Imported package with launcher export
const Launcher = (service as Services.ServicePlugin).launcher
if (typeof Launcher === 'function' && serviceName) {
launcherServices.push(new Launcher(serviceConfig, caps, config))
}
// Class reference passed directly
if (typeof service === 'function' && !serviceName) {
launcherServices.push(new service(serviceConfig, caps, config))
}
// Mark launcher-only services to skip in workers
if (serviceName && typeof (service as { default: Function }).default !== 'function'
&& typeof service !== 'function') {
ignoredWorkerServices.push(serviceName)
}
}
return { ignoredWorkerServices, launcherServices }
}
initializeWorkerService Function
// packages/wdio-utils/src/initializeServices.ts:L161-196
export async function initializeWorkerService(
config: WebdriverIO.Config,
caps: WebdriverIO.Capabilities,
ignoredWorkerServices: string[] = []
): Promise<Services.ServiceInstance[]> {
const initializedServices: Services.ServiceInstance[] = []
const workerServices = config.services!
.map(sanitizeServiceArray)
.filter(([serviceName]) => !ignoredWorkerServices.includes(serviceName as string))
const services = await initializeServices(workerServices)
for (const [service, serviceConfig, serviceName] of services) {
if (typeof service === 'object' && !serviceName) {
initializedServices.push(service as Services.ServiceInstance)
continue
}
const Service = (service as Services.ServicePlugin).default || service as Services.ServiceClass
if (typeof Service === 'function') {
initializedServices.push(new Service(serviceConfig, caps, config))
continue
}
}
return initializedServices
}
I/O Contract
initializePlugin
Import:
import { initializePlugin } from '@wdio/utils'
Inputs:
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Plugin name: short name ('browserstack'), scoped name ('@wdio/browserstack-service'), or absolute path
|
type |
string |
No | Plugin type: 'service', 'reporter', or 'runner'. Required if name is not scoped or absolute.
|
Output: Promise<Services.ServicePlugin | Services.RunnerPlugin> -- The imported plugin module
Errors:
'No plugin type provided'-- Whentypeis not a string and the name is not scoped or absolute'Couldn't find plugin "{name}" {type}...'-- When no naming convention yields a valid import
Resolution Order
| Step | Condition | Attempted Import |
|---|---|---|
| 1 | Name starts with @ |
Direct import of scoped package name |
| 2 | Name is an absolute path | Direct import via file:// URL
|
| 3 | Scoped convention | @wdio/{name.toLowerCase()}-{type}
|
| 4 | Community convention | wdio-{name.toLowerCase()}-{type}
|
| 5 | All failed | Throw descriptive error |
Related Functions
| Function | Signature | Purpose |
|---|---|---|
initializeLauncherService |
(config, caps) => { ignoredWorkerServices, launcherServices } |
Initialize services for the launcher process, track launcher-only services |
initializeWorkerService |
(config, caps, ignored) => ServiceInstance[] |
Initialize services for worker processes, filtering out launcher-only services |
executeHooksWithArgs |
Error)[]> | Execute all registered hook implementations, collecting results and errors |
Usage Examples
How WDIO uses initializePlugin internally:
// packages/wdio-utils/src/initializeServices.ts:L67-69
// When a service is specified as a string name:
log.debug(`initialize service "${serviceName}" as NPM package`)
const service = await initializePlugin(serviceName, 'service')
initializedServices.push([service as Services.ServiceClass, serviceConfig, serviceName])
Resolution example walkthrough:
Input: name='browserstack', type='service'
Step 1: name[0] !== '@' and not absolute path → skip direct import
Step 2: type is 'string' → continue
Step 3: safeImport('@wdio/browserstack-service') → module found → return module
Input: name='my-custom', type='service'
Step 1: name[0] !== '@' and not absolute path → skip direct import
Step 2: type is 'string' → continue
Step 3: safeImport('@wdio/my-custom-service') → undefined (not found)
Step 4: safeImport('wdio-my-custom-service') → undefined (not found)
Step 5: throw Error("Couldn't find plugin \"my-custom\" service...")
Input: name='@my-org/special-service', type='service'
Step 1: name[0] === '@' → safeImport('@my-org/special-service') → module found → return module
Configuration patterns that trigger resolution:
// wdio.conf.ts
export const config: WebdriverIO.Config = {
// All of these trigger initializePlugin:
services: [
'browserstack', // → @wdio/browserstack-service
'sauce', // → @wdio/sauce-service
'@my-org/custom-service', // → @my-org/custom-service (direct)
['testingbot', { tbTunnel: true }], // → @wdio/testingbot-service
],
reporters: [
'spec', // → @wdio/spec-reporter
'allure', // → @wdio/allure-reporter
]
}