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

Retrieve viewLayer and version from dependencies and support @web/dev-server-storybook #319

Merged
merged 7 commits into from Apr 23, 2021
171 changes: 102 additions & 69 deletions bin/lib/getStorybookInfo.js
@@ -1,63 +1,63 @@
// Figure out the Storybook version and view layer

import fs from 'fs-extra';
import semver from 'semver';

const viewLayers = [
'react',
'vue',
'vue3',
'angular',
'html',
'web-components',
'polymer',
'ember',
'marko',
'mithril',
'riot',
'svelte',
'preact',
'rax',
];

const supportedAddons = [
'a11y',
'actions',
'backgrounds',
'centered',
'contexts',
'cssresources',
'design-assets',
'docs',
'events',
'google-analytics',
'graphql',
'info',
'jest',
'knobs',
'links',
'notes',
'ondevice-actions',
'ondevice-backgrounds',
'ondevice-knobs',
'ondevice-notes',
'options',
'queryparams',
'storyshots',
'storysource',
'viewport',
];

const resolve = (name) => {
const viewLayers = {
'@storybook/react': 'react',
'@storybook/vue': 'vue',
'@storybook/vue3': 'vue3',
'@storybook/angular': 'angular',
'@storybook/html': 'html',
'@storybook/web-components': 'web-components',
'@storybook/polymer': 'polymer',
'@storybook/ember': 'ember',
'@storybook/marko': 'marko',
'@storybook/mithril': 'mithril',
'@storybook/riot': 'riot',
'@storybook/svelte': 'svelte',
'@storybook/preact': 'preact',
'@storybook/rax': 'rax',
'@web/dev-server-storybook': 'web-components',
};

const supportedAddons = {
'@storybook/addon-a11y': 'a11y',
'@storybook/addon-actions': 'actions',
'@storybook/addon-backgrounds': 'backgrounds',
'@storybook/addon-centered': 'centered',
'@storybook/addon-contexts': 'contexts',
'@storybook/addon-cssresources': 'cssresources',
'@storybook/addon-design-assets': 'design-assets',
'@storybook/addon-docs': 'docs',
'@storybook/addon-events': 'events',
'@storybook/addon-google-analytics': 'google-analytics',
'@storybook/addon-graphql': 'graphql',
'@storybook/addon-info': 'info',
'@storybook/addon-jest': 'jest',
'@storybook/addon-knobs': 'knobs',
'@storybook/addon-links': 'links',
'@storybook/addon-notes': 'notes',
'@storybook/addon-ondevice-actions': 'ondevice-actions',
'@storybook/addon-ondevice-backgrounds': 'ondevice-backgrounds',
'@storybook/addon-ondevice-knobs': 'ondevice-knobs',
'@storybook/addon-ondevice-notes': 'ondevice-notes',
'@storybook/addon-options': 'options',
'@storybook/addon-queryparams': 'queryparams',
'@storybook/addon-storyshots': 'storyshots',
'@storybook/addon-storysource': 'storysource',
'@storybook/addon-viewport': 'viewport',
};

const resolve = (pkg) => {
try {
const path = require.resolve(`@storybook/${name}/package.json`, { paths: [process.cwd()] });
const path = require.resolve(`${pkg}/package.json`, { paths: [process.cwd()] });
return Promise.resolve(path);
} catch (error) {
return Promise.reject(error);
}
};

const read = async (filepath) => JSON.parse(await fs.readFile(filepath, 'utf8'));

const timeout = (count) =>
new Promise((_, rej) => {
setTimeout(() => rej(new Error('The attempt to find the Storybook version timed out')), count);
Expand All @@ -66,31 +66,66 @@ const timeout = (count) =>
const neverResolve = new Promise(() => {});
const disregard = () => neverResolve;

const findViewlayer = async ({ env }) => {
// Allow setting Storybook version via CHROMATIC_STORYBOOK_VERSION='react@4.0-alpha.8' for unusual cases
const findViewlayer = async ({ env, log, options, packageJson }) => {
// Allow setting Storybook version via CHROMATIC_STORYBOOK_VERSION='@storybook/react@4.0-alpha.8' for unusual cases
if (env.CHROMATIC_STORYBOOK_VERSION) {
const [viewLayer, version] = env.CHROMATIC_STORYBOOK_VERSION.split('@');
if (!viewLayer || !version) {
throw new Error('CHROMATIC_STORYBOOK_VERSION was provided but could not be used');
const [, p, v] = env.CHROMATIC_STORYBOOK_VERSION.match(/(.+)@(.+)$/) || [];
const version = semver.valid(v); // ensures we get a specific version, not a range
if (!p || !version) {
throw new Error(
'Invalid CHROMATIC_STORYBOOK_VERSION; expecting something like "@storybook/react@6.2.0".'
);
}
const viewLayer = viewLayers[p] || viewLayers[`@storybook/${p}`];
if (!viewLayer) {
throw new Error(`Unsupported viewlayer specified in CHROMATIC_STORYBOOK_VERSION: ${p}`);
}
return { viewLayer, version };
}

// Try to find the Storybook viewlayer package
const findings = viewLayers.map((v) => resolve(v));
const rejectedFindings = findings.map((p) => p.then(disregard, () => true));
// Pull the viewlayer from dependencies in package.json
const dep = Object.entries(packageJson.dependencies || {}).find(([p]) => viewLayers[p]);
const devDep = Object.entries(packageJson.devDependencies || {}).find(([p]) => viewLayers[p]);
const peerDep = Object.entries(packageJson.peerDependencies || {}).find(([p]) => viewLayers[p]);
const dependency = dep || devDep || peerDep;

if (!dependency) {
throw new Error(
'Could not find a supported Storybook viewlayer package in your package.json dependencies. Make sure one is installed.'
);
ghengeveld marked this conversation as resolved.
Show resolved Hide resolved
}
if (dep && devDep && dep[0] === devDep[0]) {
log.warn(
`Found "${dep[0]}" in both "dependencies" and "devDependencies". This is probably a mistake.`
);
}
if (dep && peerDep && dep[0] === peerDep[0]) {
log.warn(
`Found "${dep[0]}" in both "dependencies" and "peerDependencies". This is probably a mistake.`
);
}

const [pkg, version] = dependency;
const viewLayer = pkg.replace('@storybook/', '');

// If we won't need the Storybook CLI, we can exit early
// Note that `version` can be a semver range in this case.
if (options.storybookBuildDir) return { viewLayer, version };

// Try to find the viewlayer package in node_modules so we know it's installed
const findings = Object.entries(viewLayers).map(([pk, name]) => [resolve(pk), name]);
const rejectedFindings = findings.map(([p]) => p.then(disregard, () => true));
const allFailed = Promise.all(rejectedFindings).then(() => {
throw new Error(
'Could not find a supported Storybook viewlayer package. Make sure one is installed, or set CHROMATIC_STORYBOOK_VERSION.'
'Could not find a supported Storybook viewlayer package in node_modules. Make sure one is installed.'
);
});

return Promise.race([
...findings.map((p, i) =>
p.then(
(l) => read(l).then((r) => ({ viewLayer: viewLayers[i], ...r })),
disregard // keep it pending forever
)
...findings.map(([promise, name]) =>
promise
.then(fs.readJson, disregard)
.then((pkgJson) => ({ viewLayer: name, version: pkgJson.version }))
),
allFailed,
timeout(10000),
Expand All @@ -99,12 +134,10 @@ const findViewlayer = async ({ env }) => {

const findAddons = async () => {
const result = await Promise.all(
supportedAddons.map((name) =>
resolve(`addon-${name}`)
.then((l) =>
read(l).then((r) => ({ name, packageName: r.name, packageVersion: r.version }))
)
.catch((e) => false)
Object.entries(supportedAddons).map(([pkg, name]) =>
resolve(pkg)
.then(fs.readJson, () => false)
.then((pkgJson) => ({ name, packageName: pkgJson.name, packageVersion: pkgJson.version }))
)
);

Expand Down
77 changes: 77 additions & 0 deletions bin/lib/getStorybookInfo.test.js
@@ -0,0 +1,77 @@
import getStorybookInfo from './getStorybookInfo';

const log = { warn: jest.fn() };
const context = { env: {}, log, options: {}, packageJson: {} };

const REACT = { '@storybook/react': '1.2.3' };

describe('getStorybookInfo', () => {
it('returns viewLayer and version', async () => {
const ctx = { ...context, packageJson: { dependencies: REACT } };
await expect(getStorybookInfo(ctx)).resolves.toEqual(
// We're getting the result of tracing chromatic-cli's node_modules here.
expect.objectContaining({ viewLayer: 'react', version: expect.any(String) })
);
});

it('throws on missing dependency', async () => {
await expect(getStorybookInfo(context)).rejects.toThrow(
'Could not find a supported Storybook viewlayer package in your package.json dependencies'
);
});

it('warns on duplicate devDependency', async () => {
const ctx = { ...context, packageJson: { dependencies: REACT, devDependencies: REACT } };
await getStorybookInfo(ctx);
expect(log.warn).toHaveBeenCalledWith(
expect.stringContaining('both "dependencies" and "devDependencies"')
);
});

it('warns on duplicate peerDependency', async () => {
const ctx = { ...context, packageJson: { dependencies: REACT, peerDependencies: REACT } };
await getStorybookInfo(ctx);
expect(log.warn).toHaveBeenCalledWith(
expect.stringContaining('both "dependencies" and "peerDependencies"')
);
});

describe('with CHROMATIC_STORYBOOK_VERSION', () => {
it('returns viewLayer and version from env', async () => {
const ctx = { ...context, env: { CHROMATIC_STORYBOOK_VERSION: '@storybook/react@3.2.1' } };
await expect(getStorybookInfo(ctx)).resolves.toEqual(
expect.objectContaining({ viewLayer: 'react', version: '3.2.1' })
);
});

it('supports unscoped package name', async () => {
const ctx = { ...context, env: { CHROMATIC_STORYBOOK_VERSION: 'react@3.2.1' } };
await expect(getStorybookInfo(ctx)).resolves.toEqual(
expect.objectContaining({ viewLayer: 'react', version: '3.2.1' })
);
});

it('throws on invalid value', async () => {
const ctx = { ...context, env: { CHROMATIC_STORYBOOK_VERSION: '3.2.1' } };
await expect(getStorybookInfo(ctx)).rejects.toThrow('Invalid');
});

it('throws on unsupported viewlayer', async () => {
const ctx = { ...context, env: { CHROMATIC_STORYBOOK_VERSION: '@storybook/native@3.2.1' } };
await expect(getStorybookInfo(ctx)).rejects.toThrow('Unsupported');
});
});

describe('with --storybook-build-dir', () => {
it('returns viewLayer and version from packageJson', async () => {
const ctx = {
...context,
options: { storybookBuildDir: 'storybook-static' },
packageJson: { dependencies: REACT },
};
await expect(getStorybookInfo(ctx)).resolves.toEqual(
expect.objectContaining({ viewLayer: 'react', version: '1.2.3' })
);
});
});
});