diff --git a/lib/cli/package.json b/lib/cli/package.json index fdd7c9e74c38..d3e883e60697 100644 --- a/lib/cli/package.json +++ b/lib/cli/package.json @@ -85,7 +85,13 @@ "@storybook/svelte": "6.0.0-beta.8", "@storybook/ui": "6.0.0-beta.8", "@storybook/vue": "6.0.0-beta.8", - "@storybook/web-components": "6.0.0-beta.8" + "@storybook/web-components": "6.0.0-beta.8", + "@types/cross-spawn": "^6.0.1", + "@types/inquirer": "^6.5.0", + "@types/puppeteer-core": "^2.0.0", + "@types/semver": "^7.2.0", + "@types/shelljs": "^0.8.7", + "@types/update-notifier": "^0.0.30" }, "peerDependencies": { "jest": "*" diff --git a/lib/cli/src/NpmOptions.ts b/lib/cli/src/NpmOptions.ts new file mode 100644 index 000000000000..f397f5f2f7a6 --- /dev/null +++ b/lib/cli/src/NpmOptions.ts @@ -0,0 +1,5 @@ +export type NpmOptions = { + useYarn: boolean; + skipInstall?: boolean; + installAsDevDependencies?: boolean; +}; diff --git a/lib/cli/src/PackageJson.ts b/lib/cli/src/PackageJson.ts new file mode 100644 index 000000000000..2acfc43a55b5 --- /dev/null +++ b/lib/cli/src/PackageJson.ts @@ -0,0 +1,5 @@ +export type PackageJson = { + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; +}; diff --git a/lib/cli/src/add.test.js b/lib/cli/src/add.test.ts similarity index 100% rename from lib/cli/src/add.test.js rename to lib/cli/src/add.test.ts diff --git a/lib/cli/src/add.js b/lib/cli/src/add.ts similarity index 82% rename from lib/cli/src/add.js rename to lib/cli/src/add.ts index aecb674cc097..01d82738c583 100644 --- a/lib/cli/src/add.js +++ b/lib/cli/src/add.ts @@ -4,11 +4,17 @@ import { sync as spawnSync } from 'cross-spawn'; import { hasYarn } from './has_yarn'; import { latestVersion } from './latest_version'; import { commandLog, getPackageJson } from './helpers'; +import { PackageJson } from './PackageJson'; const logger = console; export const storybookAddonScope = '@storybook/addon-'; -const isAddon = async (name, npmOptions) => { +const isAddon = async ( + name: string, + npmOptions: { + useYarn: boolean; + } +) => { try { await latestVersion(npmOptions, name); return true; @@ -17,19 +23,23 @@ const isAddon = async (name, npmOptions) => { } }; -const isStorybookAddon = async (name, npmOptions) => +const isStorybookAddon = async (name: string, npmOptions: { useYarn: boolean }) => isAddon(`${storybookAddonScope}${name}`, npmOptions); -export const getPackageName = (addonName, isOfficialAddon) => +export const getPackageName = (addonName: string, isOfficialAddon: boolean) => isOfficialAddon ? storybookAddonScope + addonName : addonName; -export const getInstalledStorybookVersion = (packageJson) => +export const getInstalledStorybookVersion = (packageJson: PackageJson) => packageJson.devDependencies[ // This only considers the first occurrence. Object.keys(packageJson.devDependencies).find((devDep) => /@storybook/.test(devDep)) ] || false; -export const getPackageArg = (addonName, isOfficialAddon, packageJson) => { +export const getPackageArg = ( + addonName: string, + isOfficialAddon: boolean, + packageJson: PackageJson +) => { if (isOfficialAddon) { const addonNameNoTag = addonName.split('@')[0]; const installedStorybookVersion = getInstalledStorybookVersion(packageJson); @@ -40,7 +50,11 @@ export const getPackageArg = (addonName, isOfficialAddon, packageJson) => { return addonName; }; -const installAddon = (addonName, npmOptions, isOfficialAddon) => { +const installAddon = ( + addonName: string, + npmOptions: { useYarn: boolean }, + isOfficialAddon: boolean +) => { const prepareDone = commandLog(`Preparing to install the ${addonName} Storybook addon`); prepareDone(); logger.log(); @@ -69,7 +83,11 @@ const installAddon = (addonName, npmOptions, isOfficialAddon) => { installDone(); }; -export const addStorybookAddonToFile = (addonName, addonsFile, isOfficialAddon) => { +export const addStorybookAddonToFile = ( + addonName: string, + addonsFile: string[], + isOfficialAddon: boolean +) => { const addonNameNoTag = addonName.split('@')[0]; const alreadyRegistered = addonsFile.find((line) => line.includes(`${addonNameNoTag}/register`)); @@ -92,7 +110,7 @@ export const addStorybookAddonToFile = (addonName, addonsFile, isOfficialAddon) const LEGACY_CONFIGS = ['addons', 'config', 'presets']; -const postinstallAddon = async (addonName, isOfficialAddon) => { +const postinstallAddon = async (addonName: string, isOfficialAddon: boolean) => { let skipMsg = null; if (!isOfficialAddon) { skipMsg = 'unofficial addon'; @@ -128,7 +146,10 @@ const postinstallAddon = async (addonName, isOfficialAddon) => { } }; -export async function add(addonName, options) { +export async function add( + addonName: string, + options: { useNpm: boolean; skipPostinstall: boolean } +) { const useYarn = Boolean(options.useNpm !== true) && hasYarn(); const npmOptions = { useYarn, diff --git a/lib/cli/src/detect.test.js b/lib/cli/src/detect.test.ts similarity index 77% rename from lib/cli/src/detect.test.js rename to lib/cli/src/detect.test.ts index bc9a151dfc90..98bb4a026ffd 100644 --- a/lib/cli/src/detect.test.js +++ b/lib/cli/src/detect.test.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import { getBowerJson, getPackageJson } from './helpers'; import { isStorybookInstalled, detectFrameworkPreset, detect, detectLanguage } from './detect'; -import { PROJECT_TYPES, SUPPORTED_FRAMEWORKS, SUPPORTED_LANGUAGES } from './project_types'; +import { ProjectType, SUPPORTED_FRAMEWORKS, SupportedLanguage } from './project_types'; jest.mock('./helpers', () => ({ getBowerJson: jest.fn(), @@ -20,13 +20,13 @@ jest.mock('path', () => ({ const MOCK_FRAMEWORK_FILES = [ { - name: PROJECT_TYPES.METEOR, + name: ProjectType.METEOR, files: { '.meteor': 'file content', }, }, { - name: PROJECT_TYPES.SFC_VUE, + name: ProjectType.SFC_VUE, files: { 'package.json': { dependencies: { @@ -39,7 +39,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.VUE, + name: ProjectType.VUE, files: { 'package.json': { dependencies: { @@ -49,7 +49,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.EMBER, + name: ProjectType.EMBER, files: { 'package.json': { devDependencies: { @@ -59,7 +59,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.REACT_PROJECT, + name: ProjectType.REACT_PROJECT, files: { 'package.json': { peerDependencies: { @@ -69,7 +69,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.REACT_NATIVE, + name: ProjectType.REACT_NATIVE, files: { 'package.json': { dependencies: { @@ -82,7 +82,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.REACT_SCRIPTS, + name: ProjectType.REACT_SCRIPTS, files: { 'package.json': { devDependencies: { @@ -92,7 +92,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.WEBPACK_REACT, + name: ProjectType.WEBPACK_REACT, files: { 'package.json': { dependencies: { @@ -105,7 +105,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.REACT, + name: ProjectType.REACT, files: { 'package.json': { dependencies: { @@ -115,7 +115,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.ANGULAR, + name: ProjectType.ANGULAR, files: { 'package.json': { dependencies: { @@ -125,7 +125,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.WEB_COMPONENTS, + name: ProjectType.WEB_COMPONENTS, files: { 'package.json': { dependencies: { @@ -135,7 +135,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.MITHRIL, + name: ProjectType.MITHRIL, files: { 'package.json': { dependencies: { @@ -145,7 +145,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.MARIONETTE, + name: ProjectType.MARIONETTE, files: { 'package.json': { dependencies: { @@ -155,7 +155,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.MARKO, + name: ProjectType.MARKO, files: { 'package.json': { dependencies: { @@ -165,7 +165,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.RIOT, + name: ProjectType.RIOT, files: { 'package.json': { dependencies: { @@ -175,7 +175,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.PREACT, + name: ProjectType.PREACT, files: { 'package.json': { dependencies: { @@ -185,7 +185,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.SVELTE, + name: ProjectType.SVELTE, files: { 'package.json': { dependencies: { @@ -195,7 +195,7 @@ const MOCK_FRAMEWORK_FILES = [ }, }, { - name: PROJECT_TYPES.RAX, + name: ProjectType.RAX, files: { '.rax': 'file content', 'package.json': { @@ -209,28 +209,28 @@ const MOCK_FRAMEWORK_FILES = [ describe('Detect', () => { it(`should return type HTML if html option is passed`, () => { - getPackageJson.mockImplementation(() => true); - expect(detect({ html: true })).toBe(PROJECT_TYPES.HTML); + (getPackageJson as jest.Mock).mockImplementation(() => true); + expect(detect({ html: true })).toBe(ProjectType.HTML); }); it(`should return type UNDETECTED if neither packageJson or bowerJson exist`, () => { - getPackageJson.mockImplementation(() => false); - getBowerJson.mockImplementation(() => false); - expect(detect()).toBe(PROJECT_TYPES.UNDETECTED); + (getPackageJson as jest.Mock).mockImplementation(() => false); + (getBowerJson as jest.Mock).mockImplementation(() => false); + expect(detect()).toBe(ProjectType.UNDETECTED); }); it(`should return language typescript if the dependency is present`, () => { - getPackageJson.mockImplementation(() => ({ + (getPackageJson as jest.Mock).mockImplementation(() => ({ dependencies: { typescript: '1.0.0', }, })); - expect(detectLanguage()).toBe(SUPPORTED_LANGUAGES.TYPESCRIPT); + expect(detectLanguage()).toBe(SupportedLanguage.TYPESCRIPT); }); it(`should return language javascript by default`, () => { - getPackageJson.mockImplementation(() => true); - expect(detectLanguage()).toBe(SUPPORTED_LANGUAGES.JAVASCRIPT); + (getPackageJson as jest.Mock).mockImplementation(() => true); + expect(detectLanguage()).toBe(SupportedLanguage.JAVASCRIPT); }); describe('isStorybookInstalled should return', () => { @@ -267,7 +267,7 @@ describe('Detect', () => { isStorybookInstalled({ devDependencies: { '@storybook/react': '4.0.0-alpha.21' }, }) - ).toBe(PROJECT_TYPES.ALREADY_HAS_STORYBOOK); + ).toBe(ProjectType.ALREADY_HAS_STORYBOOK); }); it('UPDATE_PACKAGE_ORGANIZATIONS if legacy lib is detected', () => { @@ -275,7 +275,7 @@ describe('Detect', () => { isStorybookInstalled({ devDependencies: { '@kadira/storybook': '4.0.0-alpha.21' }, }) - ).toBe(PROJECT_TYPES.UPDATE_PACKAGE_ORGANIZATIONS); + ).toBe(ProjectType.UPDATE_PACKAGE_ORGANIZATIONS); }); }); @@ -286,7 +286,7 @@ describe('Detect', () => { MOCK_FRAMEWORK_FILES.forEach((structure) => { it(`${structure.name}`, () => { - fs.existsSync.mockImplementation((filePath) => { + (fs.existsSync as jest.Mock).mockImplementation((filePath) => { return Object.keys(structure.files).includes(filePath); }); @@ -298,7 +298,7 @@ describe('Detect', () => { it(`UNDETECTED for unknown frameworks`, () => { const result = detectFrameworkPreset(); - expect(result).toBe(PROJECT_TYPES.UNDETECTED); + expect(result).toBe(ProjectType.UNDETECTED); }); it('REACT_SCRIPTS for custom react scripts config', () => { @@ -306,12 +306,12 @@ describe('Detect', () => { '/node_modules/.bin/react-scripts': 'file content', }; - fs.existsSync.mockImplementation((filePath) => { + (fs.existsSync as jest.Mock).mockImplementation((filePath) => { return Object.keys(forkedReactScriptsConfig).includes(filePath); }); const result = detectFrameworkPreset(); - expect(result).toBe(PROJECT_TYPES.REACT_SCRIPTS); + expect(result).toBe(ProjectType.REACT_SCRIPTS); }); }); }); diff --git a/lib/cli/src/detect.js b/lib/cli/src/detect.ts similarity index 54% rename from lib/cli/src/detect.js rename to lib/cli/src/detect.ts index 2b6e95f3de41..9fccbacb74d8 100644 --- a/lib/cli/src/detect.js +++ b/lib/cli/src/detect.ts @@ -2,40 +2,29 @@ import path from 'path'; import fs from 'fs'; import { - PROJECT_TYPES, + ProjectType, supportedTemplates, SUPPORTED_FRAMEWORKS, - SUPPORTED_LANGUAGES, + SupportedLanguage, + TemplateConfiguration, + TemplateMatcher, } from './project_types'; import { getBowerJson, getPackageJson } from './helpers'; +import { PackageJson } from './PackageJson'; -const hasDependency = (packageJson, name) => { +const hasDependency = (packageJson: PackageJson, name: string) => { return !!packageJson.dependencies?.[name] || !!packageJson.devDependencies?.[name]; }; -const hasPeerDependency = (packageJson, name) => { +const hasPeerDependency = (packageJson: PackageJson, name: string) => { 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 = { +const getFrameworkPreset = ( + packageJson: PackageJson, + framework: TemplateConfiguration +): ProjectType | null => { + const matcher: TemplateMatcher = { dependencies: [false], peerDependencies: [false], files: [false], @@ -44,18 +33,18 @@ const getFrameworkPreset = (packageJson, framework) => { const { preset, files, dependencies, peerDependencies, matcherFunction } = framework; if (Array.isArray(dependencies) && dependencies.length > 0) { - matches.dependencies = dependencies.map((name) => hasDependency(packageJson, name)); + matcher.dependencies = dependencies.map((name) => hasDependency(packageJson, name)); } if (Array.isArray(peerDependencies) && peerDependencies.length > 0) { - matches.peerDependencies = peerDependencies.map((name) => hasPeerDependency(packageJson, name)); + matcher.peerDependencies = peerDependencies.map((name) => hasPeerDependency(packageJson, name)); } if (Array.isArray(files) && files.length > 0) { - matches.files = files.map((name) => fs.existsSync(path.join(process.cwd(), name))); + matcher.files = files.map((name) => fs.existsSync(path.join(process.cwd(), name))); } - return matcherFunction(matches) ? preset : null; + return matcherFunction(matcher) ? preset : null; }; export function detectFrameworkPreset(packageJson = {}) { @@ -63,10 +52,10 @@ export function detectFrameworkPreset(packageJson = {}) { return getFrameworkPreset(packageJson, framework) !== null; }); - return result ? result.preset : PROJECT_TYPES.UNDETECTED; + return result ? result.preset : ProjectType.UNDETECTED; } -export function isStorybookInstalled(dependencies, force) { +export function isStorybookInstalled(dependencies: PackageJson, force?: boolean) { if (!dependencies) { return false; } @@ -75,25 +64,25 @@ export function isStorybookInstalled(dependencies, force) { if ( SUPPORTED_FRAMEWORKS.reduce( (storybookPresent, framework) => - storybookPresent || dependencies.devDependencies[`@storybook/${framework}`], + storybookPresent || !!dependencies.devDependencies[`@storybook/${framework}`], false ) ) { - return PROJECT_TYPES.ALREADY_HAS_STORYBOOK; + return ProjectType.ALREADY_HAS_STORYBOOK; } if ( dependencies.devDependencies['@kadira/storybook'] || dependencies.devDependencies['@kadira/react-native-storybook'] ) { - return PROJECT_TYPES.UPDATE_PACKAGE_ORGANIZATIONS; + return ProjectType.UPDATE_PACKAGE_ORGANIZATIONS; } } return false; } export function detectLanguage() { - let language = SUPPORTED_LANGUAGES.JAVASCRIPT; + let language = SupportedLanguage.JAVASCRIPT; const packageJson = getPackageJson(); const bowerJson = getBowerJson(); if (!packageJson && !bowerJson) { @@ -101,18 +90,18 @@ export function detectLanguage() { } if (hasDependency(packageJson || bowerJson, 'typescript')) { - language = SUPPORTED_LANGUAGES.TYPESCRIPT; + language = SupportedLanguage.TYPESCRIPT; } return language; } -export function detect(options = {}) { +export function detect(options: { force?: boolean; html?: boolean } = {}) { const packageJson = getPackageJson(); const bowerJson = getBowerJson(); if (!packageJson && !bowerJson) { - return PROJECT_TYPES.UNDETECTED; + return ProjectType.UNDETECTED; } const storyBookInstalled = isStorybookInstalled(packageJson, options.force); @@ -121,7 +110,7 @@ export function detect(options = {}) { } if (options.html) { - return PROJECT_TYPES.HTML; + return ProjectType.HTML; } return detectFrameworkPreset(packageJson || bowerJson); diff --git a/lib/cli/src/extract.js b/lib/cli/src/extract.ts similarity index 86% rename from lib/cli/src/extract.js rename to lib/cli/src/extract.ts index 053b00338bec..77aa3ba9420f 100644 --- a/lib/cli/src/extract.js +++ b/lib/cli/src/extract.ts @@ -5,7 +5,7 @@ import express from 'express'; import getPort from 'get-port'; import { logger } from '@storybook/node-logger'; -const read = async (url) => { +const read = async (url: string) => { const browser = await usePuppeteerBrowser(); const page = await browser.newPage(); @@ -20,7 +20,7 @@ const read = async (url) => { const d = window.__STORYBOOK_STORY_STORE__.extract(); const result = Object.entries(d).reduce( - (acc, [k, v]) => ({ + (acc, [k, v]: [string, any]) => ({ ...acc, [k]: { ...v, @@ -48,7 +48,7 @@ const read = async (url) => { return data; }; -const useLocation = async (input) => { +const useLocation: (input: string) => Promise<[string, () => void]> = async (input: string) => { if (input.match(/^http/)) { return [input, async () => {}]; } @@ -74,7 +74,7 @@ const useLocation = async (input) => { }); }; -const usePuppeteerBrowser = async () => { +const usePuppeteerBrowser: () => Promise = async () => { const args = ['--no-sandbox ', '--disable-setuid-sandbox']; try { return await puppeteerCore.launch({ args }); @@ -85,13 +85,13 @@ const usePuppeteerBrowser = async () => { // eslint-disable-next-line global-require require('child_process').exec( `node ${require.resolve(path.join('puppeteer-core', 'install.js'))}`, - (error) => (error ? reject(error) : resolve(puppeteerCore.launch({ args }))) + (error: any) => (error ? reject(error) : resolve(puppeteerCore.launch({ args }))) ); }); } }; -export async function extract(input, targetPath) { +export async function extract(input: string, targetPath: string) { if (input && targetPath) { const [location, exit] = await useLocation(input); diff --git a/lib/cli/src/generate.js b/lib/cli/src/generate.ts similarity index 95% rename from lib/cli/src/generate.js rename to lib/cli/src/generate.ts index 242b8234cb12..ad7457026091 100644 --- a/lib/cli/src/generate.js +++ b/lib/cli/src/generate.ts @@ -3,12 +3,14 @@ import path from 'path'; import chalk from 'chalk'; import envinfo from 'envinfo'; import leven from 'leven'; -import pkg from '../package.json'; import initiate from './initiate'; import { add } from './add'; import { migrate } from './migrate'; import { extract } from './extract'; +// Cannot be `import` as it's not under TS root dir +const pkg = require('../package.json'); + const logger = console; program @@ -81,7 +83,7 @@ program.command('*', { noHelp: true }).action(() => { const [, , invalidCmd] = process.argv; logger.error(' Invalid command: %s.\n See --help for a list of available commands.', invalidCmd); // eslint-disable-next-line - const availableCommands = program.commands.map(cmd => cmd._name); + const availableCommands = program.commands.map((cmd) => cmd._name); const suggestion = availableCommands.find((cmd) => leven(cmd, invalidCmd) < 3); if (suggestion) { logger.log(`\n Did you mean ${suggestion}?`); diff --git a/lib/cli/src/generators/ANGULAR/angular-helpers.js b/lib/cli/src/generators/ANGULAR/angular-helpers.ts similarity index 89% rename from lib/cli/src/generators/ANGULAR/angular-helpers.js rename to lib/cli/src/generators/ANGULAR/angular-helpers.ts index 0edd2995770e..cf38ccf2c642 100644 --- a/lib/cli/src/generators/ANGULAR/angular-helpers.js +++ b/lib/cli/src/generators/ANGULAR/angular-helpers.ts @@ -2,6 +2,10 @@ import * as path from 'path'; import * as fs from 'fs'; import { readFileAsJson, writeFileAsJson } from '../../helpers'; +type TsConfig = { + extends: string; +}; + export function getAngularAppTsConfigPath() { const angularJson = readFileAsJson('angular.json', true); const { defaultProject } = angularJson; @@ -23,14 +27,14 @@ export function getAngularAppTsConfigJson() { return readFileAsJson(tsConfigPath, true); } -function setStorybookTsconfigExtendsPath(tsconfigJson) { +function setStorybookTsconfigExtendsPath(tsconfigJson: TsConfig) { const angularProjectTsConfigPath = getAngularAppTsConfigPath(); const newTsconfigJson = { ...tsconfigJson }; newTsconfigJson.extends = `../${angularProjectTsConfigPath}`; return newTsconfigJson; } -export function editStorybookTsConfig(tsconfigPath) { +export function editStorybookTsConfig(tsconfigPath: string) { let tsConfigJson; try { tsConfigJson = readFileAsJson(tsconfigPath); diff --git a/lib/cli/src/generators/ANGULAR/index.js b/lib/cli/src/generators/ANGULAR/index.ts similarity index 83% rename from lib/cli/src/generators/ANGULAR/index.js rename to lib/cli/src/generators/ANGULAR/index.ts index f7f0b46039b2..cc9d5921366f 100644 --- a/lib/cli/src/generators/ANGULAR/index.js +++ b/lib/cli/src/generators/ANGULAR/index.ts @@ -14,9 +14,11 @@ import { writeFileAsJson, copyTemplate, } from '../../helpers'; -import { STORY_FORMAT } from '../../project_types'; +import { StoryFormat } from '../../project_types'; +import { NpmOptions } from '../../NpmOptions'; +import { Generator, GeneratorOptions } from '../Generator'; -async function addDependencies(npmOptions, { storyFormat }) { +async function addDependencies(npmOptions: NpmOptions, { storyFormat }: GeneratorOptions) { const packages = [ '@storybook/angular', '@storybook/addon-actions', @@ -24,7 +26,7 @@ async function addDependencies(npmOptions, { storyFormat }) { '@storybook/addons', ]; - if (storyFormat === STORY_FORMAT.MDX) { + if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } @@ -62,7 +64,7 @@ function editAngularAppTsConfig() { writeFileAsJson(getAngularAppTsConfigPath(), tsConfigJson); } -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { if (!isDefaultProjectSet()) { throw new Error( 'Could not find a default project in your Angular workspace.\nSet a defaultProject in your angular.json and re-run the installation.' @@ -75,3 +77,5 @@ export default async (npmOptions, { storyFormat }) => { editAngularAppTsConfig(); editStorybookTsConfig(path.resolve('./.storybook/tsconfig.json')); }; + +export default generator; diff --git a/lib/cli/src/generators/AURELIA/index.js b/lib/cli/src/generators/AURELIA/index.ts similarity index 83% rename from lib/cli/src/generators/AURELIA/index.js rename to lib/cli/src/generators/AURELIA/index.ts index ad5bf7dc2099..3f7362f13759 100644 --- a/lib/cli/src/generators/AURELIA/index.js +++ b/lib/cli/src/generators/AURELIA/index.ts @@ -8,6 +8,8 @@ import { copyTemplate, readFileAsJson, } from '../../helpers'; +import { Generator } from '../Generator'; +import { StoryFormat } from '../../project_types'; function addStorybookExcludeGlobToTsConfig() { const tsConfigJson = readFileAsJson('tsconfig.json', true); @@ -24,8 +26,7 @@ function addStorybookExcludeGlobToTsConfig() { tsConfigJson.exclude = [...exclude, glob]; writeFileAsJson('tsconfig.json', tsConfigJson); } - -export default async (npmOptions, { storyFormat = 'csf' }) => { +const generator: Generator = async (npmOptions, { storyFormat = StoryFormat.CSF }) => { copyTemplate(__dirname, storyFormat); const packages = [ '@storybook/aurelia', @@ -54,5 +55,7 @@ export default async (npmOptions, { storyFormat = 'csf' }) => { writePackageJson(packageJson); addStorybookExcludeGlobToTsConfig(); const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - installDependencies(npmOptions, [...versionedPackages, ...babelDependencies]); + installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/generators/EMBER/index.js b/lib/cli/src/generators/EMBER/index.ts similarity index 92% rename from lib/cli/src/generators/EMBER/index.js rename to lib/cli/src/generators/EMBER/index.ts index 93a74e5beff9..874e81e6e2e7 100644 --- a/lib/cli/src/generators/EMBER/index.js +++ b/lib/cli/src/generators/EMBER/index.ts @@ -6,8 +6,9 @@ import { installDependencies, copyTemplate, } from '../../helpers'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const [ storybookVersion, babelPluginEmberModulePolyfillVersion, @@ -56,3 +57,5 @@ export default async (npmOptions, { storyFormat }) => { ...babelDependencies, ]); }; + +export default generator; diff --git a/lib/cli/src/generators/Generator.ts b/lib/cli/src/generators/Generator.ts new file mode 100644 index 000000000000..b6143c0c92dc --- /dev/null +++ b/lib/cli/src/generators/Generator.ts @@ -0,0 +1,8 @@ +import { NpmOptions } from '../NpmOptions'; +import { StoryFormat } from '../project_types'; + +export type GeneratorOptions = { + storyFormat: StoryFormat; +}; + +export type Generator = (npmOptions: NpmOptions, options: GeneratorOptions) => Promise; diff --git a/lib/cli/src/generators/HTML/index.js b/lib/cli/src/generators/HTML/index.ts similarity index 81% rename from lib/cli/src/generators/HTML/index.js rename to lib/cli/src/generators/HTML/index.ts index ae6b5cedf804..25d850192b6c 100755 --- a/lib/cli/src/generators/HTML/index.js +++ b/lib/cli/src/generators/HTML/index.ts @@ -6,13 +6,14 @@ import { installDependencies, copyTemplate, } from '../../helpers'; -import { STORY_FORMAT } from '../../project_types'; +import { StoryFormat } from '../../project_types'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const packages = ['@storybook/html', '@storybook/addon-actions', '@storybook/addon-links']; const versionedPackages = await getVersionedPackages(npmOptions, ...packages); - if (storyFormat === STORY_FORMAT.MDX) { + if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } @@ -33,3 +34,5 @@ export default async (npmOptions, { storyFormat }) => { installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/generators/MARIONETTE/index.js b/lib/cli/src/generators/MARIONETTE/index.ts similarity index 76% rename from lib/cli/src/generators/MARIONETTE/index.js rename to lib/cli/src/generators/MARIONETTE/index.ts index 0e64606268eb..e3ce805931a8 100644 --- a/lib/cli/src/generators/MARIONETTE/index.js +++ b/lib/cli/src/generators/MARIONETTE/index.ts @@ -1,23 +1,19 @@ import fse from 'fs-extra'; import path from 'path'; -import { npmInit } from '../../npm_init'; import { getVersion, - getPackageJson, writePackageJson, getBabelDependencies, installDependencies, + retrievePackageJson, } from '../../helpers'; +import { Generator } from '../Generator'; -export default async (npmOptions) => { +const generator: Generator = async (npmOptions) => { const storybookVersion = await getVersion(npmOptions, '@storybook/marionette'); fse.copySync(path.resolve(__dirname, 'template/'), '.', { overwrite: true }); - let packageJson = getPackageJson(); - if (!packageJson) { - await npmInit(); - packageJson = getPackageJson(); - } + const packageJson = await retrievePackageJson(); packageJson.dependencies = packageJson.dependencies || {}; packageJson.devDependencies = packageJson.devDependencies || {}; @@ -30,8 +26,10 @@ export default async (npmOptions) => { const babelDependencies = await getBabelDependencies(npmOptions, packageJson); - installDependencies(npmOptions, [ + installDependencies({ ...npmOptions, packageJson }, [ `@storybook/marionette@${storybookVersion}`, ...babelDependencies, ]); }; + +export default generator; diff --git a/lib/cli/src/generators/MARKO/index.js b/lib/cli/src/generators/MARKO/index.ts similarity index 88% rename from lib/cli/src/generators/MARKO/index.js rename to lib/cli/src/generators/MARKO/index.ts index 7d5240761d49..b8a3bc0e40e1 100644 --- a/lib/cli/src/generators/MARKO/index.js +++ b/lib/cli/src/generators/MARKO/index.ts @@ -6,8 +6,9 @@ import { installDependencies, copyTemplate, } from '../../helpers'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const [storybookVersion, addonActionVersion, addonKnobsVersion] = await getVersions( npmOptions, '@storybook/marko', @@ -37,3 +38,5 @@ export default async (npmOptions, { storyFormat }) => { ...babelDependencies, ]); }; + +export default generator; diff --git a/lib/cli/src/generators/METEOR/index.js b/lib/cli/src/generators/METEOR/index.ts similarity index 94% rename from lib/cli/src/generators/METEOR/index.js rename to lib/cli/src/generators/METEOR/index.ts index e0c7e6ef75b0..6420a01f0b03 100644 --- a/lib/cli/src/generators/METEOR/index.js +++ b/lib/cli/src/generators/METEOR/index.ts @@ -8,8 +8,9 @@ import { installDependencies, copyTemplate, } from '../../helpers'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const [ storybookVersion, actionsVersion, @@ -94,3 +95,5 @@ export default async (npmOptions, { storyFormat }) => { installDependencies({ ...npmOptions, packageJson }, [...devDependencies, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/generators/MITHRIL/index.js b/lib/cli/src/generators/MITHRIL/index.ts similarity index 90% rename from lib/cli/src/generators/MITHRIL/index.js rename to lib/cli/src/generators/MITHRIL/index.ts index 74c1e19df457..d6bff9454643 100644 --- a/lib/cli/src/generators/MITHRIL/index.js +++ b/lib/cli/src/generators/MITHRIL/index.ts @@ -6,8 +6,9 @@ import { installDependencies, copyTemplate, } from '../../helpers'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const [ storybookVersion, actionsVersion, @@ -47,3 +48,5 @@ export default async (npmOptions, { storyFormat }) => { ...babelDependencies, ]); }; + +export default generator; diff --git a/lib/cli/src/generators/PREACT/index.js b/lib/cli/src/generators/PREACT/index.ts similarity index 89% rename from lib/cli/src/generators/PREACT/index.js rename to lib/cli/src/generators/PREACT/index.ts index 617ed0426499..42c9eb416c9d 100644 --- a/lib/cli/src/generators/PREACT/index.js +++ b/lib/cli/src/generators/PREACT/index.ts @@ -6,8 +6,9 @@ import { installDependencies, copyTemplate, } from '../../helpers'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const [storybookVersion, actionsVersion, linksVersion, addonsVersion] = await getVersions( npmOptions, '@storybook/preact', @@ -39,3 +40,5 @@ export default async (npmOptions, { storyFormat }) => { ...babelDependencies, ]); }; + +export default generator; diff --git a/lib/cli/src/generators/RAX/index.js b/lib/cli/src/generators/RAX/index.ts similarity index 93% rename from lib/cli/src/generators/RAX/index.js rename to lib/cli/src/generators/RAX/index.ts index 4a91edb665d5..4ac18e259ab9 100644 --- a/lib/cli/src/generators/RAX/index.js +++ b/lib/cli/src/generators/RAX/index.ts @@ -6,8 +6,9 @@ import { installDependencies, copyTemplate, } from '../../helpers'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const [ storybookVersion, actionsVersion, @@ -57,3 +58,5 @@ export default async (npmOptions, { storyFormat }) => { ...babelDependencies, ]); }; + +export default generator; diff --git a/lib/cli/src/generators/WEBPACK_REACT/index.js b/lib/cli/src/generators/REACT/index.ts similarity index 81% rename from lib/cli/src/generators/WEBPACK_REACT/index.js rename to lib/cli/src/generators/REACT/index.ts index 3f1a140d53fa..586dac39814b 100644 --- a/lib/cli/src/generators/WEBPACK_REACT/index.js +++ b/lib/cli/src/generators/REACT/index.ts @@ -1,23 +1,25 @@ import { - retrievePackageJson, getVersionedPackages, + retrievePackageJson, writePackageJson, getBabelDependencies, installDependencies, copyTemplate, } from '../../helpers'; -import { STORY_FORMAT } from '../../project_types'; +import { StoryFormat } from '../../project_types'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const packages = [ '@storybook/react', '@storybook/addon-actions', '@storybook/addon-links', '@storybook/addons', ]; - if (storyFormat === STORY_FORMAT.MDX) { + if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } + const versionedPackages = await getVersionedPackages(npmOptions, ...packages); copyTemplate(__dirname, storyFormat); @@ -37,3 +39,5 @@ export default async (npmOptions, { storyFormat }) => { installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/generators/REACT_NATIVE/index.js b/lib/cli/src/generators/REACT_NATIVE/index.ts similarity index 91% rename from lib/cli/src/generators/REACT_NATIVE/index.js rename to lib/cli/src/generators/REACT_NATIVE/index.ts index 90970e2dc311..aad02bcb16c6 100644 --- a/lib/cli/src/generators/REACT_NATIVE/index.js +++ b/lib/cli/src/generators/REACT_NATIVE/index.ts @@ -9,8 +9,14 @@ import { installDependencies, copyTemplate, } from '../../helpers'; +import { NpmOptions } from '../../NpmOptions'; +import { GeneratorOptions } from '../Generator'; -export default async (npmOptions, installServer, { storyFormat }) => { +export default async ( + npmOptions: NpmOptions, + installServer: boolean, + { storyFormat }: GeneratorOptions +) => { const [storybookVersion, addonsVersion, actionsVersion, linksVersion] = await getVersions( npmOptions, '@storybook/react-native', diff --git a/lib/cli/src/generators/REACT_SCRIPTS/index.js b/lib/cli/src/generators/REACT_SCRIPTS/index.ts similarity index 85% rename from lib/cli/src/generators/REACT_SCRIPTS/index.js rename to lib/cli/src/generators/REACT_SCRIPTS/index.ts index ab7add020d3c..cc43eafdb98e 100644 --- a/lib/cli/src/generators/REACT_SCRIPTS/index.js +++ b/lib/cli/src/generators/REACT_SCRIPTS/index.ts @@ -8,9 +8,10 @@ import { installDependencies, copyTemplate, } from '../../helpers'; -import { STORY_FORMAT } from '../../project_types'; +import { StoryFormat } from '../../project_types'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const packages = [ '@storybook/react', '@storybook/preset-create-react-app', @@ -19,7 +20,7 @@ export default async (npmOptions, { storyFormat }) => { '@storybook/addons', ]; - if (storyFormat === STORY_FORMAT.MDX) { + if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } @@ -48,3 +49,5 @@ export default async (npmOptions, { storyFormat }) => { installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/generators/RIOT/index.js b/lib/cli/src/generators/RIOT/index.ts similarity index 91% rename from lib/cli/src/generators/RIOT/index.js rename to lib/cli/src/generators/RIOT/index.ts index 8fe243082df5..2c624431018a 100644 --- a/lib/cli/src/generators/RIOT/index.js +++ b/lib/cli/src/generators/RIOT/index.ts @@ -6,8 +6,9 @@ import { installDependencies, copyTemplate, } from '../../helpers'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const [ storybookVersion, actionsVersion, @@ -52,3 +53,5 @@ export default async (npmOptions, { storyFormat }) => { installDependencies({ ...npmOptions, packageJson }, [...dependencies, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/generators/SFC_VUE/index.js b/lib/cli/src/generators/SFC_VUE/index.ts similarity index 81% rename from lib/cli/src/generators/SFC_VUE/index.js rename to lib/cli/src/generators/SFC_VUE/index.ts index 99317a86ccb5..0f85dbce6b12 100644 --- a/lib/cli/src/generators/SFC_VUE/index.js +++ b/lib/cli/src/generators/SFC_VUE/index.ts @@ -6,16 +6,17 @@ import { installDependencies, copyTemplate, } from '../../helpers'; -import { STORY_FORMAT } from '../../project_types'; +import { StoryFormat } from '../../project_types'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const packages = [ '@storybook/vue', '@storybook/addon-actions', '@storybook/addon-links', '@storybook/addons', ]; - if (storyFormat === STORY_FORMAT.MDX) { + if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } const versionedPackages = await getVersionedPackages(npmOptions, ...packages); @@ -37,3 +38,5 @@ export default async (npmOptions, { storyFormat }) => { installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/generators/SVELTE/index.js b/lib/cli/src/generators/SVELTE/index.ts similarity index 90% rename from lib/cli/src/generators/SVELTE/index.js rename to lib/cli/src/generators/SVELTE/index.ts index b03a26eb23bb..9da7bfa15463 100644 --- a/lib/cli/src/generators/SVELTE/index.js +++ b/lib/cli/src/generators/SVELTE/index.ts @@ -6,8 +6,9 @@ import { installDependencies, copyTemplate, } from '../../helpers'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const [ storybookVersion, actionsVersion, @@ -50,3 +51,5 @@ export default async (npmOptions, { storyFormat }) => { ...babelDependencies, ]); }; + +export default generator; diff --git a/lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.js b/lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.ts similarity index 83% rename from lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.js rename to lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.ts index 3dbf3582399a..ab3a1e0fcf79 100644 --- a/lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.js +++ b/lib/cli/src/generators/UPDATE_PACKAGE_ORGANIZATIONS/index.ts @@ -3,22 +3,29 @@ import path from 'path'; import { sync as spawnSync } from 'cross-spawn'; import { packageNames } from '@storybook/codemod'; import { + getBabelDependencies, + getPackageJson, getVersion, getVersions, - getBabelDependencies, installDependencies, - getPackageJson, writePackageJson, } from '../../helpers'; +import { PackageJson } from '../../PackageJson'; +import { NpmOptions } from '../../NpmOptions'; -async function updatePackage(devDependencies, oldName, newName, npmOptions) { +async function updatePackage( + devDependencies: PackageJson['devDependencies'], + oldName: string, + newName: string, + npmOptions: NpmOptions +) { if (devDependencies[oldName]) { delete devDependencies[oldName]; devDependencies[newName] = await getVersion(npmOptions, newName); } } -async function updatePackageJson(npmOptions) { +async function updatePackageJson(npmOptions: NpmOptions) { const packageJson = getPackageJson(); const { devDependencies } = packageJson; @@ -51,7 +58,7 @@ async function updatePackageJson(npmOptions) { } } -function updateSourceCode(parser) { +function updateSourceCode(parser: string) { const jscodeshiftPath = path.dirname(require.resolve('jscodeshift')); const jscodeshiftCommand = path.join(jscodeshiftPath, 'bin', 'jscodeshift.sh'); @@ -69,7 +76,7 @@ function updateSourceCode(parser) { }); } -export default async (parser, npmOptions) => { +export default async (parser: string, npmOptions: NpmOptions) => { await updatePackageJson(npmOptions); updateSourceCode(parser); }; diff --git a/lib/cli/src/generators/VUE/index.js b/lib/cli/src/generators/VUE/index.ts similarity index 88% rename from lib/cli/src/generators/VUE/index.js rename to lib/cli/src/generators/VUE/index.ts index eb3364907d49..01e33e1978cb 100644 --- a/lib/cli/src/generators/VUE/index.js +++ b/lib/cli/src/generators/VUE/index.ts @@ -8,9 +8,10 @@ import { addToDevDependenciesIfNotPresent, copyTemplate, } from '../../helpers'; -import { STORY_FORMAT } from '../../project_types'; +import { StoryFormat } from '../../project_types'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const packages = [ '@storybook/vue', '@storybook/addon-actions', @@ -19,7 +20,7 @@ export default async (npmOptions, { storyFormat }) => { 'babel-preset-vue', '@babel/core', ]; - if (storyFormat === STORY_FORMAT.MDX) { + if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } const versionedPackages = await getVersionedPackages(npmOptions, ...packages); @@ -57,3 +58,5 @@ export default async (npmOptions, { storyFormat }) => { // see: https://github.com/storybookjs/storybook/issues/4475#issuecomment-432141296 installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/generators/WEB-COMPONENTS/index.js b/lib/cli/src/generators/WEB-COMPONENTS/index.ts similarity index 82% rename from lib/cli/src/generators/WEB-COMPONENTS/index.js rename to lib/cli/src/generators/WEB-COMPONENTS/index.ts index 2b6159651136..373eac327a67 100755 --- a/lib/cli/src/generators/WEB-COMPONENTS/index.js +++ b/lib/cli/src/generators/WEB-COMPONENTS/index.ts @@ -7,9 +7,10 @@ import { getBabelDependencies, installDependencies, } from '../../helpers'; -import { STORY_FORMAT } from '../../project_types'; +import { StoryFormat } from '../../project_types'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const packages = [ '@storybook/web-components', '@storybook/addon-actions', @@ -21,7 +22,7 @@ export default async (npmOptions, { storyFormat }) => { fse.copySync(path.resolve(__dirname, 'template/'), '.', { overwrite: true }); - if (storyFormat === STORY_FORMAT.MDX) { + if (storyFormat === StoryFormat.MDX) { // TODO: handle adding of docs mode } @@ -40,3 +41,5 @@ export default async (npmOptions, { storyFormat }) => { installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/generators/REACT/index.js b/lib/cli/src/generators/WEBPACK_REACT/index.ts similarity index 82% rename from lib/cli/src/generators/REACT/index.js rename to lib/cli/src/generators/WEBPACK_REACT/index.ts index cd014d1bef97..75bec7324ed2 100644 --- a/lib/cli/src/generators/REACT/index.js +++ b/lib/cli/src/generators/WEBPACK_REACT/index.ts @@ -1,24 +1,24 @@ import { - getVersionedPackages, retrievePackageJson, + getVersionedPackages, writePackageJson, getBabelDependencies, installDependencies, copyTemplate, } from '../../helpers'; -import { STORY_FORMAT } from '../../project_types'; +import { StoryFormat } from '../../project_types'; +import { Generator } from '../Generator'; -export default async (npmOptions, { storyFormat }) => { +const generator: Generator = async (npmOptions, { storyFormat }) => { const packages = [ '@storybook/react', '@storybook/addon-actions', '@storybook/addon-links', '@storybook/addons', ]; - if (storyFormat === STORY_FORMAT.MDX) { + if (storyFormat === StoryFormat.MDX) { packages.push('@storybook/addon-docs'); } - const versionedPackages = await getVersionedPackages(npmOptions, ...packages); copyTemplate(__dirname, storyFormat); @@ -38,3 +38,5 @@ export default async (npmOptions, { storyFormat }) => { installDependencies({ ...npmOptions, packageJson }, [...versionedPackages, ...babelDependencies]); }; + +export default generator; diff --git a/lib/cli/src/has_yarn.js b/lib/cli/src/has_yarn.ts similarity index 63% rename from lib/cli/src/has_yarn.js rename to lib/cli/src/has_yarn.ts index a6708adc0425..cba80af6cb5e 100644 --- a/lib/cli/src/has_yarn.js +++ b/lib/cli/src/has_yarn.ts @@ -1,10 +1,10 @@ -import { sync as spawnSync } from 'cross-spawn'; +import { sync } from 'cross-spawn'; import path from 'path'; import findUp from 'find-up'; export function hasYarn() { - const yarnAvailable = spawnSync('yarn', ['--version'], { silent: true }); - const npmAvailable = spawnSync('npm', ['--version'], { silent: true }); + const yarnAvailable = sync('yarn', ['--version']); + const npmAvailable = sync('npm', ['--version']); const lockFile = findUp.sync(['yarn.lock', 'package-lock.json']); const hasYarnLock = lockFile && path.basename(lockFile) === 'yarn.lock'; diff --git a/lib/cli/src/helpers.test.js b/lib/cli/src/helpers.test.ts similarity index 62% rename from lib/cli/src/helpers.test.js rename to lib/cli/src/helpers.test.ts index 4b7931e36dd8..47dedbb8da34 100644 --- a/lib/cli/src/helpers.test.js +++ b/lib/cli/src/helpers.test.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import fse from 'fs-extra'; import * as helpers from './helpers'; -import { STORY_FORMAT } from './project_types'; +import { StoryFormat } from './project_types'; jest.mock('fs', () => ({ existsSync: jest.fn(), @@ -23,31 +23,31 @@ jest.mock('./npm_init', () => ({ 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) => { + it(`should fall back to ${StoryFormat.CSF} + in case ${StoryFormat.CSF_TYPESCRIPT} is not available`, () => { + const csfDirectory = `template-${StoryFormat.CSF}/`; + (fs.existsSync as jest.Mock).mockImplementation((filePath) => { return filePath === csfDirectory; }); - helpers.copyTemplate('', STORY_FORMAT.CSF_TYPESCRIPT); + helpers.copyTemplate('', StoryFormat.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) => { + it(`should use ${StoryFormat.CSF_TYPESCRIPT} if it is available`, () => { + const csfDirectory = `template-${StoryFormat.CSF_TYPESCRIPT}/`; + (fs.existsSync as jest.Mock).mockImplementation((filePath) => { return filePath === csfDirectory; }); - helpers.copyTemplate('', STORY_FORMAT.CSF_TYPESCRIPT); + helpers.copyTemplate('', StoryFormat.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 storyFormat = 'non-existent-format' as StoryFormat; const expectedMessage = `Unsupported story format: ${storyFormat}`; expect(() => { helpers.copyTemplate('', storyFormat); diff --git a/lib/cli/src/helpers.js b/lib/cli/src/helpers.ts similarity index 78% rename from lib/cli/src/helpers.js rename to lib/cli/src/helpers.ts index a9840e5cac31..4e345911d346 100644 --- a/lib/cli/src/helpers.js +++ b/lib/cli/src/helpers.ts @@ -8,13 +8,17 @@ import { gt, satisfies } from 'semver'; import stripJsonComments from 'strip-json-comments'; import { latestVersion } from './latest_version'; -import { version, devDependencies } from '../package.json'; import { npmInit } from './npm_init'; -import { STORY_FORMAT } from './project_types'; +import { StoryFormat } from './project_types'; +import { PackageJson } from './PackageJson'; +import { NpmOptions } from './NpmOptions'; + +// Cannot be `import` as it's not under TS root dir +const { version, devDependencies } = require('../package.json'); const logger = console; -export async function getVersion(npmOptions, packageName, constraint) { +export async function getVersion(npmOptions: NpmOptions, packageName: string, constraint?: any) { let current; if (packageName === '@storybook/cli') { current = version; @@ -42,11 +46,11 @@ export async function getVersion(npmOptions, packageName, constraint) { return `^${versionToUse}`; } -export function getVersions(npmOptions, ...packageNames) { +export function getVersions(npmOptions: NpmOptions, ...packageNames: string[]) { return Promise.all(packageNames.map((packageName) => getVersion(npmOptions, packageName))); } -export function getVersionedPackages(npmOptions, ...packageNames) { +export function getVersionedPackages(npmOptions: NpmOptions, ...packageNames: string[]) { return Promise.all( packageNames.map( async (packageName) => `${packageName}@${await getVersion(npmOptions, packageName)}` @@ -87,7 +91,7 @@ export function getBowerJson() { return JSON.parse(jsonContent); } -export function readFileAsJson(jsonPath, allowComments) { +export function readFileAsJson(jsonPath: string, allowComments?: boolean) { const filePath = path.resolve(jsonPath); if (!fs.existsSync(filePath)) { return false; @@ -98,7 +102,7 @@ export function readFileAsJson(jsonPath, allowComments) { return JSON.parse(jsonContent); } -export const writeFileAsJson = (jsonPath, content) => { +export const writeFileAsJson = (jsonPath: string, content: unknown) => { const filePath = path.resolve(jsonPath); if (!fs.existsSync(filePath)) { return false; @@ -108,17 +112,18 @@ export const writeFileAsJson = (jsonPath, content) => { return true; }; -export function writePackageJson(packageJson) { +export function writePackageJson(packageJson: object) { const content = `${JSON.stringify(packageJson, null, 2)}\n`; const packageJsonPath = path.resolve('package.json'); fs.writeFileSync(packageJsonPath, content, 'utf8'); } -export const commandLog = (message) => { +export const commandLog = (message: string) => { process.stdout.write(chalk.cyan(' • ') + message); - return (errorMessage, errorInfo) => { + // Need `void` to be able to use this function in a then of a Promise + return (errorMessage?: string | void, errorInfo?: string) => { if (errorMessage) { process.stdout.write(`. ${chalk.red('✖')}\n`); logger.error(`\n ${chalk.red(errorMessage)}`); @@ -139,7 +144,7 @@ export const commandLog = (message) => { }; }; -export function paddedLog(message) { +export function paddedLog(message: string) { const newMessage = message .split('\n') .map((line) => ` ${line}`) @@ -148,7 +153,7 @@ export function paddedLog(message) { logger.log(newMessage); } -export function getChars(char, amount) { +export function getChars(char: string, amount: number) { let line = ''; for (let lc = 0; lc < amount; lc += 1) { line += char; @@ -157,7 +162,7 @@ export function getChars(char, amount) { return line; } -export function codeLog(codeLines, leftPadAmount) { +export function codeLog(codeLines: string[], leftPadAmount?: number) { let maxLength = 0; const newLines = codeLines.map((line) => { maxLength = line.length > maxLength ? line.length : maxLength; @@ -176,7 +181,7 @@ export function codeLog(codeLines, leftPadAmount) { logger.log(finalResult); } -export function installDepsFromPackageJson(options) { +export function installDepsFromPackageJson(options: NpmOptions) { let done = commandLog('Preparing to install dependencies'); done(); logger.log(); @@ -200,21 +205,24 @@ export function installDepsFromPackageJson(options) { /** * Add dependencies to a project using `yarn add` or `npm install`. * - * @param {Object} npmOptions contains `useYarn`, `runInstall` and `installAsDevDependencies` which we use to determine how we install packages. + * @param {Object} options contains `useYarn`, `runInstall` and `installAsDevDependencies` which we use to determine how we install packages. * @param {Array} dependencies contains a list of packages to add. * @example - * installDependencies(npmOptions, [ + * installDependencies(options, [ * `@storybook/react@${storybookVersion}`, * `@storybook/addon-actions@${actionsVersion}`, * `@storybook/addon-links@${linksVersion}`, * `@storybook/addons@${addonsVersion}`, * ]); */ -export function installDependencies(npmOptions, dependencies) { - const { skipInstall } = npmOptions; +export function installDependencies( + options: NpmOptions & { packageJson: PackageJson }, + dependencies: string[] +) { + const { skipInstall } = options; if (skipInstall) { - const { packageJson } = npmOptions; + const { packageJson } = options; const dependenciesMap = dependencies.reduce((acc, dep) => { const idx = dep.lastIndexOf('@'); @@ -224,7 +232,7 @@ export function installDependencies(npmOptions, dependencies) { return { ...acc, [packageName]: packageVersion }; }, {}); - if (npmOptions.installAsDevDependencies) { + if (options.installAsDevDependencies) { packageJson.devDependencies = { ...packageJson.devDependencies, ...dependenciesMap, @@ -238,12 +246,12 @@ export function installDependencies(npmOptions, dependencies) { writePackageJson(packageJson); } else { - const spawnCommand = npmOptions.useYarn ? 'yarn' : 'npm'; - const installCommand = npmOptions.useYarn ? 'add' : 'install'; + const spawnCommand = options.useYarn ? 'yarn' : 'npm'; + const installCommand = options.useYarn ? 'add' : 'install'; const installArgs = [installCommand, ...dependencies]; - if (npmOptions.installAsDevDependencies) { + if (options.installAsDevDependencies) { installArgs.push('-D'); } @@ -272,7 +280,7 @@ export function installDependencies(npmOptions, dependencies) { * ...babelDependencies, * ]); */ -export async function getBabelDependencies(npmOptions, packageJson) { +export async function getBabelDependencies(npmOptions: NpmOptions, packageJson: PackageJson) { const dependenciesToAdd = []; let babelLoaderVersion = '^8.0.0-0'; @@ -308,18 +316,22 @@ export async function getBabelDependencies(npmOptions, packageJson) { return dependenciesToAdd; } -export function addToDevDependenciesIfNotPresent(packageJson, name, packageVersion) { +export function addToDevDependenciesIfNotPresent( + packageJson: PackageJson, + name: string, + packageVersion: string +) { if (!packageJson.dependencies[name] && !packageJson.devDependencies[name]) { packageJson.devDependencies[name] = packageVersion; } } -export function copyTemplate(templateRoot, storyFormat) { +export function copyTemplate(templateRoot: string, storyFormat: 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); + if (storyFormat === StoryFormat.CSF_TYPESCRIPT) { + copyTemplate(templateRoot, StoryFormat.CSF); return; } diff --git a/lib/cli/src/initiate.js b/lib/cli/src/initiate.ts similarity index 84% rename from lib/cli/src/initiate.js rename to lib/cli/src/initiate.ts index 8f97273f12be..1c080c029a09 100644 --- a/lib/cli/src/initiate.js +++ b/lib/cli/src/initiate.ts @@ -1,13 +1,13 @@ -import updateNotifier from 'update-notifier'; +import { UpdateNotifier, IPackage } from 'update-notifier'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { detect, isStorybookInstalled, detectLanguage } from './detect'; import { hasYarn } from './has_yarn'; import { installableProjectTypes, - PROJECT_TYPES, - STORY_FORMAT, - SUPPORTED_LANGUAGES, + ProjectType, + StoryFormat, + SupportedLanguage, } from './project_types'; import { commandLog, @@ -38,7 +38,18 @@ import raxGenerator from './generators/RAX'; const logger = console; -const installStorybook = (projectType, options) => { +type CommandOptions = { + useNpm?: boolean; + type?: any; + force?: any; + html?: boolean; + skipInstall?: boolean; + storyFormat?: StoryFormat; + parser?: string; + yes?: boolean; +}; + +const installStorybook = (projectType: ProjectType, options: CommandOptions): Promise => { const useYarn = Boolean(options.useNpm !== true) && hasYarn(); const npmOptions = { @@ -48,9 +59,9 @@ const installStorybook = (projectType, options) => { }; const defaultStoryFormat = - detectLanguage() === SUPPORTED_LANGUAGES.TYPESCRIPT - ? STORY_FORMAT.CSF_TYPESCRIPT - : STORY_FORMAT.CSF; + detectLanguage() === SupportedLanguage.TYPESCRIPT + ? StoryFormat.CSF_TYPESCRIPT + : StoryFormat.CSF; const generatorOptions = { storyFormat: options.storyFormat || defaultStoryFormat, @@ -74,9 +85,9 @@ const installStorybook = (projectType, options) => { const REACT_NATIVE_DISCUSSION = 'https://github.com/storybookjs/storybook/blob/master/app/react-native/docs/manual-setup.md'; - const runGenerator = () => { + const runGenerator: () => Promise = () => { switch (projectType) { - case PROJECT_TYPES.ALREADY_HAS_STORYBOOK: + case ProjectType.ALREADY_HAS_STORYBOOK: logger.log(); paddedLog('There seems to be a storybook already available in this project.'); paddedLog('Apply following command to force:\n'); @@ -86,26 +97,26 @@ const installStorybook = (projectType, options) => { logger.log(); return Promise.resolve(); - case PROJECT_TYPES.UPDATE_PACKAGE_ORGANIZATIONS: + case ProjectType.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 PROJECT_TYPES.REACT_SCRIPTS: + case ProjectType.REACT_SCRIPTS: return reactScriptsGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Create React App" based project')) .then(end); - case PROJECT_TYPES.REACT: + case ProjectType.REACT: return reactGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "React" app')) .then(end); - case PROJECT_TYPES.REACT_NATIVE: { + case ProjectType.REACT_NATIVE: { return (options.yes ? Promise.resolve({ server: true }) - : inquirer.prompt([ + : (inquirer.prompt([ { type: 'confirm', name: 'server', @@ -113,7 +124,7 @@ const installStorybook = (projectType, options) => { 'Do you want to install dependencies necessary to run storybook server? You can manually do it later by install @storybook/react-native-server', default: false, }, - ]) + ]) as Promise<{ server: boolean }>) ) .then(({ server }) => reactNativeGenerator(npmOptions, server, generatorOptions)) .then(commandLog('Adding storybook support to your "React Native" app')) @@ -128,82 +139,82 @@ const installStorybook = (projectType, options) => { }); } - case PROJECT_TYPES.METEOR: + case ProjectType.METEOR: return meteorGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Meteor" app')) .then(end); - case PROJECT_TYPES.WEBPACK_REACT: + case ProjectType.WEBPACK_REACT: return webpackReactGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Webpack React" app')) .then(end); - case PROJECT_TYPES.REACT_PROJECT: + case ProjectType.REACT_PROJECT: return reactGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "React" library')) .then(end); - case PROJECT_TYPES.SFC_VUE: + case ProjectType.SFC_VUE: return sfcVueGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Single File Components Vue" app')) .then(end); - case PROJECT_TYPES.VUE: + case ProjectType.VUE: return vueGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Vue" app')) .then(end); - case PROJECT_TYPES.ANGULAR: + case ProjectType.ANGULAR: return angularGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Angular" app')) .then(end); - case PROJECT_TYPES.EMBER: + case ProjectType.EMBER: return emberGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Ember" app')) .then(end); - case PROJECT_TYPES.MITHRIL: + case ProjectType.MITHRIL: return mithrilGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Mithril" app')) .then(end); - case PROJECT_TYPES.MARIONETTE: - return marionetteGenerator(npmOptions) + case ProjectType.MARIONETTE: + return marionetteGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Marionette.js" app')) .then(end); - case PROJECT_TYPES.MARKO: + case ProjectType.MARKO: return markoGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Marko" app')) .then(end); - case PROJECT_TYPES.HTML: + case ProjectType.HTML: return htmlGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "HTML" app')) .then(end); - case PROJECT_TYPES.WEB_COMPONENTS: + case ProjectType.WEB_COMPONENTS: return webComponentsGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "web components" app')) .then(end); - case PROJECT_TYPES.RIOT: + case ProjectType.RIOT: return riotGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "riot.js" app')) .then(end); - case PROJECT_TYPES.PREACT: + case ProjectType.PREACT: return preactGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Preact" app')) .then(end); - case PROJECT_TYPES.SVELTE: + case ProjectType.SVELTE: return svelteGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Svelte" app')) .then(end); - case PROJECT_TYPES.RAX: + case ProjectType.RAX: return raxGenerator(npmOptions, generatorOptions) .then(commandLog('Adding storybook support to your "Rax" app')) .then(end); @@ -227,7 +238,7 @@ const installStorybook = (projectType, options) => { }); }; -const projectTypeInquirer = async (options) => { +const projectTypeInquirer = async (options: { yes?: boolean }) => { const manualAnswer = options.yes ? true : await inquirer.prompt([ @@ -239,7 +250,7 @@ const projectTypeInquirer = async (options) => { }, ]); - if (manualAnswer.manual) { + if (manualAnswer !== true && manualAnswer.manual) { const frameworkAnswer = await inquirer.prompt([ { type: 'list', @@ -253,12 +264,12 @@ const projectTypeInquirer = async (options) => { return Promise.resolve(); }; -export default function (options, pkg) { +export default function (options: CommandOptions, pkg: IPackage): Promise { const welcomeMessage = 'sb init - the simplest way to add a storybook to your project.'; logger.log(chalk.inverse(`\n ${welcomeMessage} \n`)); // Update notify code. - updateNotifier({ + new UpdateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60, // every hour (we could increase this later on.) }).notify(); @@ -275,7 +286,7 @@ export default function (options, pkg) { if (installableProjectTypes.includes(options.type)) { const storybookInstalled = isStorybookInstalled(getPackageJson(), options.force); projectType = storybookInstalled - ? PROJECT_TYPES.ALREADY_HAS_STORYBOOK + ? ProjectType.ALREADY_HAS_STORYBOOK : options.type.toUpperCase(); } else { done(`The provided project type was not recognized by Storybook.`); diff --git a/lib/cli/src/latest_version.js b/lib/cli/src/latest_version.ts similarity index 85% rename from lib/cli/src/latest_version.js rename to lib/cli/src/latest_version.ts index 7751a1d98751..fded6ea39f36 100644 --- a/lib/cli/src/latest_version.js +++ b/lib/cli/src/latest_version.ts @@ -10,14 +10,19 @@ import { satisfies } from 'semver'; * @param {Object} constraint Version range to use to constraint the returned version * @return {Promise} Promise resolved with a version */ -export async function latestVersion(npmOptions, packageName, constraint) { - let versions; +export async function latestVersion( + npmOptions: { useYarn: boolean }, + packageName: string, + constraint?: any +): Promise { + let versions: string | string[]; // TODO: Refactor things to hide the package manager details: // Create a `PackageManager` interface that expose some functions like `version`, `add` etc // and then create classes that handle the npm/yarn/yarn2 specific behavior if (npmOptions.useYarn) { - const yarnVersion = sync('yarn', ['--version'], { silent: true }) + const yarnVersion = sync('yarn', ['--version']) + // @ts-ignore .output.toString('utf8') .replace(/,/g, '') .replace(/"/g, ''); @@ -32,10 +37,10 @@ export async function latestVersion(npmOptions, packageName, constraint) { } if (!constraint) { - return versions; + return versions as string; } - return versions.reverse().find((version) => satisfies(version, constraint)); + return (versions as string[]).reverse().find((version) => satisfies(version, constraint)); } /** @@ -45,7 +50,7 @@ export async function latestVersion(npmOptions, packageName, constraint) { * @param {Object} constraint Version range to use to constraint the returned version * @returns {Promise>} versions Promise resolved with a version or an array of versions */ -function spawnVersionsWithNpm(packageName, constraint) { +function spawnVersionsWithNpm(packageName: string, constraint: any): Promise { return new Promise((resolve, reject) => { const command = spawn( 'npm', @@ -54,8 +59,6 @@ function spawnVersionsWithNpm(packageName, constraint) { cwd: process.cwd(), env: process.env, stdio: 'pipe', - encoding: 'utf-8', - silent: true, } ); @@ -81,7 +84,7 @@ function spawnVersionsWithNpm(packageName, constraint) { * @param {Object} constraint Version range to use to constraint the returned version * @returns {Promise>} versions Promise resolved with a version or an array of versions */ -function spawnVersionsWithYarn(packageName, constraint) { +function spawnVersionsWithYarn(packageName: string, constraint: any): Promise { return new Promise((resolve, reject) => { const command = spawn( 'yarn', @@ -90,8 +93,6 @@ function spawnVersionsWithYarn(packageName, constraint) { cwd: process.cwd(), env: process.env, stdio: 'pipe', - encoding: 'utf-8', - silent: true, } ); @@ -122,7 +123,10 @@ function spawnVersionsWithYarn(packageName, constraint) { * @param {Object} constraint Version range to use to constraint the returned version * @returns {Promise>} versions Promise resolved with a version or an array of versions */ -async function spawnVersionsWithYarn2(packageName, constraint) { +async function spawnVersionsWithYarn2( + packageName: string, + constraint: any +): Promise { const field = constraint ? 'versions' : 'version'; const commandResult = sync('yarn', ['npm', 'info', packageName, '--fields', field, '--json'], { @@ -130,7 +134,6 @@ async function spawnVersionsWithYarn2(packageName, constraint) { env: process.env, stdio: 'pipe', encoding: 'utf-8', - silent: true, }); if (commandResult.status !== 0) { diff --git a/lib/cli/src/migrate.js b/lib/cli/src/migrate.ts similarity index 69% rename from lib/cli/src/migrate.js rename to lib/cli/src/migrate.ts index 5f243bc4d5e7..89e3e6759fa9 100644 --- a/lib/cli/src/migrate.js +++ b/lib/cli/src/migrate.ts @@ -1,11 +1,11 @@ import { listCodemods, runCodemod } from '@storybook/codemod'; export async function migrate( - migration, - { configDir, glob, dryRun, list, rename, logger, parser } + migration: any, + { configDir, glob, dryRun, list, rename, logger, parser }: any ) { if (list) { - listCodemods().forEach((key) => logger.log(key)); + listCodemods().forEach((key: any) => logger.log(key)); } else if (migration) { await runCodemod(migration, { configDir, glob, dryRun, logger, rename, parser }); } else { diff --git a/lib/cli/src/npm_init.js b/lib/cli/src/npm_init.ts similarity index 59% rename from lib/cli/src/npm_init.js rename to lib/cli/src/npm_init.ts index 1ee1656940b5..f409557809fc 100644 --- a/lib/cli/src/npm_init.js +++ b/lib/cli/src/npm_init.ts @@ -1,15 +1,14 @@ -import { spawn } from 'cross-spawn'; +import { sync } from 'cross-spawn'; import { hasYarn } from './has_yarn'; const packageManager = hasYarn() ? 'yarn' : 'npm'; -export function npmInit() { - const results = spawn.sync(packageManager, ['init', '-y'], { +export function npmInit(): string { + const results = sync(packageManager, ['init', '-y'], { cwd: process.cwd(), env: process.env, stdio: 'pipe', encoding: 'utf-8', - silent: true, }); return results.stdout; } diff --git a/lib/cli/src/project_types.test.js b/lib/cli/src/project_types.test.ts similarity index 100% rename from lib/cli/src/project_types.test.js rename to lib/cli/src/project_types.test.ts diff --git a/lib/cli/src/project_types.js b/lib/cli/src/project_types.ts similarity index 56% rename from lib/cli/src/project_types.js rename to lib/cli/src/project_types.ts index 1bfe979bc49e..48df5d200767 100644 --- a/lib/cli/src/project_types.js +++ b/lib/cli/src/project_types.ts @@ -1,40 +1,44 @@ -export const PROJECT_TYPES = { - UNDETECTED: 'UNDETECTED', - REACT_SCRIPTS: 'REACT_SCRIPTS', - METEOR: 'METEOR', - REACT: 'REACT', - REACT_NATIVE: 'REACT_NATIVE', - REACT_PROJECT: 'REACT_PROJECT', - WEBPACK_REACT: 'WEBPACK_REACT', - VUE: 'VUE', - SFC_VUE: 'SFC_VUE', - ANGULAR: 'ANGULAR', - EMBER: 'EMBER', - ALREADY_HAS_STORYBOOK: 'ALREADY_HAS_STORYBOOK', - UPDATE_PACKAGE_ORGANIZATIONS: 'UPDATE_PACKAGE_ORGANIZATIONS', - WEB_COMPONENTS: 'WEB_COMPONENTS', - MITHRIL: 'MITHRIL', - MARIONETTE: 'MARIONETTE', - MARKO: 'MARKO', - HTML: 'HTML', - RIOT: 'RIOT', - PREACT: 'PREACT', - SVELTE: 'SVELTE', - RAX: 'RAX', -}; - -export const STORY_FORMAT = { - CSF: 'csf', - CSF_TYPESCRIPT: 'csf-ts', - MDX: 'mdx', -}; +export type SupportedFrameworks = + | 'react' + | 'react-native' + | 'vue' + | 'angular' + | 'mithril' + | 'riot' + | 'ember' + | 'marionette' + | 'marko' + | 'meteor' + | 'preact' + | 'svelte' + | 'rax'; -export const SUPPORTED_LANGUAGES = { - JAVASCRIPT: 'javascript', - TYPESCRIPT: 'typescript', -}; +export enum ProjectType { + UNDETECTED = 'UNDETECTED', + REACT_SCRIPTS = 'REACT_SCRIPTS', + METEOR = 'METEOR', + REACT = 'REACT', + REACT_NATIVE = 'REACT_NATIVE', + REACT_PROJECT = 'REACT_PROJECT', + WEBPACK_REACT = 'WEBPACK_REACT', + VUE = 'VUE', + SFC_VUE = 'SFC_VUE', + ANGULAR = 'ANGULAR', + EMBER = 'EMBER', + ALREADY_HAS_STORYBOOK = 'ALREADY_HAS_STORYBOOK', + UPDATE_PACKAGE_ORGANIZATIONS = 'UPDATE_PACKAGE_ORGANIZATIONS', + WEB_COMPONENTS = 'WEB_COMPONENTS', + MITHRIL = 'MITHRIL', + MARIONETTE = 'MARIONETTE', + MARKO = 'MARKO', + HTML = 'HTML', + RIOT = 'RIOT', + PREACT = 'PREACT', + SVELTE = 'SVELTE', + RAX = 'RAX', +} -export const SUPPORTED_FRAMEWORKS = [ +export const SUPPORTED_FRAMEWORKS: SupportedFrameworks[] = [ 'react', 'react-native', 'vue', @@ -50,70 +54,84 @@ export const SUPPORTED_FRAMEWORKS = [ 'rax', ]; +export enum StoryFormat { + CSF = 'csf', + CSF_TYPESCRIPT = 'csf-ts', + MDX = 'mdx', +} + +export enum SupportedLanguage { + JAVASCRIPT = 'javascript', + TYPESCRIPT = 'typescript', +} + +export type TemplateMatcher = { + files?: boolean[]; + dependencies?: boolean[]; + peerDependencies?: boolean[]; +}; + +export type TemplateConfiguration = { + preset: ProjectType; + /** will be checked both against dependencies and devDependencies */ + dependencies?: string[]; + peerDependencies?: string[]; + files?: string[]; + matcherFunction: (matcher: TemplateMatcher) => boolean; +}; + /** - * Configuration objects to match a storybook preset template. + * Configuration 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 = [ +export const supportedTemplates: TemplateConfiguration[] = [ { - preset: PROJECT_TYPES.METEOR, + preset: ProjectType.METEOR, files: ['.meteor'], matcherFunction: ({ files }) => { return files.every(Boolean); }, }, { - preset: PROJECT_TYPES.SFC_VUE, + preset: ProjectType.SFC_VUE, dependencies: ['vue-loader', 'vuetify'], matcherFunction: ({ dependencies }) => { return dependencies.some(Boolean); }, }, { - preset: PROJECT_TYPES.VUE, + preset: ProjectType.VUE, dependencies: ['vue', 'nuxt'], matcherFunction: ({ dependencies }) => { return dependencies.some(Boolean); }, }, { - preset: PROJECT_TYPES.EMBER, + preset: ProjectType.EMBER, dependencies: ['ember-cli'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.REACT_PROJECT, + preset: ProjectType.REACT_PROJECT, peerDependencies: ['react'], matcherFunction: ({ peerDependencies }) => { return peerDependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.REACT_NATIVE, + preset: ProjectType.REACT_NATIVE, dependencies: ['react-native', 'react-native-scripts'], matcherFunction: ({ dependencies }) => { return dependencies.some(Boolean); }, }, { - preset: PROJECT_TYPES.REACT_SCRIPTS, + preset: ProjectType.REACT_SCRIPTS, // For projects using a custom/forked `react-scripts` package. files: ['/node_modules/.bin/react-scripts'], // For standard CRA projects @@ -123,77 +141,77 @@ export const supportedTemplates = [ }, }, { - preset: PROJECT_TYPES.WEBPACK_REACT, + preset: ProjectType.WEBPACK_REACT, dependencies: ['react', 'webpack'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.REACT, + preset: ProjectType.REACT, dependencies: ['react'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.ANGULAR, + preset: ProjectType.ANGULAR, dependencies: ['@angular/core'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.WEB_COMPONENTS, + preset: ProjectType.WEB_COMPONENTS, dependencies: ['lit-element'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.MITHRIL, + preset: ProjectType.MITHRIL, dependencies: ['mithril'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.MARIONETTE, + preset: ProjectType.MARIONETTE, dependencies: ['backbone.marionette'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.MARKO, + preset: ProjectType.MARKO, dependencies: ['marko'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.RIOT, + preset: ProjectType.RIOT, dependencies: ['riot'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.PREACT, + preset: ProjectType.PREACT, dependencies: ['preact'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.SVELTE, + preset: ProjectType.SVELTE, dependencies: ['svelte'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); }, }, { - preset: PROJECT_TYPES.RAX, + preset: ProjectType.RAX, dependencies: ['rax'], matcherFunction: ({ dependencies }) => { return dependencies.every(Boolean); @@ -201,12 +219,12 @@ export const supportedTemplates = [ }, ]; -const notInstallableProjectTypes = [ - PROJECT_TYPES.UNDETECTED, - PROJECT_TYPES.ALREADY_HAS_STORYBOOK, - PROJECT_TYPES.UPDATE_PACKAGE_ORGANIZATIONS, +const notInstallableProjectTypes: ProjectType[] = [ + ProjectType.UNDETECTED, + ProjectType.ALREADY_HAS_STORYBOOK, + ProjectType.UPDATE_PACKAGE_ORGANIZATIONS, ]; -export const installableProjectTypes = Object.values(PROJECT_TYPES) +export const installableProjectTypes = Object.values(ProjectType) .filter((type) => !notInstallableProjectTypes.includes(type)) .map((type) => type.toLowerCase()); diff --git a/lib/cli/src/typings.d.ts b/lib/cli/src/typings.d.ts new file mode 100644 index 000000000000..75cb2ee22218 --- /dev/null +++ b/lib/cli/src/typings.d.ts @@ -0,0 +1,2 @@ +declare module '@storybook/codemod'; +declare module 'envinfo'; diff --git a/lib/cli/src/window.d.ts b/lib/cli/src/window.d.ts new file mode 100644 index 000000000000..420d0c9916c0 --- /dev/null +++ b/lib/cli/src/window.d.ts @@ -0,0 +1,7 @@ +import { StoryStore } from '@storybook/client-api'; + +declare global { + interface Window { + __STORYBOOK_STORY_STORE__: StoryStore; + } +} diff --git a/lib/cli/tsconfig.json b/lib/cli/tsconfig.json new file mode 100644 index 000000000000..c4e40706894b --- /dev/null +++ b/lib/cli/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "types": ["node", "jest"], + "strict": false, + "strictNullChecks": false, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["src/**/template*"] +} diff --git a/yarn.lock b/yarn.lock index fc2c0dbeed8b..8bccbc494019 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4580,6 +4580,14 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" integrity sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA== +"@types/inquirer@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be" + integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/is-function@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" @@ -4779,6 +4787,20 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/puppeteer-core@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/puppeteer-core/-/puppeteer-core-2.0.0.tgz#3b7fbbac53d56b566f5ef096116e1d60d504aa45" + integrity sha512-JvoEb7KgEkUet009ZDrtpUER3hheXoHgQByuYpJZ5WWT7LWwMH+0NTqGQXGgoOKzs+G5NA1T4DZwXK79Bhnejw== + dependencies: + "@types/puppeteer" "*" + +"@types/puppeteer@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-2.1.0.tgz#31367580654632f87f86df565f1bde0533577401" + integrity sha512-QIRQXl0VaSgnwOZ1LwxD321Tfb1jLOzCWuF2BrwjEkWq2IhxSicPOddUywLV7dRSO6mcU4sWKRdoGdci6gk0Aw== + dependencies: + "@types/node" "*" + "@types/puppeteer@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-2.1.0.tgz#31367580654632f87f86df565f1bde0533577401" @@ -4920,6 +4942,13 @@ dependencies: "@types/node" "*" +"@types/semver@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b" + integrity sha512-TbB0A8ACUWZt3Y6bQPstW9QNbhNeebdgLX4T/ZfkrswAfUzRiXrgd9seol+X379Wa589Pu4UEx9Uok0D4RjRCQ== + dependencies: + "@types/node" "*" + "@types/serve-static@*": version "1.13.3" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -4928,6 +4957,14 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/shelljs@^0.8.7": + version "0.8.7" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.7.tgz#a2a606b185165abadf8b7995fea5e326e637088e" + integrity sha512-Mg2qGjLIJIieeJ1/NjswAOY9qXDShLeh6JwpD1NZsvUvI0hxdUCNDpnBXv9YQeugKi2EHU+BqkbUE4jpY4GKmQ== + dependencies: + "@types/glob" "*" + "@types/node" "*" + "@types/sinon-chai@3.2.3": version "3.2.3" resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.3.tgz#afe392303dda95cc8069685d1e537ff434fa506e" @@ -4994,6 +5031,13 @@ "@types/testing-library__dom" "*" pretty-format "^25.1.0" +"@types/through@*": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@types/tmp@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.1.0.tgz#19cf73a7bcf641965485119726397a096f0049bd" @@ -5011,6 +5055,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== +"@types/update-notifier@^0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/update-notifier/-/update-notifier-0.0.30.tgz#5148ffc81189832870ee3f19cf2bb1a35250b05e" + integrity sha1-UUj/yBGJgyhw7j8Zzyuxo1JQsF4= + "@types/util-deprecate@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/util-deprecate/-/util-deprecate-1.0.0.tgz#341d0815fe5a661b94e3ea738d182b4c359e3958"