UNPKG

@applitools/eyes-storybook

Version:
187 lines (167 loc) 5.36 kB
'use strict'; const {spawn} = require('child_process'); const EventEmitter = require('events'); const stripAnsi = require('strip-ansi'); const {lt} = require('semver'); class StorybookConnector extends EventEmitter { constructor({ storybookPath, storybookPort, storybookHost, storybookConfigDir, storybookStaticDir, isWindows, logger, sbArg, sbVersion, }) { super(); this._storybookPath = storybookPath; this._storybookPort = storybookPort; this._storybookHost = storybookHost; this._storybookConfigDir = storybookConfigDir; this._storybookStaticDir = storybookStaticDir; this._isWindows = isWindows; this._logger = logger; this._sbArg = sbArg; this._sbVersion = sbVersion; this._childProcess = null; this._version = 5; this._onStderr = data => { this.emit('stderr', this._bufferToString(data)); }; this._onStdout = data => { this.emit('stdout', this._bufferToString(data)); }; } async start(timeout) { this._doSpawan(this._version); return this._wait(timeout); } async kill() { if (!this._childProcess) { return; } try { if (this._isWindows) { spawn('taskkill', ['/pid', this._childProcess.pid, '/f', '/t']); } else { process.kill(this._childProcess.pid); } } catch (e) { this._logger.log("Can't kill child (Storybook) process.", e); } } async _wait(timeout) { return new Promise((resolve, reject) => { const removeListeners = () => { this.removeListener('stdout', successMessageListener); this.removeListener('stderr', portBusyListener); this.removeListener('stderr', successMessageListener); }; const portBusyListener = str => { if (str.includes('Error: listen EADDRINUSE')) { clearTimeout(timeoutID); removeListeners(); this.on('stderr', portBusyListener); reject(new Error('Storybook port already in use')); } }; const successMessageListener = str => { const isReady = stripAnsi(str).match( /Storybook \d{1,2}\.\d{1,2}\.\d{1,2}(.+)? started|Storybook started on =>|Storybook ready/, ); if (isReady) { clearTimeout(timeoutID); removeListeners(); resolve(); } }; this.on('stdout', successMessageListener); this.on('stderr', portBusyListener); this.on('stderr', successMessageListener); const minutes = Math.floor(timeout / 1000 / 60); const seconds = (timeout / 1000) % 60; const timeoutID = setTimeout( reject, timeout, `Storybook dev server didn't start after waiting ${minutes ? `${minutes} minutes` : ''}${ minutes && seconds ? ' and ' : '' }${ seconds ? `${seconds} seconds` : '' }.\nPlease consider setting a higher timeout using the --startStorybookServerTimeout option (in seconds). For example: eyes-storybook --startStorybookServerTimeout 600`, ); }); } _doSpawan(version) { let isLt8; try { isLt8 = lt(this._sbVersion, '8.0.0'); } catch (err) {} const args = [ '-p', this._storybookPort, '-h', this._storybookHost, '-c', this._storybookConfigDir, ]; if (this._storybookStaticDir && isLt8) { args.push('-s'); args.push(this._storybookStaticDir); } if (version >= 4) { args.push('--ci'); } if (this._sbArg) { args.unshift(this._sbArg); } const spawnOptions = {detached: false}; if (this._isWindows) { if (/\s/.test(this._storybookPath)) { this._storybookPath = `"${this._storybookPath}"`; } } this._logger.log(`storybook spawn path:${this._storybookPath} ${args.join(' ')}`); // Storybook has an issue when running with Node.js v18 - https://github.com/storybookjs/storybook/issues/20482 if (parseInt(process.versions.node) > 16) { this._logger.log( 'Adding --openssl-legacy-provider to NODE_OPTIONS for Storybook compatibility with Node.js v18+', ); spawnOptions.env = { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --openssl-legacy-provider`.trim(), }; if (this._isWindows) { Object.assign(spawnOptions, {shell: true}); } } this._childProcess = spawn(this._storybookPath, args, spawnOptions); this._addListeners(); this._childProcess.once('exit', code => { if (!code) return; this._removeListeners(); this._childProcess = null; if (this._version === 5) { this._logger.log('failed to start storybook, retrying lower version.'); this._version = 3; this._doSpawan(this._version); } else { this._logger.log('failed to start storybook.'); this.emit('failure'); } }); } _bufferToString(data) { return data.toString('utf8').trim(); } _removeListeners() { this._childProcess.stdout.removeListener('data', this._onStdout); this._childProcess.stderr.removeListener('data', this._onStderr); } _addListeners() { this._childProcess.stdout.on('data', this._onStdout); this._childProcess.stderr.on('data', this._onStderr); } } module.exports = StorybookConnector;