Skip to content

Commit

Permalink
Merge pull request #840 from chromaui/fix-diagnostics-flag
Browse files Browse the repository at this point in the history
Fix reading `diagnostics` from undefined
  • Loading branch information
ghengeveld authored Oct 23, 2023
2 parents 58a5b74 + 733d456 commit 9a3872d
Show file tree
Hide file tree
Showing 18 changed files with 289 additions and 273 deletions.
182 changes: 90 additions & 92 deletions node-src/main.test.ts → node-src/index.test.ts

Large diffs are not rendered by default.

165 changes: 143 additions & 22 deletions node-src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
import 'any-observable/register/zen';
import Listr from 'listr';
import readPkgUp from 'read-pkg-up';
import { v4 as uuid } from 'uuid';
import 'any-observable/register/zen';

import { getBranch, getCommit, getSlug, getUncommittedHash, getUserEmail } from './git/git';
import GraphQLClient from './io/GraphQLClient';
import HTTPClient from './io/HTTPClient';
import NonTTYRenderer from './lib/NonTTYRenderer';
import checkForUpdates from './lib/checkForUpdates';
import checkPackageJson from './lib/checkPackageJson';
import { emailHash } from './lib/emailHash';
import { getConfiguration } from './lib/getConfiguration';
import getEnv from './lib/getEnv';
import getOptions from './lib/getOptions';
import { createLogger } from './lib/log';
import parseArgs from './lib/parseArgs';
import { Context, Flags, Options } from './types';
import { exitCodes, setExitCode } from './lib/setExitCode';
import { runBuild } from './runBuild';
import checkForUpdates from './lib/checkForUpdates';
import checkPackageJson from './lib/checkPackageJson';
import { rewriteErrorMessage } from './lib/utils';
import { writeChromaticDiagnostics } from './lib/writeChromaticDiagnostics';
import getTasks from './tasks';
import { Context, Flags, Options } from './types';
import { endActivity } from './ui/components/activity';
import buildCanceled from './ui/messages/errors/buildCanceled';
import { default as fatalError } from './ui/messages/errors/fatalError';
import fetchError from './ui/messages/errors/fetchError';
import graphqlError from './ui/messages/errors/graphqlError';
import invalidPackageJson from './ui/messages/errors/invalidPackageJson';
import missingStories from './ui/messages/errors/missingStories';
import noPackageJson from './ui/messages/errors/noPackageJson';
import { getBranch, getCommit, getSlug, getUserEmail, getUncommittedHash } from './git/git';
import { emailHash } from './lib/emailHash';
import runtimeError from './ui/messages/errors/runtimeError';
import taskError from './ui/messages/errors/taskError';
import intro from './ui/messages/info/intro';

/**
Make keys of `T` outside of `R` optional.
*/
Expand All @@ -37,12 +53,32 @@ interface Output {
inheritedCaptureCount: number;
}

export type { Flags, Options, TaskName, Context, Configuration } from './types';
export type { Configuration, Context, Flags, Options, TaskName } from './types';

export type InitialContext = Omit<
AtLeast<
Context,
| 'argv'
| 'flags'
| 'help'
| 'pkg'
| 'extraOptions'
| 'packagePath'
| 'packageJson'
| 'env'
| 'log'
| 'sessionId'
>,
'options'
>;

const isContext = (ctx: InitialContext): ctx is Context => 'options' in ctx;

