diff --git a/bin/tasks/build.js b/bin/tasks/build.js index 52737b9b6..c3e653467 100644 --- a/bin/tasks/build.js +++ b/bin/tasks/build.js @@ -3,6 +3,7 @@ import fs from 'fs'; import path from 'path'; import semver from 'semver'; import tmp from 'tmp-promise'; +import yarnOrNpm, { hasYarn } from 'yarn-or-npm'; import { createTask, transitionTo } from '../lib/tasks'; import buildFailed from '../ui/messages/errors/buildFailed'; @@ -21,22 +22,21 @@ export const setSourceDir = async (ctx) => { }; export const setSpawnParams = (ctx) => { - // Run either: - // npm/yarn run scriptName (depending on npm_execpath) - // node path/to/npm.js run scriptName (if npm run via node) - // Based on https://github.com/mysticatea/npm-run-all/blob/52eaf86242ba408dedd015f53ca7ca368f25a026/lib/run-task.js#L156-L174 - const npmExecPath = process.env.npm_execpath; - const isJsPath = typeof npmExecPath === 'string' && /\.m?js/.test(path.extname(npmExecPath)); - const isYarn = npmExecPath && /^yarn(\.js)?$/.test(path.basename(npmExecPath)); ctx.spawnParams = { - command: (isJsPath ? process.execPath : npmExecPath) || 'npm', - clientArgs: isJsPath ? [npmExecPath, 'run'] : ['run', '--silent'], + client: yarnOrNpm(), + platform: process.platform, + command: yarnOrNpm(), + clientArgs: ['run', '--silent'], scriptArgs: [ ctx.options.buildScriptName, - isYarn ? '' : '--', + hasYarn() ? '' : '--', '--output-dir', ctx.sourceDir, ].filter(Boolean), + spawnOptions: { + preferLocal: true, + localDir: path.resolve('node_modules/.bin'), + }, }; }; @@ -52,9 +52,13 @@ export const buildStorybook = async (ctx) => { }); try { - const { command, clientArgs, scriptArgs } = ctx.spawnParams; + const { command, clientArgs, scriptArgs, spawnOptions } = ctx.spawnParams; + ctx.log.debug('Using spawnParams:', JSON.stringify(ctx.spawnParams, null, 2)); await Promise.race([ - execa(command, [...clientArgs, ...scriptArgs], { stdio: [null, logFile, logFile] }), + execa(command, [...clientArgs, ...scriptArgs], { + stdio: [null, logFile, logFile], + ...spawnOptions, + }), timeoutAfter(ctx.env.STORYBOOK_BUILD_TIMEOUT), ]); } catch (e) { diff --git a/bin/tasks/build.test.js b/bin/tasks/build.test.js index fbc1d7a76..f15f14007 100644 --- a/bin/tasks/build.test.js +++ b/bin/tasks/build.test.js @@ -36,9 +36,32 @@ describe('setSpawnParams', () => { const ctx = { sourceDir: './source-dir/', options: { buildScriptName: 'build:storybook' } }; await setSpawnParams(ctx); expect(ctx.spawnParams).toEqual({ + client: 'npm', + platform: expect.stringMatching(/darwin|linux|win32/), command: 'npm', clientArgs: ['run', '--silent'], scriptArgs: ['build:storybook', '--', '--output-dir', './source-dir/'], + spawnOptions: { + preferLocal: true, + localDir: expect.stringMatching(/node_modules[/\\]\.bin$/), + }, + }); + }); + + it('supports yarn', async () => { + process.env.npm_execpath = '/path/to/yarn'; + const ctx = { sourceDir: './source-dir/', options: { buildScriptName: 'build:storybook' } }; + await setSpawnParams(ctx); + expect(ctx.spawnParams).toEqual({ + client: 'yarn', + platform: expect.stringMatching(/darwin|linux|win32/), + command: '/path/to/yarn', + clientArgs: ['run', '--silent'], + scriptArgs: ['build:storybook', '--output-dir', './source-dir/'], + spawnOptions: { + preferLocal: true, + localDir: expect.stringMatching(/node_modules[/\\]\.bin$/), + }, }); }); @@ -63,6 +86,7 @@ describe('buildStorybook', () => { scriptArgs: ['--script-args'], }, env: { STORYBOOK_BUILD_TIMEOUT: 1000 }, + log: { debug: jest.fn() }, }; await buildStorybook(ctx); expect(ctx.buildLogFile).toMatch(/build-storybook\.log$/); @@ -71,6 +95,10 @@ describe('buildStorybook', () => { ['--client-args', '--script-args'], expect.objectContaining({ stdio: expect.any(Array) }) ); + expect(ctx.log.debug).toHaveBeenCalledWith( + 'Using spawnParams:', + JSON.stringify(ctx.spawnParams, null, 2) + ); }); it('fails when build times out', async () => { @@ -82,7 +110,7 @@ describe('buildStorybook', () => { }, options: { buildScriptName: '' }, env: { STORYBOOK_BUILD_TIMEOUT: 0 }, - log: { error: jest.fn() }, + log: { debug: jest.fn(), error: jest.fn() }, }; execa.mockReturnValue(new Promise((resolve) => setTimeout(resolve, 10))); await expect(buildStorybook(ctx)).rejects.toThrow('Command failed'); diff --git a/bin/ui/messages/errors/fatalError.js b/bin/ui/messages/errors/fatalError.js index a1e62badc..3cc33a777 100644 --- a/bin/ui/messages/errors/fatalError.js +++ b/bin/ui/messages/errors/fatalError.js @@ -14,7 +14,7 @@ export default function fatalError(ctx, error, timestamp = new Date().toISOStrin const website = link(pkg.docs); const errors = [].concat(error); - const { git = {}, storybook, exitCode, isolatorUrl, cachedUrl, build } = ctx; + const { git = {}, storybook, spawnParams, exitCode, isolatorUrl, cachedUrl, build } = ctx; const debugInfo = { timestamp, sessionId, @@ -27,6 +27,7 @@ export default function fatalError(ctx, error, timestamp = new Date().toISOStrin flags, ...(options.buildScriptName ? { buildScript: scripts[options.buildScriptName] } : {}), ...(options.scriptName ? { storybookScript: scripts[options.scriptName] } : {}), + ...(spawnParams ? { spawnParams } : {}), exitCode, errorType: errors.map((err) => err.name).join('\n'), errorMessage: stripAnsi(errors[0].message.split('\n')[0].trim()), diff --git a/bin/ui/tasks/build.js b/bin/ui/tasks/build.js index ecc452099..2c6c12064 100644 --- a/bin/ui/tasks/build.js +++ b/bin/ui/tasks/build.js @@ -1,5 +1,8 @@ import { getDuration } from '../../lib/tasks'; +const fullCommand = ({ command, clientArgs, scriptArgs }) => + [command, ...clientArgs, ...scriptArgs].join(' '); + export const initial = { status: 'initial', title: 'Build Storybook', @@ -7,8 +10,8 @@ export const initial = { export const pending = (ctx) => ({ status: 'pending', - title: 'Building your Storybook', - output: `Running command: ${ctx.spawnParams.scriptArgs.join(' ')}`, + title: `Building your Storybook`, + output: `Running command: ${fullCommand(ctx.spawnParams)}`, }); export const success = (ctx) => ({ @@ -25,6 +28,6 @@ export const skipped = (ctx) => ({ export const failed = (ctx) => ({ status: 'error', - title: 'Building your Storybook', - output: `Command failed: ${ctx.spawnParams.scriptArgs.join(' ')}`, + title: `Building your Storybook`, + output: `Command failed: ${fullCommand(ctx.spawnParams)}`, }); diff --git a/bin/ui/tasks/build.stories.js b/bin/ui/tasks/build.stories.js index 465fb1ab2..1ad11c03b 100644 --- a/bin/ui/tasks/build.stories.js +++ b/bin/ui/tasks/build.stories.js @@ -1,17 +1,20 @@ import task from '../components/task'; -import { initial, pending, skipped, success } from './build'; +import { initial, pending, skipped, success, failed } from './build'; export default { title: 'CLI/Tasks/Build', decorators: [(storyFn) => task(storyFn())], }; +const spawnParams = { + command: 'yarn', + clientArgs: ['run', '--silent'], + scriptArgs: ['build-storybook', '-o', 'storybook-static'], +}; + export const Initial = () => initial; -export const Building = () => - pending({ - spawnParams: { scriptArgs: ['build-storybook', '-o', 'storybook-static'] }, - }); +export const Building = () => pending({ spawnParams }); export const Built = () => success({ @@ -24,3 +27,5 @@ export const Skipped = () => skipped({ options: { storybookBuildDir: '/users/me/project/storybook-static' }, }); + +export const Failed = () => failed({ spawnParams }); diff --git a/package.json b/package.json index e1d220bc1..9bbd1c9c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chromatic", - "version": "5.7.1", + "version": "5.8.0-debug.2", "description": "Automate visual testing across browsers. Gather UI feedback. Versioned documentation.", "homepage": "https://www.chromatic.com", "bugs": {