Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for a JSON configuration file #814

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ jobs:
build:
docker:
- image: cimg/node:16.18.0
resource_class: xlarge

working_directory: ~/repo

Expand Down
14 changes: 8 additions & 6 deletions node-src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import invalidPackageJson from './ui/messages/errors/invalidPackageJson';
import noPackageJson from './ui/messages/errors/noPackageJson';
import { getBranch, getCommit, getSlug, getUserEmail, getUncommittedHash } from './git/git';
import { emailHash } from './lib/emailHash';

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

export type { Flags, Options, TaskName, Context } from './types';
export type { Flags, Options, TaskName, Context, Configuration } from './types';
tmeasday marked this conversation as resolved.
Show resolved Hide resolved

export async function run({
argv = [],
Expand Down Expand Up @@ -81,8 +80,9 @@ export async function run({
setExitCode(ctx, exitCodes.OK);

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

await runAll(ctx, options);
await runAll(ctx);

return {
// Keep this in sync with the configured outputs in action.yml
Expand All @@ -102,15 +102,15 @@ export async function run({
};
}

export async function runAll(ctx, options?: Partial<Options>) {
export async function runAll(ctx) {
// Run these in parallel; neither should ever reject
await Promise.all([runBuild(ctx, options), checkForUpdates(ctx)]);
await Promise.all([runBuild(ctx), checkForUpdates(ctx)]);

if (ctx.exitCode === 0 || ctx.exitCode === 1) {
await checkPackageJson(ctx);
}

if (ctx.flags.diagnostics) {
if (ctx.options.diagnostics) {
await writeChromaticDiagnostics(ctx);
}
}
Expand Down Expand Up @@ -147,3 +147,5 @@ export async function getGitInfo(): Promise<GitInfo> {
userEmailHash,
};
}

export { getConfiguration } from './lib/getConfiguration';
2 changes: 2 additions & 0 deletions node-src/lib/compareBaseline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { compareBaseline } from './compareBaseline';
import { getDependencies } from './getDependencies';
import TestLogger from './testLogger';

jest.setTimeout(30 * 1000);

const getContext: any = (baselineCommits: string[]) => ({
log: new TestLogger(),
git: { baselineCommits },
Expand Down
53 changes: 53 additions & 0 deletions node-src/lib/getConfiguration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { readFile } from 'jsonfile';
import { getConfiguration } from './getConfiguration';

jest.mock('jsonfile');
const mockedReadFile = jest.mocked(readFile);

beforeEach(() => {
mockedReadFile.mockReset();
});

it('reads configuration successfully', async () => {
mockedReadFile.mockResolvedValue({ projectToken: 'json-file-token' });

expect(await getConfiguration()).toEqual({ projectToken: 'json-file-token' });
});

it('reads from chromatic.config.json by default', async () => {
mockedReadFile.mockResolvedValue({ projectToken: 'json-file-token' }).mockClear();
await getConfiguration();

expect(mockedReadFile).toHaveBeenCalledWith('chromatic.config.json');
});

it('can read from a different location', async () => {
mockedReadFile.mockResolvedValue({ projectToken: 'json-file-token' }).mockClear();
await getConfiguration('test.file');

expect(mockedReadFile).toHaveBeenCalledWith('test.file');
});

it('returns nothing if there is no config file and it was not specified', async () => {
mockedReadFile.mockRejectedValue(new Error('ENOENT'));

expect(await getConfiguration()).toEqual({});
});

it('returns nothing if there is no config file and it was specified', async () => {
mockedReadFile.mockRejectedValue(new Error('ENOENT'));

await expect(getConfiguration('test.file')).rejects.toThrow(/could not be found/);
});

it('errors if config file contains invalid data', async () => {
mockedReadFile.mockResolvedValue({ projectToken: 1 });

await expect(getConfiguration('test.file')).rejects.toThrow(/projectToken/);
});

it('errors if config file contains unknown keys', async () => {
mockedReadFile.mockResolvedValue({ random: 1 });

await expect(getConfiguration('test.file')).rejects.toThrow(/random/);
});
63 changes: 63 additions & 0 deletions node-src/lib/getConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { readFile } from 'jsonfile';
import { ZodError, z } from 'zod';
import { missingConfigurationFile } from '../ui/messages/errors/missingConfigurationFile';
import { unparseableConfigurationFile } from '../ui/messages/errors/unparseableConfigurationFile';
import { invalidConfigurationFile } from '../ui/messages/errors/invalidConfigurationFile';

const configurationSchema = z
.object({
projectId: z.string(),
projectToken: z.string(), // deprecated

onlyChanged: z.union([z.string(), z.boolean()]),
onlyStoryFiles: z.array(z.string()),
onlyStoryNames: z.array(z.string()),
untraced: z.array(z.string()),
externals: z.array(z.string()),
debug: z.boolean(),
diagnostics: z.union([z.string(), z.boolean()]),
junitReport: z.union([z.string(), z.boolean()]),
tmeasday marked this conversation as resolved.
Show resolved Hide resolved
zip: z.boolean(),
autoAcceptChanges: z.union([z.string(), z.boolean()]),
exitZeroOnChanges: z.union([z.string(), z.boolean()]),
exitOnceUploaded: z.union([z.string(), z.boolean()]),
ignoreLastBuildOnBranch: z.string(),

buildScriptName: z.string(),
outputDir: z.string(),

storybookBuildDir: z.string(),
storybookBaseDir: z.string(),
storybookConfigDir: z.string(),
})
.partial()
.strict();

export type Configuration = z.infer<typeof configurationSchema>;

export async function getConfiguration(configFile?: string) {
const usedConfigFile = configFile || 'chromatic.config.json';
try {
const rawJson = await readFile(usedConfigFile);

return configurationSchema.parse(rawJson);
} catch (err) {
// Config file does not exist
if (err.message.match(/ENOENT/)) {
// The user passed no configFile option so it's OK for the file not to exist
if (!configFile) {
return {};
}
if (configFile) {
throw new Error(missingConfigurationFile(configFile));
}
}
if (err.message.match('Unexpected string')) {
throw new Error(unparseableConfigurationFile(usedConfigFile, err));
}
if (err instanceof ZodError) {
throw new Error(invalidConfigurationFile(usedConfigFile, err));
}
throw err;
}
}
2 changes: 2 additions & 0 deletions node-src/lib/getDependencies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import packageJson from '../../package.json';
import { checkoutFile } from '../git/git';
import TestLogger from './testLogger';

jest.setTimeout(30 * 1000);

const ctx = { log: new TestLogger() } as any;

describe('getDependencies', () => {
Expand Down
48 changes: 43 additions & 5 deletions node-src/lib/getOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ describe('getOptions', () => {
projectToken: 'cli-code',
buildScriptName: 'build-storybook',
fromCI: !!process.env.CI,
autoAcceptChanges: undefined,
exitZeroOnChanges: undefined,
exitOnceUploaded: undefined,
autoAcceptChanges: false,
exitZeroOnChanges: false,
exitOnceUploaded: false,
interactive: false,
verbose: false,
debug: false,
originalArgv: ['--project-token', 'cli-code'],
});
});
Expand Down Expand Up @@ -68,7 +68,7 @@ describe('getOptions', () => {
autoAcceptChanges: true,
exitZeroOnChanges: true,
exitOnceUploaded: true,
verbose: true,
debug: true,
interactive: false,
});
});
Expand Down Expand Up @@ -123,6 +123,44 @@ describe('getOptions', () => {
const flags = ['--only-changed', '--externals', 'foo', '--externals', '', '--externals', 'bar'];
expect(getOptions(getContext(flags))).toMatchObject({ externals: ['foo', 'bar'] });
});

it('allows you to set options with configuration', async () => {
expect(
getOptions({ ...getContext([]), configuration: { projectToken: 'config-token' } })
).toMatchObject({
projectToken: 'config-token',
});
});

it('allows you to override configuration with flags', async () => {
expect(
getOptions({
...getContext(['--project-token', 'cli-token']),
configuration: { projectToken: 'config-token' },
})
).toMatchObject({
projectToken: 'cli-token',
});
});

it('allows you to set options with extraOptions', async () => {
expect(
getOptions({ ...getContext([]), extraOptions: { projectToken: 'extra-token' } })
).toMatchObject({
projectToken: 'extra-token',
});
});

it('allows you to override flags with extraOptions', async () => {
expect(
getOptions({
...getContext(['--project-token', 'cli-token']),
extraOptions: { projectToken: 'extra-token' },
})
).toMatchObject({
projectToken: 'extra-token',
});
});
});

describe('getStorybookConfiguration', () => {
Expand Down
Loading
Loading