UNPKG

creevey

Version:

Cross-browser screenshot testing tool for Storybook with fancy UI Runner

137 lines (121 loc) 4.11 kB
import cluster from 'cluster'; import ora from 'ora'; import { Writable } from 'stream'; import Dockerode, { Container } from 'dockerode'; import { Config, BrowserConfig, isDockerMessage, DockerAuth } from '../types.js'; import { subscribeOn, sendDockerMessage, emitDockerMessage } from './messages.js'; import { isInsideDocker, LOCALHOST_REGEXP } from './utils.js'; import { logger } from './logger.js'; const docker = new Dockerode(); class DevNull extends Writable { _write(_chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null | undefined) => void): void { setImmediate(callback); } } export async function pullImages( images: string[], { auth, platform }: { auth?: DockerAuth; platform?: string } = {}, ): Promise<void> { const args: Record<string, unknown> = {}; if (auth) args.authconfig = auth; if (platform) args.platform = platform; logger().info('Pull docker images'); for (const image of images) { await new Promise<void>((resolve, reject) => { const spinner = ora(`${image}: Pull start`).start(); docker.pull(image, args, (pullError: Error | null, stream?: NodeJS.ReadableStream) => { if (pullError || !stream) { spinner.fail(); reject(pullError ?? new Error('Unknown error')); return; } docker.modem.followProgress(stream, onFinished, onProgress); function onFinished(error: Error | null): void { if (error) { spinner.fail(); reject(error); return; } spinner.succeed(`${image}: Pull complete`); resolve(); } function onProgress(event: { id: string; status: string; progress?: string }): void { if (!/^[a-z0-9]{12}$/i.test(event.id)) return; spinner.text = `${image}: [${event.id}] ${event.status} ${event.progress ? event.progress : ''}`; } }); }); } } export async function runImage( image: string, args: string[], options: Record<string, unknown>, debug: boolean, ): Promise<string> { await Promise.all( (await docker.listContainers({ all: true, filters: { ancestor: [image] } })).map(async (info) => { const container = docker.getContainer(info.Id); try { await container.stop(); } catch { /* noop */ } await container.remove(); }), ); const hub = docker.run(image, args, debug ? process.stdout : new DevNull(), options, (error) => { if (error) throw error; }); return new Promise((resolve) => { hub.once('container', (container: Container) => { // eslint-disable-next-line @typescript-eslint/no-misused-promises subscribeOn('shutdown', async () => { try { await container.stop(); await container.remove(); } catch { /* noop */ } }); }); hub.once( 'start', (container: Container) => void container.inspect().then((info) => { resolve(info.NetworkSettings.Networks.bridge.IPAddress); }), ); }); } export async function initDocker( config: Config, browser: string | undefined, startContainer: () => Promise<string>, ): Promise<void> { if (cluster.isPrimary) { const host = await startContainer(); let gridUrl = 'http://localhost:4444/wd/hub'; gridUrl = isInsideDocker ? gridUrl.replace(LOCALHOST_REGEXP, host) : gridUrl; cluster.on('message', (worker, message: unknown) => { if (!isDockerMessage(message)) return; const dockerMessage = message; if (dockerMessage.type != 'start') return; sendDockerMessage(worker, { type: 'success', payload: { gridUrl }, }); }); } else { if (browser && (config.browsers[browser] as BrowserConfig).gridUrl) return Promise.resolve(); return new Promise((resolve) => { subscribeOn('docker', (message) => { if (message.type == 'success') { config.gridUrl = message.payload.gridUrl; resolve(); } }); emitDockerMessage({ type: 'start' }); }); } }