Merge pull request #10547 from storybookjs/feat/automatically-detect-ts-in-cli

This commit is contained in:
Michael Shilman 2020-04-27 11:38:09 +08:00 committed by GitHub
commit 614858ce19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 674 additions and 213 deletions

View File

@ -1,128 +1,69 @@
import path from 'path';
import fs from 'fs';
import types, { supportedFrameworks } from './project_types';
import {
PROJECT_TYPES,
supportedTemplates,
SUPPORTED_FRAMEWORKS,
SUPPORTED_LANGUAGES,
} from './project_types';
import { getBowerJson, getPackageJson } from './helpers';
function detectFramework(dependencies) {
if (!dependencies) {
return false;
}
if (
dependencies.devDependencies &&
(dependencies.devDependencies['vue-loader'] || dependencies.devDependencies.vueify)
) {
return types.SFC_VUE;
const hasDependency = (packageJson, name) => {
return !!packageJson.dependencies?.[name] || !!packageJson.devDependencies?.[name];
};
const hasPeerDependency = (packageJson, name) => {
return !!packageJson.peerDependencies?.[name];
};
/**
* Returns a framework preset based on a given configuration.
*
* @param {Object} packageJson contains `dependencies`, `devDependencies`
* and/or `peerDependencies` which we use to get installed packages.
* @param {Object} framework contains a configuration of a framework preset.
* Refer to supportedTemplates in project_types.js for more info.
* @returns a preset name like PROJECT_TYPES.REACT, or null if not found.
* @example
* getFrameworkPreset(packageJson, * {
* preset: PROJECT_TYPES.REACT,
* dependencies: ['react'],
* matcherFunction: ({ dependencies }) => {
* return dependencies.every(Boolean);
* },
* });
*/
const getFrameworkPreset = (packageJson, framework) => {
const matches = {
dependencies: [false],
peerDependencies: [false],
files: [false],
};
const { preset, files, dependencies, peerDependencies, matcherFunction } = framework;
if (Array.isArray(dependencies) && dependencies.length > 0) {
matches.dependencies = dependencies.map((name) => hasDependency(packageJson, name));
}
if (
(dependencies.dependencies && dependencies.dependencies.vue) ||
(dependencies.devDependencies && dependencies.devDependencies.vue) ||
(dependencies.dependencies && dependencies.dependencies.nuxt) ||
(dependencies.devDependencies && dependencies.devDependencies.nuxt)
) {
return types.VUE;
if (Array.isArray(peerDependencies) && peerDependencies.length > 0) {
matches.peerDependencies = peerDependencies.map((name) => hasPeerDependency(packageJson, name));
}
if (
(dependencies.dependencies && dependencies.dependencies['ember-cli']) ||
(dependencies.devDependencies && dependencies.devDependencies['ember-cli'])
) {
return types.EMBER;
if (Array.isArray(files) && files.length > 0) {
matches.files = files.map((name) => fs.existsSync(path.join(process.cwd(), name)));
}
if (
(dependencies.dependencies && dependencies.dependencies['react-scripts']) ||
(dependencies.devDependencies && dependencies.devDependencies['react-scripts']) ||
// For projects using a custom/forked `react-scripts` package.
fs.existsSync(path.join(process.cwd(), '/node_modules/.bin/react-scripts'))
) {
return types.REACT_SCRIPTS;
}
return matcherFunction(matches) ? preset : null;
};
if (
((dependencies.devDependencies && dependencies.devDependencies.webpack) ||
(dependencies.dependencies && dependencies.dependencies.webpack)) &&
((dependencies.devDependencies && dependencies.devDependencies.react) ||
(dependencies.dependencies && dependencies.dependencies.react))
) {
return types.WEBPACK_REACT;
}
export function detectFrameworkPreset(packageJson = {}) {
const result = supportedTemplates.find((framework) => {
return getFrameworkPreset(packageJson, framework) !== null;
});
if (dependencies.peerDependencies && dependencies.peerDependencies.react) {
return types.REACT_PROJECT;
}
if (
(dependencies.dependencies && dependencies.dependencies['react-native']) ||
(dependencies.dependencies && dependencies.dependencies['react-native-scripts']) ||
(dependencies.devDependencies && dependencies.devDependencies['react-native-scripts'])
) {
return types.REACT_NATIVE;
}
if (
(dependencies.dependencies && dependencies.dependencies.react) ||
(dependencies.devDependencies && dependencies.devDependencies.react)
) {
return types.REACT;
}
if (
(dependencies.dependencies && dependencies.dependencies['@angular/core']) ||
(dependencies.devDependencies && dependencies.devDependencies['@angular/core'])
) {
return types.ANGULAR;
}
if (
(dependencies.dependencies && dependencies.dependencies['lit-element']) ||
(dependencies.devDependencies && dependencies.devDependencies['lit-element'])
) {
return types.WEB_COMPONENTS;
}
if (
(dependencies.dependencies && dependencies.dependencies.mithril) ||
(dependencies.devDependencies && dependencies.devDependencies.mithril)
) {
return types.MITHRIL;
}
if (
(dependencies.dependencies && dependencies.dependencies['backbone.marionette']) ||
(dependencies.devDependencies && dependencies.devDependencies['backbone.marionette'])
) {
return types.MARIONETTE;
}
if (
(dependencies.dependencies && dependencies.dependencies.marko) ||
(dependencies.devDependencies && dependencies.devDependencies.marko)
) {
return types.MARKO;
}
if (
(dependencies.dependencies && dependencies.dependencies.riot) ||
(dependencies.devDependencies && dependencies.devDependencies.riot)
) {
return types.RIOT;
}
if (
(dependencies.dependencies && dependencies.dependencies.preact) ||
(dependencies.devDependencies && dependencies.devDependencies.preact)
) {
return types.PREACT;
}
if (
(dependencies.dependencies && dependencies.dependencies.rax) ||
(dependencies.devDependencies && dependencies.devDependencies.rax)
) {
return types.RAX;
}
return false;
return result ? result.preset : PROJECT_TYPES.UNDETECTED;
}
export function isStorybookInstalled(dependencies, force) {
@ -132,39 +73,46 @@ export function isStorybookInstalled(dependencies, force) {
if (!force && dependencies.devDependencies) {
if (
supportedFrameworks.reduce(
SUPPORTED_FRAMEWORKS.reduce(
(storybookPresent, framework) =>
storybookPresent || dependencies.devDependencies[`@storybook/${framework}`],
false
)
) {
return types.ALREADY_HAS_STORYBOOK;
return PROJECT_TYPES.ALREADY_HAS_STORYBOOK;
}
if (
dependencies.devDependencies['@kadira/storybook'] ||
dependencies.devDependencies['@kadira/react-native-storybook']
) {
return types.UPDATE_PACKAGE_ORGANIZATIONS;
return PROJECT_TYPES.UPDATE_PACKAGE_ORGANIZATIONS;
}
}
return false;
}
export function detect(options) {
if (options.html) {
return types.HTML;
export function detectLanguage() {
let language = SUPPORTED_LANGUAGES.JAVASCRIPT;
const packageJson = getPackageJson();
const bowerJson = getBowerJson();
if (!packageJson && !bowerJson) {
return language;
}
if (hasDependency(packageJson || bowerJson, 'typescript')) {
language = SUPPORTED_LANGUAGES.TYPESCRIPT;
}
return language;
}
export function detect(options = {}) {
const packageJson = getPackageJson();
const bowerJson = getBowerJson();
if (!packageJson && !bowerJson) {
return types.UNDETECTED;
}
if (fs.existsSync(path.resolve('.meteor'))) {
return types.METEOR;
return PROJECT_TYPES.UNDETECTED;
}
const storyBookInstalled = isStorybookInstalled(packageJson, options.force);
@ -172,5 +120,9 @@ export function detect(options) {
return storyBookInstalled;
}
return detectFramework(packageJson) || detectFramework(bowerJson) || types.UNDETECTED;
if (options.html) {
return PROJECT_TYPES.HTML;
}
return detectFrameworkPreset(packageJson || bowerJson);
}

View File

@ -1,27 +1,307 @@
import { isStorybookInstalled } from './detect';
import { supportedFrameworks } from './project_types';
import fs from 'fs';
describe('isStorybookInstalled should return', () => {
it('false if empty devDependency', () => {
expect(isStorybookInstalled({ devDependencies: {} }, false)).toBe(false);
});
it('false if no devDependency', () => {
expect(isStorybookInstalled({}, false)).toBe(false);
import { getBowerJson, getPackageJson } from './helpers';
import { isStorybookInstalled, detectFrameworkPreset, detect, detectLanguage } from './detect';
import { PROJECT_TYPES, SUPPORTED_FRAMEWORKS, SUPPORTED_LANGUAGES } from './project_types';
jest.mock('./helpers', () => ({
getBowerJson: jest.fn(),
getPackageJson: jest.fn(),
}));
jest.mock('fs', () => ({
existsSync: jest.fn(),
}));
jest.mock('path', () => ({
// make it return just the second path, for easier testing
join: jest.fn((_, p) => p),
}));
const MOCK_FRAMEWORK_FILES = [
{
name: PROJECT_TYPES.METEOR,
files: {
'.meteor': 'file content',
},
},
{
name: PROJECT_TYPES.SFC_VUE,
files: {
'package.json': {
dependencies: {
vuetify: '1.0.0',
},
devDependencies: {
'vue-loader': '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.VUE,
files: {
'package.json': {
dependencies: {
vue: '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.EMBER,
files: {
'package.json': {
devDependencies: {
'ember-cli': '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.REACT_PROJECT,
files: {
'package.json': {
peerDependencies: {
react: '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.REACT_NATIVE,
files: {
'package.json': {
dependencies: {
'react-native': '1.0.0',
},
devDependencies: {
'react-native-scripts': '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.REACT_SCRIPTS,
files: {
'package.json': {
devDependencies: {
'react-scripts': '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.WEBPACK_REACT,
files: {
'package.json': {
dependencies: {
react: '1.0.0',
},
devDependencies: {
webpack: '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.REACT,
files: {
'package.json': {
dependencies: {
react: '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.ANGULAR,
files: {
'package.json': {
dependencies: {
'@angular/core': '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.WEB_COMPONENTS,
files: {
'package.json': {
dependencies: {
'lit-element': '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.MITHRIL,
files: {
'package.json': {
dependencies: {
mithril: '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.MARIONETTE,
files: {
'package.json': {
dependencies: {
'backbone.marionette': '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.MARKO,
files: {
'package.json': {
dependencies: {
marko: '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.RIOT,
files: {
'package.json': {
dependencies: {
riot: '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.PREACT,
files: {
'package.json': {
dependencies: {
preact: '1.0.0',
},
},
},
},
{
name: PROJECT_TYPES.RAX,
files: {
'.rax': 'file content',
'package.json': {
dependencies: {
rax: '1.0.0',
},
},
},
},
];
describe('Detect', () => {
it(`should return type HTML if html option is passed`, () => {
getPackageJson.mockImplementation(() => true);
expect(detect({ html: true })).toBe(PROJECT_TYPES.HTML);
});
supportedFrameworks.forEach((framework) => {
it(`true if devDependencies has ${framework} Storybook version`, () => {
const devDependencies = {};
devDependencies[`@storybook/${framework}`] = '4.0.0-alpha.21';
expect(isStorybookInstalled({ devDependencies }, false)).toBeTruthy();
it(`should return type UNDETECTED if neither packageJson or bowerJson exist`, () => {
getPackageJson.mockImplementation(() => false);
getBowerJson.mockImplementation(() => false);
expect(detect()).toBe(PROJECT_TYPES.UNDETECTED);
});
it(`should return language typescript if the dependency is present`, () => {
getPackageJson.mockImplementation(() => ({
dependencies: {
typescript: '1.0.0',
},
}));
expect(detectLanguage()).toBe(SUPPORTED_LANGUAGES.TYPESCRIPT);
});
it(`should return language javascript by default`, () => {
getPackageJson.mockImplementation(() => true);
expect(detectLanguage()).toBe(SUPPORTED_LANGUAGES.JAVASCRIPT);
});
describe('isStorybookInstalled should return', () => {
it('false if empty devDependency', () => {
expect(isStorybookInstalled({ devDependencies: {} }, false)).toBe(false);
});
it('false if no devDependency', () => {
expect(isStorybookInstalled({}, false)).toBe(false);
});
SUPPORTED_FRAMEWORKS.forEach((framework) => {
it(`true if devDependencies has ${framework} Storybook version`, () => {
const devDependencies = {
[`@storybook/${framework}`]: '4.0.0-alpha.21',
};
expect(isStorybookInstalled({ devDependencies }, false)).toBeTruthy();
});
});
it('false if forced flag', () => {
expect(
isStorybookInstalled(
{
devDependencies: { '@storybook/react': '4.0.0-alpha.21' },
},
true
)
).toBe(false);
});
it('ALREADY_HAS_STORYBOOK if lib is present', () => {
expect(
isStorybookInstalled({
devDependencies: { '@storybook/react': '4.0.0-alpha.21' },
})
).toBe(PROJECT_TYPES.ALREADY_HAS_STORYBOOK);
});
it('UPDATE_PACKAGE_ORGANIZATIONS if legacy lib is detected', () => {
expect(
isStorybookInstalled({
devDependencies: { '@kadira/storybook': '4.0.0-alpha.21' },
})
).toBe(PROJECT_TYPES.UPDATE_PACKAGE_ORGANIZATIONS);
});
});
it('true if forced flag', () => {
expect(
isStorybookInstalled({
devDependencies: { 'storybook/react': '4.0.0-alpha.21' },
})
).toBe(false);
describe('detectFrameworkPreset should return', () => {
afterEach(() => {
jest.clearAllMocks();
});
MOCK_FRAMEWORK_FILES.forEach((structure) => {
it(`${structure.name}`, () => {
fs.existsSync.mockImplementation((filePath) => {
return Object.keys(structure.files).includes(filePath);
});
const result = detectFrameworkPreset(structure.files['package.json']);
expect(result).toBe(structure.name);
});
});
it(`UNDETECTED for unknown frameworks`, () => {
const result = detectFrameworkPreset();
expect(result).toBe(PROJECT_TYPES.UNDETECTED);
});
it('REACT_SCRIPTS for custom react scripts config', () => {
const forkedReactScriptsConfig = {
'/node_modules/.bin/react-scripts': 'file content',
};
fs.existsSync.mockImplementation((filePath) => {
return Object.keys(forkedReactScriptsConfig).includes(filePath);
});
const result = detectFrameworkPreset();
expect(result).toBe(PROJECT_TYPES.REACT_SCRIPTS);
});
});
});

View File

@ -14,6 +14,7 @@ import {
writeFileAsJson,
copyTemplate,
} from '../../helpers';
import { STORY_FORMAT } from '../../project_types';
async function addDependencies(npmOptions, { storyFormat }) {
const packages = [
@ -23,7 +24,7 @@ async function addDependencies(npmOptions, { storyFormat }) {
'@storybook/addons',
];
if (storyFormat === 'mdx') {
if (storyFormat === STORY_FORMAT.MDX) {
packages.push('@storybook/addon-docs');
}
@ -61,7 +62,7 @@ function editAngularAppTsConfig() {
writeFileAsJson(getAngularAppTsConfigPath(), tsConfigJson);
}
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
if (!isDefaultProjectSet()) {
throw new Error(
'Could not find a default project in your Angular workspace. Add a project and re-run the installation.'

View File

@ -7,7 +7,7 @@ import {
copyTemplate,
} from '../../helpers';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const [
storybookVersion,
babelPluginEmberModulePolyfillVersion,

View File

@ -6,11 +6,12 @@ import {
installDependencies,
copyTemplate,
} from '../../helpers';
import { STORY_FORMAT } from '../../project_types';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const packages = ['@storybook/html'];
const versionedPackages = await getVersionedPackages(npmOptions, ...packages);
if (storyFormat === 'mdx') {
if (storyFormat === STORY_FORMAT.MDX) {
packages.push('@storybook/addon-docs');
}

View File

@ -7,7 +7,7 @@ import {
copyTemplate,
} from '../../helpers';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const [storybookVersion, addonActionVersion, addonKnobsVersion] = await getVersions(
npmOptions,
'@storybook/marko',

View File

@ -9,7 +9,7 @@ import {
copyTemplate,
} from '../../helpers';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const [
storybookVersion,
actionsVersion,

View File

@ -7,7 +7,7 @@ import {
copyTemplate,
} from '../../helpers';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const [
storybookVersion,
actionsVersion,

View File

@ -7,7 +7,7 @@ import {
copyTemplate,
} from '../../helpers';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const [storybookVersion, actionsVersion, linksVersion, addonsVersion] = await getVersions(
npmOptions,
'@storybook/preact',

View File

@ -7,7 +7,7 @@ import {
copyTemplate,
} from '../../helpers';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const [
storybookVersion,
actionsVersion,

View File

@ -6,15 +6,16 @@ import {
installDependencies,
copyTemplate,
} from '../../helpers';
import { STORY_FORMAT } from '../../project_types';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const packages = [
'@storybook/react',
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addons',
];
if (storyFormat === 'mdx') {
if (storyFormat === STORY_FORMAT.MDX) {
packages.push('@storybook/addon-docs');
}

View File

@ -10,7 +10,7 @@ import {
copyTemplate,
} from '../../helpers';
export default async (npmOptions, installServer, { storyFormat = 'csf' }) => {
export default async (npmOptions, installServer, { storyFormat }) => {
const [storybookVersion, addonsVersion, actionsVersion, linksVersion] = await getVersions(
npmOptions,
'@storybook/react-native',

View File

@ -8,8 +8,9 @@ import {
installDependencies,
copyTemplate,
} from '../../helpers';
import { STORY_FORMAT } from '../../project_types';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const packages = [
'@storybook/react',
'@storybook/preset-create-react-app',
@ -18,7 +19,7 @@ export default async (npmOptions, { storyFormat = 'csf' }) => {
'@storybook/addons',
];
if (storyFormat === 'mdx') {
if (storyFormat === STORY_FORMAT.MDX) {
packages.push('@storybook/addon-docs');
}

View File

@ -7,7 +7,7 @@ import {
copyTemplate,
} from '../../helpers';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const [
storybookVersion,
actionsVersion,

View File

@ -6,15 +6,16 @@ import {
installDependencies,
copyTemplate,
} from '../../helpers';
import { STORY_FORMAT } from '../../project_types';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const packages = [
'@storybook/vue',
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addons',
];
if (storyFormat === 'mdx') {
if (storyFormat === STORY_FORMAT.MDX) {
packages.push('@storybook/addon-docs');
}
const versionedPackages = await getVersionedPackages(npmOptions, ...packages);

View File

@ -7,7 +7,7 @@ import {
copyTemplate,
} from '../../helpers';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const [
storybookVersion,
actionsVersion,

View File

@ -8,8 +8,9 @@ import {
addToDevDependenciesIfNotPresent,
copyTemplate,
} from '../../helpers';
import { STORY_FORMAT } from '../../project_types';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const packages = [
'@storybook/vue',
'@storybook/addon-actions',
@ -18,7 +19,7 @@ export default async (npmOptions, { storyFormat = 'csf' }) => {
'babel-preset-vue',
'@babel/core',
];
if (storyFormat === 'mdx') {
if (storyFormat === STORY_FORMAT.MDX) {
packages.push('@storybook/addon-docs');
}
const versionedPackages = await getVersionedPackages(npmOptions, ...packages);

View File

@ -7,12 +7,13 @@ import {
getBabelDependencies,
installDependencies,
} from '../../helpers';
import { STORY_FORMAT } from '../../project_types';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const storybookVersion = await getVersion(npmOptions, '@storybook/web-components');
fse.copySync(path.resolve(__dirname, 'template/'), '.', { overwrite: true });
if (storyFormat === 'mdx') {
if (storyFormat === STORY_FORMAT.MDX) {
// TODO: handle adding of docs mode
}

View File

@ -6,15 +6,16 @@ import {
installDependencies,
copyTemplate,
} from '../../helpers';
import { STORY_FORMAT } from '../../project_types';
export default async (npmOptions, { storyFormat = 'csf' }) => {
export default async (npmOptions, { storyFormat }) => {
const packages = [
'@storybook/react',
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addons',
];
if (storyFormat === 'mdx') {
if (storyFormat === STORY_FORMAT.MDX) {
packages.push('@storybook/addon-docs');
}
const versionedPackages = await getVersionedPackages(npmOptions, ...packages);

View File

@ -6,10 +6,11 @@ import chalk from 'chalk';
import { sync as spawnSync } from 'cross-spawn';
import { gt, satisfies } from 'semver';
import stripJsonComments from 'strip-json-comments';
import { latestVersion } from './latest_version';
import { latestVersion } from './latest_version';
import { version, devDependencies } from '../package.json';
import { npmInit } from './npm_init';
import { STORY_FORMAT } from './project_types';
const logger = console;
@ -316,6 +317,12 @@ export function addToDevDependenciesIfNotPresent(packageJson, name, packageVersi
export function copyTemplate(templateRoot, storyFormat) {
const templateDir = path.resolve(templateRoot, `template-${storyFormat}/`);
if (!fs.existsSync(templateDir)) {
// Fallback to CSF plain first, in case format is typescript but template is not available.
if (storyFormat === STORY_FORMAT.CSF_TYPESCRIPT) {
copyTemplate(templateRoot, STORY_FORMAT.CSF);
return;
}
throw new Error(`Unsupported story format: ${storyFormat}`);
}
fse.copySync(templateDir, '.', { overwrite: true });

View File

@ -0,0 +1,57 @@
import fs from 'fs';
import fse from 'fs-extra';
import * as helpers from './helpers';
import { STORY_FORMAT } from './project_types';
jest.mock('fs', () => ({
existsSync: jest.fn(),
}));
jest.mock('fs-extra', () => ({
copySync: jest.fn(() => ({})),
}));
jest.mock('path', () => ({
// make it return just the second path, for easier testing
resolve: jest.fn((_, p) => p),
}));
jest.mock('./npm_init', () => ({
npmInit: jest.fn(),
}));
describe('Helpers', () => {
describe('copyTemplate', () => {
it(`should fall back to ${STORY_FORMAT.CSF}
in case ${STORY_FORMAT.CSF_TYPESCRIPT} is not available`, () => {
const csfDirectory = `template-${STORY_FORMAT.CSF}/`;
fs.existsSync.mockImplementation((filePath) => {
return filePath === csfDirectory;
});
helpers.copyTemplate('', STORY_FORMAT.CSF_TYPESCRIPT);
const copySyncSpy = jest.spyOn(fse, 'copySync');
expect(copySyncSpy).toHaveBeenCalledWith(csfDirectory, expect.anything(), expect.anything());
});
it(`should use ${STORY_FORMAT.CSF_TYPESCRIPT} if it is available`, () => {
const csfDirectory = `template-${STORY_FORMAT.CSF_TYPESCRIPT}/`;
fs.existsSync.mockImplementation((filePath) => {
return filePath === csfDirectory;
});
helpers.copyTemplate('', STORY_FORMAT.CSF_TYPESCRIPT);
const copySyncSpy = jest.spyOn(fse, 'copySync');
expect(copySyncSpy).toHaveBeenCalledWith(csfDirectory, expect.anything(), expect.anything());
});
it(`should throw an error for unsupported story format`, () => {
const storyFormat = 'non-existent-format';
const expectedMessage = `Unsupported story format: ${storyFormat}`;
expect(() => {
helpers.copyTemplate('', storyFormat);
}).toThrowError(expectedMessage);
});
});
});

View File

@ -1,9 +1,14 @@
import updateNotifier from 'update-notifier';
import chalk from 'chalk';
import inquirer from 'inquirer';
import { detect, isStorybookInstalled } from './detect';
import { detect, isStorybookInstalled, detectLanguage } from './detect';
import { hasYarn } from './has_yarn';
import types, { installableProjectTypes } from './project_types';
import {
installableProjectTypes,
PROJECT_TYPES,
STORY_FORMAT,
SUPPORTED_LANGUAGES,
} from './project_types';
import {
commandLog,
codeLog,
@ -42,8 +47,13 @@ const installStorybook = (projectType, options) => {
skipInstall: options.skipInstall,
};
const defaultStoryFormat =
detectLanguage() === SUPPORTED_LANGUAGES.TYPESCRIPT
? STORY_FORMAT.CSF_TYPESCRIPT
: STORY_FORMAT.CSF;
const generatorOptions = {
storyFormat: options.storyFormat,
storyFormat: options.storyFormat || defaultStoryFormat,
};
const runStorybookCommand = useYarn ? 'yarn storybook' : 'npm run storybook';
@ -66,7 +76,7 @@ const installStorybook = (projectType, options) => {
const runGenerator = () => {
switch (projectType) {
case types.ALREADY_HAS_STORYBOOK:
case PROJECT_TYPES.ALREADY_HAS_STORYBOOK:
logger.log();
paddedLog('There seems to be a storybook already available in this project.');
paddedLog('Apply following command to force:\n');
@ -76,23 +86,23 @@ const installStorybook = (projectType, options) => {
logger.log();
return Promise.resolve();
case types.UPDATE_PACKAGE_ORGANIZATIONS:
case PROJECT_TYPES.UPDATE_PACKAGE_ORGANIZATIONS:
return updateOrganisationsGenerator(options.parser, npmOptions)
.then(() => null) // commmandLog doesn't like to see output
.then(commandLog('Upgrading your project to the new storybook packages.'))
.then(end);
case types.REACT_SCRIPTS:
case PROJECT_TYPES.REACT_SCRIPTS:
return reactScriptsGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Create React App" based project'))
.then(end);
case types.REACT:
case PROJECT_TYPES.REACT:
return reactGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "React" app'))
.then(end);
case types.REACT_NATIVE: {
case PROJECT_TYPES.REACT_NATIVE: {
return (options.yes
? Promise.resolve({ server: true })
: inquirer.prompt([
@ -118,82 +128,82 @@ const installStorybook = (projectType, options) => {
});
}
case types.METEOR:
case PROJECT_TYPES.METEOR:
return meteorGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Meteor" app'))
.then(end);
case types.WEBPACK_REACT:
case PROJECT_TYPES.WEBPACK_REACT:
return webpackReactGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Webpack React" app'))
.then(end);
case types.REACT_PROJECT:
case PROJECT_TYPES.REACT_PROJECT:
return reactGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "React" library'))
.then(end);
case types.SFC_VUE:
case PROJECT_TYPES.SFC_VUE:
return sfcVueGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Single File Components Vue" app'))
.then(end);
case types.VUE:
case PROJECT_TYPES.VUE:
return vueGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Vue" app'))
.then(end);
case types.ANGULAR:
case PROJECT_TYPES.ANGULAR:
return angularGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Angular" app'))
.then(end);
case types.EMBER:
case PROJECT_TYPES.EMBER:
return emberGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Ember" app'))
.then(end);
case types.MITHRIL:
case PROJECT_TYPES.MITHRIL:
return mithrilGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Mithril" app'))
.then(end);
case types.MARIONETTE:
case PROJECT_TYPES.MARIONETTE:
return marionetteGenerator(npmOptions)
.then(commandLog('Adding storybook support to your "Marionette.js" app'))
.then(end);
case types.MARKO:
case PROJECT_TYPES.MARKO:
return markoGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Marko" app'))
.then(end);
case types.HTML:
case PROJECT_TYPES.HTML:
return htmlGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "HTML" app'))
.then(end);
case types.WEB_COMPONENTS:
case PROJECT_TYPES.WEB_COMPONENTS:
return webComponentsGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "web components" app'))
.then(end);
case types.RIOT:
case PROJECT_TYPES.RIOT:
return riotGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "riot.js" app'))
.then(end);
case types.PREACT:
case PROJECT_TYPES.PREACT:
return preactGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Preact" app'))
.then(end);
case types.SVELTE:
case PROJECT_TYPES.SVELTE:
return svelteGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Svelte" app'))
.then(end);
case types.RAX:
case PROJECT_TYPES.RAX:
return raxGenerator(npmOptions, generatorOptions)
.then(commandLog('Adding storybook support to your "Rax" app'))
.then(end);
@ -264,7 +274,9 @@ export default function (options, pkg) {
if (projectTypeProvided) {
if (installableProjectTypes.includes(options.type)) {
const storybookInstalled = isStorybookInstalled(getPackageJson(), options.force);
projectType = storybookInstalled ? types.ALREADY_HAS_STORYBOOK : options.type.toUpperCase();
projectType = storybookInstalled
? PROJECT_TYPES.ALREADY_HAS_STORYBOOK
: options.type.toUpperCase();
} else {
done(`The provided project type was not recognized by Storybook.`);
logger.log(`\nThe project types currently supported by Storybook are:\n`);

View File

@ -1,4 +1,4 @@
const projectTypes = {
export const PROJECT_TYPES = {
UNDETECTED: 'UNDETECTED',
REACT_SCRIPTS: 'REACT_SCRIPTS',
METEOR: 'METEOR',
@ -23,9 +23,18 @@ const projectTypes = {
RAX: 'RAX',
};
export default projectTypes;
export const STORY_FORMAT = {
CSF: 'csf',
CSF_TYPESCRIPT: 'csf-ts',
MDX: 'mdx',
};
export const supportedFrameworks = [
export const SUPPORTED_LANGUAGES = {
JAVASCRIPT: 'javascript',
TYPESCRIPT: 'typescript',
};
export const SUPPORTED_FRAMEWORKS = [
'react',
'react-native',
'vue',
@ -41,12 +50,156 @@ export const supportedFrameworks = [
'rax',
];
const notInstallableProjectTypes = [
projectTypes.UNDETECTED,
projectTypes.ALREADY_HAS_STORYBOOK,
projectTypes.UPDATE_PACKAGE_ORGANIZATIONS,
/**
* Configuration objects to match a storybook preset template.
*
* This has to be an array sorted in order of specificity/priority.
* Reason: both REACT and WEBPACK_REACT have react as dependency,
* therefore WEBPACK_REACT has to come first, as it's more specific.
*
* @example
* {
* preset: PROJECT_TYPES.NEW_SUPPORTED_TEMPLATE,
* dependencies: [string], // optional, tests for these both as dependencies and devDependencies
* peerDependencies: [string], // optional
* files: [string], // optional
* matcherFunction: ({ dependencies, files, peerDependencies }) => {
* // every argument is returned as an array of booleans
* return // whatever assertion you want, as long as it returns boolean.
* },
* }
*/
export const supportedTemplates = [
{
preset: PROJECT_TYPES.METEOR,
files: ['.meteor'],
matcherFunction: ({ files }) => {
return files.every(Boolean);
},
},
{
preset: PROJECT_TYPES.SFC_VUE,
dependencies: ['vue-loader', 'vuetify'],
matcherFunction: ({ dependencies }) => {
return dependencies.some(Boolean);
},
},
{
preset: PROJECT_TYPES.VUE,
dependencies: ['vue', 'nuxt'],
matcherFunction: ({ dependencies }) => {
return dependencies.some(Boolean);
},
},
{
preset: PROJECT_TYPES.EMBER,
dependencies: ['ember-cli'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.REACT_PROJECT,
peerDependencies: ['react'],
matcherFunction: ({ peerDependencies }) => {
return peerDependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.REACT_NATIVE,
dependencies: ['react-native', 'react-native-scripts'],
matcherFunction: ({ dependencies }) => {
return dependencies.some(Boolean);
},
},
{
preset: PROJECT_TYPES.REACT_SCRIPTS,
// For projects using a custom/forked `react-scripts` package.
files: ['/node_modules/.bin/react-scripts'],
// For standard CRA projects
dependencies: ['react-scripts'],
matcherFunction: ({ dependencies, files }) => {
return dependencies.every(Boolean) || files.every(Boolean);
},
},
{
preset: PROJECT_TYPES.WEBPACK_REACT,
dependencies: ['react', 'webpack'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.REACT,
dependencies: ['react'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.ANGULAR,
dependencies: ['@angular/core'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.WEB_COMPONENTS,
dependencies: ['lit-element'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.MITHRIL,
dependencies: ['mithril'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.MARIONETTE,
dependencies: ['backbone.marionette'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.MARKO,
dependencies: ['marko'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.RIOT,
dependencies: ['riot'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.PREACT,
dependencies: ['preact'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
{
preset: PROJECT_TYPES.RAX,
dependencies: ['rax'],
matcherFunction: ({ dependencies }) => {
return dependencies.every(Boolean);
},
},
];
export const installableProjectTypes = Object.values(projectTypes)
const notInstallableProjectTypes = [
PROJECT_TYPES.UNDETECTED,
PROJECT_TYPES.ALREADY_HAS_STORYBOOK,
PROJECT_TYPES.UPDATE_PACKAGE_ORGANIZATIONS,
];
export const installableProjectTypes = Object.values(PROJECT_TYPES)
.filter((type) => !notInstallableProjectTypes.includes(type))
.map((type) => type.toLowerCase());

View File

@ -1,7 +1,7 @@
import { installableProjectTypes, supportedFrameworks } from './project_types';
import { installableProjectTypes, SUPPORTED_FRAMEWORKS } from './project_types';
describe('installableProjectTypes should have an entry for the supported framework', () => {
supportedFrameworks.forEach((framework) => {
SUPPORTED_FRAMEWORKS.forEach((framework) => {
it(`${framework}`, () => {
expect(installableProjectTypes.includes(framework.replace(/-/g, '_'))).toBe(true);
});

View File

@ -50,14 +50,6 @@ do
yarn sb init --skip-install --yes --story-format mdx
fi
;;
csf-ts)
if [[ $dir =~ (react_scripts_ts) ]]
then
yarn sb init --skip-install --yes --story-format csf-ts
else
yarn sb init --skip-install --yes
fi
;;
esac
cd ..
done