// Entry point for the CLI, GitHub Action, and Node API
export async function run({
argv = [],
flags,
options,
options: extraOptions,
}: {
argv?: string[];
flags?: Flags;
Expand All @@ -64,24 +100,17 @@ export async function run({
process.exit(252);
}

const ctx: AtLeast<
Context,
'argv' | 'flags' | 'help' | 'pkg' | 'packagePath' | 'packageJson' | 'env' | 'log' | 'sessionId'
> = {
const ctx: InitialContext = {
...parseArgs(argv),
...(flags && { flags }),
...(extraOptions && { extraOptions }),
packagePath,
packageJson,
env,
log,
sessionId,
...(flags && { flags }),
};

setExitCode(ctx, exitCodes.OK);

ctx.http = (ctx.http as HTTPClient) || new HTTPClient(ctx);
ctx.extraOptions = options;

await runAll(ctx);

return {
Expand All @@ -102,11 +131,47 @@ export async function run({
};
}

export async function runAll(ctx) {
// Entry point for testing only (typically invoked via `run` above)
export async function runAll(ctx: InitialContext) {
ctx.log.info('');
ctx.log.info(intro(ctx));

const onError = (e: Error | Error[]) => {
ctx.log.info('');
ctx.log.error(fatalError(ctx, [].concat(e)));
ctx.extraOptions?.experimental_onTaskError?.(ctx, {
formattedError: fatalError(ctx, [].concat(e)),
originalError: e,
});
setExitCode(ctx, exitCodes.INVALID_OPTIONS, true);
};

try {
ctx.http = new HTTPClient(ctx);
ctx.client = new GraphQLClient(ctx, `${ctx.env.CHROMATIC_INDEX_URL}/graphql`, {
headers: {
'x-chromatic-session-id': ctx.sessionId,
'x-chromatic-cli-version': ctx.pkg.version,
},
retries: 3,
});
ctx.configuration = await getConfiguration(
ctx.extraOptions?.configFile || ctx.flags.configFile
);
(ctx as Context).options = getOptions(ctx);
setExitCode(ctx, exitCodes.OK);
} catch (e) {
return onError(e);
}

if (!isContext(ctx)) {
return onError(new Error('Invalid context'));
}

// Run these in parallel; neither should ever reject
await Promise.all([runBuild(ctx), checkForUpdates(ctx)]);
await Promise.all([runBuild(ctx), checkForUpdates(ctx)]).catch(onError);

if (ctx.exitCode === 0 || ctx.exitCode === 1) {
if ([0, 1].includes(ctx.exitCode)) {
await checkPackageJson(ctx);
}

Expand All @@ -115,6 +180,62 @@ export async function runAll(ctx) {
}
}

async function runBuild(ctx: Context) {
try {
try {
ctx.log.info('');
if (ctx.options.interactive) ctx.log.queue(); // queue up any log messages while Listr is running
const options = ctx.options.interactive ? {} : { renderer: NonTTYRenderer, log: ctx.log };
await new Listr(getTasks(ctx.options), options).run(ctx);
} catch (err) {
endActivity(ctx);
if (err.code === 'ECONNREFUSED' || err.name === 'StatusCodeError') {
setExitCode(ctx, exitCodes.FETCH_ERROR);
throw rewriteErrorMessage(err, fetchError(ctx, err));
}
if (err.name === 'GraphQLError') {
setExitCode(ctx, exitCodes.GRAPHQL_ERROR);
throw rewriteErrorMessage(err, graphqlError(ctx, err));
}
if (err.message.startsWith('Cannot run a build with no stories')) {
setExitCode(ctx, exitCodes.BUILD_NO_STORIES);
throw rewriteErrorMessage(err, missingStories(ctx));
}
if (ctx.options.experimental_abortSignal?.aborted) {
setExitCode(ctx, exitCodes.BUILD_WAS_CANCELED, true);
throw rewriteErrorMessage(err, buildCanceled());
}
throw rewriteErrorMessage(err, taskError(ctx, err));
} finally {
// Handle potential runtime errors from JSDOM
const { runtimeErrors, runtimeWarnings } = ctx;
if ((runtimeErrors && runtimeErrors.length) || (runtimeWarnings && runtimeWarnings.length)) {
ctx.log.info('');
ctx.log.error(runtimeError(ctx));
}

ctx.log.flush();
}
} catch (error) {
const errors = [].concat(error); // GraphQLClient might throw an array of errors
const formattedError = fatalError(ctx, errors);

ctx.options.experimental_onTaskError?.(ctx, {
formattedError,
originalError: errors[0],
});

if (!ctx.userError) {
ctx.log.info('');
ctx.log.error(formattedError);
}

if (!ctx.exitCode) {
setExitCode(ctx, exitCodes.UNKNOWN_ERROR);
}
}
}

export type GitInfo = {
slug: string;
branch: string;
Expand Down
16 changes: 7 additions & 9 deletions node-src/io/GraphQLClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import retry from 'async-retry';

import { InitialContext } from '..';
import HTTPClient, { HTTPClientOptions } from './HTTPClient';
import { Context } from '../types';

const RETRYABLE_ERROR_CODE = 'RETRYABLE_ERROR_CODE';

Expand All @@ -16,27 +16,25 @@ export interface GraphQLError {

export default class GraphQLClient {
endpoint: string;

client: HTTPClient;

headers: HTTPClientOptions['headers'];
client: HTTPClient;

constructor(context: Context, endpoint: string, httpClientOptions: HTTPClientOptions) {
constructor(ctx: InitialContext, endpoint: string, httpClientOptions: HTTPClientOptions) {
if (!endpoint) throw new Error('Option `endpoint` required.');
this.endpoint = endpoint;
this.client = new HTTPClient(context, httpClientOptions);
this.client = new HTTPClient(ctx, httpClientOptions);
this.headers = { 'Content-Type': 'application/json' };
}

setAuthorization(token) {
setAuthorization(token: string) {
this.headers.Authorization = `Bearer ${token}`;
}

async runQuery(
async runQuery<T>(
query: string,
variables: Record<string, any>,
{ headers = {}, retries = 2 } = {}
) {
): Promise<T> {
return retry(
async (bail) => {
const { data, errors } = await this.client
Expand Down
2 changes: 1 addition & 1 deletion node-src/io/HTTPClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface HTTPClientOptions {

export interface HTTPClientFetchOptions {
noLogErrorBody?: boolean;
proxy?: HttpsProxyAgentOptions;
proxy?: HttpsProxyAgentOptions<any>;
retries?: number;
}

Expand Down
15 changes: 3 additions & 12 deletions node-src/io/getProxyAgent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import createHttpsProxyAgent, { HttpsProxyAgentOptions } from 'https-proxy-agent';
import { HttpsProxyAgent, HttpsProxyAgentOptions } from 'https-proxy-agent';
import noProxy from 'no-proxy';
import { URL } from 'url';
import { Context } from '../types';
Expand All @@ -8,24 +8,15 @@ const agents = {};
const getProxyAgent = (
{ env, log }: Pick<Context, 'env' | 'log'>,
url: string,
options: HttpsProxyAgentOptions
options: HttpsProxyAgentOptions<any>
) => {
const proxy = env.HTTPS_PROXY || env.HTTP_PROXY;
if (!proxy || noProxy(url)) return undefined;

log.debug({ url, proxy, options }, 'Using proxy agent');
const requestHost = new URL(url).host;
if (!agents[requestHost]) {
const { hostname, port, protocol, username, password, pathname } = new URL(proxy);
const auth = username && password ? `${username}:${password}` : undefined;
agents[requestHost] = createHttpsProxyAgent({
auth,
hostname,
port,
protocol,
pathname,
...options,
});
agents[requestHost] = new HttpsProxyAgent(proxy, options);
}
return agents[requestHost];
};
Expand Down
9 changes: 4 additions & 5 deletions node-src/lib/checkForUpdates.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import semver from 'semver';
import { hasYarn } from 'yarn-or-npm';

import spawn from './spawn';
import { Context } from '../types';

import { InitialContext } from '..';
import outdatedPackage from '../ui/messages/warnings/outdatedPackage';
import spawn from './spawn';

const rejectIn = (ms: number) => new Promise<any>((_, reject) => setTimeout(reject, ms));
const withTimeout = <T>(promise: Promise<T>, ms: number): Promise<T> =>
Promise.race([promise, rejectIn(ms)]);

export default async function checkForUpdates(ctx: Context) {
export default async function checkForUpdates(ctx: InitialContext) {
if (!semver.valid(ctx.pkg.version)) {
ctx.log.warn(`Invalid semver version in package.json: ${ctx.pkg.version}`);
return;
Expand All @@ -35,7 +34,7 @@ export default async function checkForUpdates(ctx: Context) {
latestVersion = distTags.latest;
} catch (e) {
ctx.log.warn(`Could not retrieve package info from registry; skipping update check`);
ctx.log.debug(e);
ctx.log.warn(e);
return;
}

Expand Down
2 changes: 1 addition & 1 deletion node-src/lib/checkPackageJson.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import jsonfile from 'jsonfile';
import { confirm } from 'node-ask';
import { Context } from '../types';

import { Context } from '..';
import addedScript from '../ui/messages/info/addedScript';
import notAddedScript from '../ui/messages/info/notAddedScript';
import scriptNotFound from '../ui/messages/warnings/scriptNotFound';
Expand Down
2 changes: 1 addition & 1 deletion node-src/lib/getConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const configurationSchema = z
untraced: z.array(z.string()),
externals: z.array(z.string()),
debug: z.boolean(),
diagnostics: z.union([z.string(), z.boolean()]),
diagnostics: z.boolean(),
junitReport: z.union([z.string(), z.boolean()]),
zip: z.boolean(),
autoAcceptChanges: z.union([z.string(), z.boolean()]),
Expand Down
4 changes: 2 additions & 2 deletions node-src/lib/getOptions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';
import { Context, Options } from '../types';

import { InitialContext, Options } from '..';
import dependentOption from '../ui/messages/errors/dependentOption';
import duplicatePatchBuild from '../ui/messages/errors/duplicatePatchBuild';
import incompatibleOptions from '../ui/messages/errors/incompatibleOptions';
Expand Down Expand Up @@ -36,7 +36,7 @@ export default function getOptions({
configuration,
log,
packageJson,
}: Context): Options {
}: InitialContext): Options {
const defaultOptions = {
projectToken: env.CHROMATIC_PROJECT_TOKEN,
fromCI: !!process.env.CI,
Expand Down
3 changes: 2 additions & 1 deletion node-src/lib/parseArgs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import meow from 'meow';

import pkg from '../../package.json';
import { Flags } from '../types';

export default function parseArgs(argv: string[]) {
const { input, flags, help } = meow(
Expand Down Expand Up @@ -107,5 +108,5 @@ export default function parseArgs(argv: string[]) {
}
);

return { argv, input, flags, help, pkg };
return { argv, input, flags: flags as Flags, help, pkg };
}
3 changes: 2 additions & 1 deletion node-src/lib/writeChromaticDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import jsonfile from 'jsonfile';
import { Context } from '../types';

import { Context } from '..';
import wroteReport from '../ui/messages/info/wroteReport';

const { writeFile } = jsonfile;
Expand Down
Loading

0 comments on commit 9a3872d

Please sign in to comment.