Merge pull request #18781 from storybookjs/tom/sb-568-script-asks-the-maintainer-which

Create a new `yarn example` command that will drive our new dev experience
This commit is contained in:
Tom Coleman 2022-07-31 14:41:43 +10:00 committed by GitHub
commit d3aa364597
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 777 additions and 22 deletions

View File

@ -410,6 +410,20 @@ jobs:
command: |
cd code
yarn lint
script-unit-tests:
executor: sb_node_14_browsers
steps:
- git-shallow-clone/checkout_advanced:
clone_options: '--depth 1 --verbose'
- attach_workspace:
at: .
- run:
name: Test
command: |
cd scripts
yarn test --coverage --runInBand --ci
- store_test_results:
path: scripts/junit.xml
unit-tests:
executor: sb_node_14_browsers
steps:
@ -462,6 +476,9 @@ workflows:
- unit-tests:
requires:
- build
- script-unit-tests:
requires:
- build
- coverage:
requires:
- unit-tests

3
.gitignore vendored
View File

@ -16,4 +16,5 @@ junit.xml
!/**/.yarn/sdks
!/**/.yarn/versions
/**/.pnp.*
/yarn.lock
/yarn.lock
./examples/

View File

@ -46,7 +46,7 @@
"@storybook/client-logger": "7.0.0-alpha.17",
"@storybook/components": "7.0.0-alpha.17",
"@storybook/core-events": "7.0.0-alpha.17",
"@storybook/csf": "0.0.2--canary.7c6c115.0",
"@storybook/csf": "0.0.2--canary.0899bb7.0",
"@storybook/docs-tools": "7.0.0-alpha.17",
"@storybook/preview-web": "7.0.0-alpha.17",
"@storybook/store": "7.0.0-alpha.17",

View File

@ -150,8 +150,9 @@ program
.command('link <repo-url-or-directory>')
.description('Pull down a repro from a URL (or a local directory), link it, and run storybook')
.option('--local', 'Link a local directory already in your file system')
.action((target, { local }) =>
link({ target, local }).catch((e) => {
.option('--no-start', 'Start the storybook', true)
.action((target, { local, start }) =>
link({ target, local, start }).catch((e) => {
logger.error(e);
process.exit(1);
})

View File

@ -7,9 +7,10 @@ import { exec } from './repro-generators/scripts';
interface LinkOptions {
target: string;
local?: boolean;
start: boolean;
}
export const link = async ({ target, local }: LinkOptions) => {
export const link = async ({ target, local, start }: LinkOptions) => {
const storybookDir = process.cwd();
try {
const packageJson = JSON.parse(fse.readFileSync('package.json', 'utf8'));
@ -58,6 +59,8 @@ export const link = async ({ target, local }: LinkOptions) => {
);
await exec(`yarn add -D webpack-hot-middleware`, { cwd: reproDir });
logger.info(`Running ${reproName} storybook`);
await exec(`yarn run storybook`, { cwd: reproDir });
if (start) {
logger.info(`Running ${reproName} storybook`);
await exec(`yarn run storybook`, { cwd: reproDir });
}
};

View File

@ -51,11 +51,19 @@ export interface Options extends Parameters {
export const exec = async (
command: string,
options: ExecOptions = {},
{ startMessage, errorMessage }: { startMessage?: string; errorMessage?: string } = {}
{
startMessage,
errorMessage,
dryRun,
}: { startMessage?: string; errorMessage?: string; dryRun?: boolean } = {}
) => {
if (startMessage) {
logger.info(startMessage);
if (startMessage) logger.info(startMessage);
if (dryRun) {
logger.info(`\n> ${command}\n`);
return undefined;
}
logger.debug(command);
return new Promise((resolve, reject) => {
const defaultOptions: ExecOptions = {

View File

@ -62,6 +62,7 @@
"clean:dist": "del **/dist",
"coverage": "codecov",
"danger": "danger",
"example": "ts-node ../scripts/example.ts",
"generate-repros": "zx ../scripts/repros-generator/index.mjs",
"github-release": "github-release-from-changelog",
"linear-export": "ts-node --project=../scripts/tsconfig.json ../scripts/linear-export.ts",

View File

@ -7522,7 +7522,7 @@ __metadata:
"@storybook/client-logger": 7.0.0-alpha.17
"@storybook/components": 7.0.0-alpha.17
"@storybook/core-events": 7.0.0-alpha.17
"@storybook/csf": 0.0.2--canary.7c6c115.0
"@storybook/csf": 0.0.2--canary.0899bb7.0
"@storybook/docs-tools": 7.0.0-alpha.17
"@storybook/preview-web": 7.0.0-alpha.17
"@storybook/store": 7.0.0-alpha.17
@ -8076,15 +8076,6 @@ __metadata:
languageName: node
linkType: hard
"@storybook/csf@npm:0.0.2--canary.7c6c115.0":
version: 0.0.2--canary.7c6c115.0
resolution: "@storybook/csf@npm:0.0.2--canary.7c6c115.0"
dependencies:
lodash: ^4.17.15
checksum: 85a179664d18eeca8462c1b6ff36f9b68b856c9f9c5143aa6f19b17e4cc97bc08ed69921a5287a61d8c90f61366ff6a5ab89930d158402e7c04d07a3ffaad8bb
languageName: node
linkType: hard
"@storybook/csf@npm:^0.0.1":
version: 0.0.1
resolution: "@storybook/csf@npm:0.0.1"

@ -1 +0,0 @@
Subproject commit b7ef5bd9f548330f7a0c0b0f6b8d285bcd12a6f7

235
scripts/example.ts Normal file
View File

@ -0,0 +1,235 @@
import path from 'path';
import { remove, pathExists, readJSON, writeJSON } from 'fs-extra';
import prompts from 'prompts';
import { getOptionsOrPrompt } from './utils/options';
import { executeCLIStep } from './utils/cli-step';
import { exec } from '../code/lib/cli/src/repro-generators/scripts';
import type { Parameters } from '../code/lib/cli/src/repro-generators/configs';
import { getInterpretedFile } from '../code/lib/core-common';
import { readConfig, writeConfig } from '../code/lib/csf-tools';
import { babelParse } from '../code/lib/csf-tools/src/babelParse';
const frameworks = ['react', 'angular'];
const addons = ['a11y', 'storysource'];
const examplesDir = path.resolve(__dirname, '../examples');
const codeDir = path.resolve(__dirname, '../code');
async function getOptions() {
return getOptionsOrPrompt('yarn example', {
framework: {
description: 'Which framework would you like to use?',
values: frameworks,
required: true as const,
},
addon: {
description: 'Which extra addons (beyond the CLI defaults) would you like installed?',
values: addons,
multiple: true as const,
},
includeStories: {
description: "Include Storybook's own stories?",
promptType: (_, { framework }) => framework === 'react',
},
create: {
description: 'Create the example from scratch (rather than degitting it)?',
},
forceDelete: {
description: 'Always delete an existing example, even if it has the same configuration?',
promptType: false,
},
forceReuse: {
description: 'Always reuse an existing example, even if it has a different configuration?',
promptType: false,
},
link: {
description: 'Link the storybook to the local code?',
inverse: true,
},
start: {
description: 'Start the example Storybook?',
inverse: true,
},
build: {
description: 'Build the example Storybook?',
},
watch: {
description: 'Start building used packages in watch mode as well as the example Storybook?',
},
dryRun: {
description: "Don't execute commands, just list them (dry run)?",
},
});
}
const steps = {
repro: {
command: 'repro',
description: 'Bootstrapping example',
icon: '👷',
hasArgument: true,
options: {
template: { values: frameworks },
e2e: {},
},
},
add: {
command: 'add',
description: 'Adding addon',
icon: '+',
hasArgument: true,
options: {},
},
link: {
command: 'link',
description: 'Linking packages',
icon: '🔗',
hasArgument: true,
options: { local: {}, start: { inverse: true } },
},
build: {
command: 'build',
description: 'Building example',
icon: '🔨',
options: {},
},
dev: {
command: 'dev',
description: 'Starting example',
icon: '🖥 ',
options: {},
},
};
const logger = console;
export const overrideMainConfig = async ({
cwd,
mainOverrides,
}: {
cwd: string;
mainOverrides: Parameters['mainOverrides'];
}) => {
logger.info(`📝 Overwriting main.js with the following configuration:`);
const configDir = path.join(cwd, '.storybook');
const mainConfigPath = getInterpretedFile(path.resolve(configDir, 'main'));
logger.debug(mainOverrides);
const mainConfig = await readConfig(mainConfigPath);
Object.keys(mainOverrides).forEach((field) => {
// NOTE: using setFieldNode and passing the output of babelParse()
mainConfig.setFieldNode([field], mainOverrides[field]);
});
await writeConfig(mainConfig);
};
const addPackageScripts = async ({
cwd,
scripts,
}: {
cwd: string;
scripts: Record<string, string>;
}) => {
logger.info(`🔢 Adding package resolutions:`);
const packageJsonPath = path.join(cwd, 'package.json');
const packageJson = await readJSON(packageJsonPath);
packageJson.scripts = {
...packageJson.scripts,
...scripts,
};
await writeJSON(packageJsonPath, packageJson, { spaces: 2 });
};
async function main() {
const optionValues = await getOptions();
const { framework, forceDelete, forceReuse, link, dryRun } = optionValues;
const cwd = path.join(examplesDir, framework as string);
const exists = await pathExists(cwd);
let shouldDelete = exists && !forceReuse;
if (exists && !forceDelete && !forceReuse) {
const relativePath = path.relative(process.cwd(), cwd);
({ shouldDelete } = await prompts({
type: 'toggle',
message: `${relativePath} already exists, should delete it and create a new one?`,
name: 'shouldDelete',
initial: false,
active: 'yes',
inactive: 'no',
}));
}
if (exists && shouldDelete && !dryRun) await remove(cwd);
if (!exists || shouldDelete) {
await executeCLIStep(steps.repro, {
argument: cwd,
optionValues: { template: framework },
cwd: examplesDir,
dryRun,
});
// TODO -- sb add <addon> doesn't actually work properly:
// - installs in `deps` not `devDeps`
// - does a `workspace:^` install (what does that mean?)
// - doesn't add to `main.js`
// eslint-disable-next-line no-restricted-syntax
for (const addon of optionValues.addon as string[]) {
const addonName = `@storybook/addon-${addon}`;
// eslint-disable-next-line no-await-in-loop
await executeCLIStep(steps.add, { argument: addonName, cwd, dryRun });
}
// TODO copy stories
if (link) {
await executeCLIStep(steps.link, {
argument: cwd,
cwd: codeDir,
dryRun,
optionValues: { local: true, start: false },
});
// TODO -- work out exactly where this should happen
const code = '(c) => ({ ...c, resolve: { ...c.resolve, symlinks: false } })';
const mainOverrides = {
// @ts-ignore (not sure why TS complains here, it does exist)
webpackFinal: babelParse(code).program.body[0].expression,
};
await overrideMainConfig({ cwd, mainOverrides } as any);
await addPackageScripts({
cwd,
scripts: {
storybook:
'NODE_OPTIONS="--preserve-symlinks --preserve-symlinks-main" storybook dev -p 6006',
'build-storybook':
'NODE_OPTIONS="--preserve-symlinks --preserve-symlinks-main" storybook build',
},
});
}
}
const { start } = optionValues;
if (start) {
await exec(
'yarn storybook',
{ cwd },
{
dryRun,
startMessage: `⬆️ Starting Storybook`,
errorMessage: `🚨 Starting Storybook failed`,
}
);
} else {
await executeCLIStep(steps.build, { cwd, dryRun });
// TODO serve
}
// TODO start dev
}
main().catch((err) => console.error(err));

1
scripts/jest.config.js Normal file
View File

@ -0,0 +1 @@
module.exports = {};

View File

@ -6,7 +6,8 @@
"lint": "yarn lint:js && yarn lint:md",
"lint:js": "yarn lint:js:cmd . --quiet",
"lint:js:cmd": "cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives",
"lint:package": "sort-package-json"
"lint:package": "sort-package-json",
"test": "jest --config ./jest.config.js"
},
"husky": {
"hooks": {
@ -54,6 +55,7 @@
"@compodoc/compodoc": "^1.1.18",
"@emotion/babel-plugin": "^11.7.2",
"@emotion/jest": "^11.8.0",
"@jest/globals": "^26.6.2",
"@linear/sdk": "^1.21.0",
"@nicolo-ribaudo/chokidar-2": "^2.1.8",
"@nrwl/cli": "12.3.4",
@ -140,6 +142,7 @@
"jest-watch-typeahead": "^0.6.1",
"js-yaml": "^3.14.1",
"lint-staged": "^10.5.4",
"lodash": "^4.17.21",
"mocha-list-tests": "^1.0.5",
"node-cleanup": "^2.1.2",
"node-fetch": "^2.6.1",
@ -190,5 +193,8 @@
"engines": {
"node": ">=10.13.0",
"yarn": ">=1.3.2"
},
"devDependencies": {
"@types/lodash": "^4"
}
}

43
scripts/utils/cli-step.ts Normal file
View File

@ -0,0 +1,43 @@
import { getCommand, OptionSpecifier, OptionValues } from './options';
import { exec } from '../../code/lib/cli/src/repro-generators/scripts';
const cliExecutable = require.resolve('../../code/lib/cli/bin/index.js');
export type CLIStep<TOptions extends OptionSpecifier> = {
command: string;
description: string;
hasArgument?: boolean;
icon: string;
// It would be kind of great to be able to share these with `lib/cli/src/generate.ts`
options: TOptions;
};
export async function executeCLIStep<TOptions extends OptionSpecifier>(
cliStep: CLIStep<TOptions>,
options: {
argument?: string;
optionValues?: Partial<OptionValues<TOptions>>;
cwd: string;
dryRun?: boolean;
}
) {
if (cliStep.hasArgument && !options.argument)
throw new Error(`Argument required for ${cliStep.command} command.`);
const prefix = `node ${cliExecutable} ${cliStep.command}`;
const command = getCommand(
cliStep.hasArgument ? `${prefix} ${options.argument}` : prefix,
cliStep.options,
options.optionValues || {}
);
await exec(
command,
{ cwd: options.cwd },
{
startMessage: `${cliStep.icon} ${cliStep.description}`,
errorMessage: `🚨 ${cliStep.description} failed`,
dryRun: options.dryRun,
}
);
}

View File

@ -0,0 +1,165 @@
import { createCommand } from 'commander';
import { describe, it, expect } from '@jest/globals';
import {
getOptions,
areOptionsSatisfied,
getCommand,
OptionValues,
MaybeOptionValues,
} from './options';
const allOptions = {
first: {
description: 'first',
},
second: {
description: 'second',
inverse: true,
},
third: {
description: 'third',
values: ['one', 'two', 'three'],
required: true as const,
},
fourth: {
description: 'fourth',
values: ['a', 'b', 'c'],
multiple: true as const,
},
};
// TS "tests"
// deepscan-disable-next-line
function test(mv: MaybeOptionValues<typeof allOptions>, v: OptionValues<typeof allOptions>) {
console.log(mv.first, mv.second, mv.third, mv.fourth);
// @ts-expect-error as it's not allowed
console.log(mv.fifth);
console.log(v.first, v.second, v.third, v.fourth);
// @ts-expect-error as it's not allowed
console.log(v.fifth);
}
describe('getOptions', () => {
it('deals with boolean options', () => {
expect(getOptions(createCommand(), allOptions, ['command', 'name', '--first'])).toMatchObject({
first: true,
second: true,
});
});
it('deals with inverse boolean options', () => {
expect(
getOptions(createCommand(), allOptions, ['command', 'name', '--no-second'])
).toMatchObject({
first: false,
second: false,
});
});
it('deals with short options', () => {
expect(getOptions(createCommand(), allOptions, ['command', 'name', '-f', '-S'])).toMatchObject({
first: true,
second: false,
});
});
it('deals with string options', () => {
const r = getOptions(createCommand(), allOptions, ['command', 'name', '--third', 'one']);
expect(
getOptions(createCommand(), allOptions, ['command', 'name', '--third', 'one'])
).toMatchObject({
third: 'one',
});
});
it('disallows invalid string options', () => {
expect(() =>
getOptions(createCommand(), allOptions, ['command', 'name', '--third', 'random'])
).toThrow(/Unexpected value/);
});
it('deals with multiple string options', () => {
expect(
getOptions(createCommand(), allOptions, ['command', 'name', '--fourth', 'a'])
).toMatchObject({
fourth: ['a'],
});
expect(
getOptions(createCommand(), allOptions, ['command', 'name', '--fourth', 'a', '--fourth', 'b'])
).toMatchObject({
fourth: ['a', 'b'],
});
});
it('disallows invalid multiple string options', () => {
expect(() =>
getOptions(createCommand(), allOptions, ['command', 'name', '--fourth', 'random'])
).toThrow(/Unexpected value/);
});
});
describe('areOptionsSatisfied', () => {
it('checks each required string option has a value', () => {
expect(
areOptionsSatisfied(allOptions, {
first: true,
second: true,
third: undefined,
fourth: ['a', 'c'],
})
).toBe(false);
expect(
areOptionsSatisfied(allOptions, {
first: true,
second: true,
third: 'one',
fourth: [],
})
).toBe(true);
});
});
describe('getCommand', () => {
const { first, second, third, fourth } = allOptions;
it('works with boolean options', () => {
expect(getCommand('node foo', { first, second }, { first: true, second: true })).toBe(
'node foo --first'
);
});
it('works with inverse boolean options', () => {
expect(getCommand('node foo', { first, second }, { first: false, second: false })).toBe(
'node foo --no-second'
);
});
it('works with string options', () => {
expect(getCommand('node foo', { third }, { third: 'one' })).toBe('node foo --third one');
});
it('works with multiple string options', () => {
expect(getCommand('node foo', { fourth }, { fourth: ['a', 'b'] })).toBe(
'node foo --fourth a --fourth b'
);
});
// This is for convenience
it('works with partial options', () => {
expect(getCommand('node foo', allOptions, { third: 'one' })).toBe(
'node foo --no-second --third one'
);
});
it('works with combinations string options', () => {
expect(
getCommand('node foo', allOptions, {
first: true,
second: false,
third: 'one',
fourth: ['a', 'b'],
})
).toBe('node foo --first --no-second --third one --fourth a --fourth b');
});
});

274
scripts/utils/options.ts Normal file
View File

@ -0,0 +1,274 @@
/**
* Use commander and prompts to gather a list of options for a script
*/
import prompts, { Falsy, PrevCaller, PromptType } from 'prompts';
import type { PromptObject } from 'prompts';
import program from 'commander';
import kebabCase from 'lodash/kebabCase';
// Option types
export type OptionId = string;
export type BaseOption = {
description?: string;
/**
* By default the one-char version of the option key will be used as short flag. Override here,
* e.g. `shortFlag: 'c'`
*/
shortFlag?: string;
/**
* What type of prompt to use? (return false to skip, true for default)
*/
promptType?: PromptType | Falsy | PrevCaller<string, PromptType | boolean>;
};
export type BooleanOption = BaseOption & {
/**
* Does this option default true?
*/
inverse?: boolean;
};
export type StringOption = BaseOption & {
/**
* What values are allowed for this option?
*/
values: string[];
/**
* Is a value required for this option?
*/
required?: boolean;
};
export type StringArrayOption = BaseOption & {
/**
* What values are allowed for this option?
*/
values: string[];
/**
* This must be set to true
*/
multiple: true;
};
type StringArrayOptionMatch = Omit<StringArrayOption, 'multiple'> & { multiple: true };
export type Option = BooleanOption | StringOption | StringArrayOption;
export type MaybeOptionValue<TOption extends Option> = TOption extends StringArrayOptionMatch
? string[]
: TOption extends StringOption
? string | undefined
: TOption extends BooleanOption
? boolean
: never;
export type OptionValue<TOption extends Option> = TOption extends { required: true }
? string
: MaybeOptionValue<TOption>;
export type OptionSpecifier = Record<OptionId, Option>;
export type MaybeOptionValues<TOptions extends OptionSpecifier> = {
[TKey in keyof TOptions]: MaybeOptionValue<TOptions[TKey]>;
};
export type OptionValues<TOptions extends OptionSpecifier = OptionSpecifier> = {
[TKey in keyof TOptions]: OptionValue<TOptions[TKey]>;
};
export function isStringOption(option: Option): option is StringOption {
return 'values' in option && !('multiple' in option);
}
export function isBooleanOption(option: Option): option is BooleanOption {
return !('values' in option);
}
export function isStringArrayOption(option: Option): option is StringArrayOption {
return 'values' in option && 'multiple' in option;
}
function shortFlag(key: OptionId, option: Option) {
const inverse = isBooleanOption(option) && option.inverse;
const defaultShortFlag = inverse ? key.substring(0, 1).toUpperCase() : key.substring(0, 1);
const short = option.shortFlag || defaultShortFlag;
if (short.length !== 1) {
throw new Error(
`Invalid shortFlag for ${key}: '${short}', needs to be a single character (e.g. 's')`
);
}
return short;
}
function longFlag(key: OptionId, option: Option) {
const inverse = isBooleanOption(option) && option.inverse;
return inverse ? `no-${kebabCase(key)}` : kebabCase(key);
}
function optionFlags(key: OptionId, option: Option) {
const base = `-${shortFlag(key, option)}, --${longFlag(key, option)}`;
if (isStringOption(option) || isStringArrayOption(option)) {
return `${base} <${key}>`;
}
return base;
}
export function getOptions<TOptions extends OptionSpecifier>(
command: program.Command,
options: TOptions,
argv: string[]
): MaybeOptionValues<TOptions> {
Object.entries(options)
.reduce((acc, [key, option]) => {
const flags = optionFlags(key, option);
if (isBooleanOption(option)) return acc.option(flags, option.description, !!option.inverse);
const checkStringValue = (raw: string) => {
if (!option.values.includes(raw))
throw new Error(`Unexpected value '${raw}' for option '${key}'`);
return raw;
};
if (isStringOption(option))
return acc.option(flags, option.description, (raw) => checkStringValue(raw));
if (isStringArrayOption(option)) {
return acc.option(
flags,
option.description,
(raw, values) => [...values, checkStringValue(raw)],
[]
);
}
throw new Error(`Unexpected option type '${key}'`);
}, command)
.parse(argv);
// Note the code above guarantees the types as they come in, so we cast here.
// Not sure there is an easier way to do this
return command.opts() as MaybeOptionValues<TOptions>;
}
export function areOptionsSatisfied<TOptions extends OptionSpecifier>(
options: TOptions,
values: MaybeOptionValues<TOptions>
) {
return !Object.entries(options)
.filter(([, option]) => isStringOption(option) && option.required)
.find(([key]) => !values[key]);
}
export async function promptOptions<TOptions extends OptionSpecifier>(
options: TOptions,
values: MaybeOptionValues<TOptions>
): Promise<OptionValues<TOptions>> {
const questions = Object.entries(options).map(([key, option]): PromptObject => {
let defaultType: PromptType = 'toggle';
if (!isBooleanOption(option))
defaultType = isStringArrayOption(option) ? 'autocompleteMultiselect' : 'select';
const passedType = option.promptType;
let type: PromptObject['type'] = defaultType;
// Allow returning `undefined` from `type()` function to fallback to default
if (typeof passedType === 'function') {
type = (...args: Parameters<typeof passedType>) => {
const chosenType = passedType(...args);
return chosenType === true ? defaultType : chosenType;
};
} else if (passedType) {
type = passedType;
}
if (!isBooleanOption(option)) {
const currentValue = values[key];
return {
type,
message: option.description,
name: key,
// warn: ' ',
// pageSize: Object.keys(tasks).length + Object.keys(groups).length,
choices: option.values.map((value) => ({
title: value,
value,
selected:
currentValue === value ||
(Array.isArray(currentValue) && currentValue.includes?.(value)),
})),
};
}
return {
type,
message: option.description,
name: key,
initial: option.inverse,
active: 'yes',
inactive: 'no',
};
});
const selection = await prompts(questions);
// Again the structure of the questions guarantees we get responses of the type we need
return selection as OptionValues<TOptions>;
}
function getFlag<TOption extends Option>(
key: OptionId,
option: TOption,
value?: OptionValue<TOption>
) {
if (isBooleanOption(option)) {
const toggled = option.inverse ? !value : value;
return toggled ? `--${longFlag(key, option)}` : '';
}
if (isStringArrayOption(option)) {
// I'm not sure why TS isn't able to infer that OptionValue<TOption> is a
// OptionValue<StringArrayOption> (i.e. a string[]), given that it knows
// option is a StringArrayOption
return ((value || []) as OptionValue<typeof option>)
.map((v) => `--${longFlag(key, option)} ${v}`)
.join(' ');
}
if (isStringOption(option)) {
if (value) {
return `--${longFlag(key, option)} ${value}`;
}
return '';
}
throw new Error(`Unknown option type for '${key}'`);
}
export function getCommand<TOptions extends OptionSpecifier>(
prefix: string,
options: TOptions,
values: Partial<OptionValues<TOptions>>
) {
const flags = Object.keys(options)
.map((key) => getFlag(key, options[key], values[key]))
.filter(Boolean);
return `${prefix} ${flags.join(' ')}`;
}
export async function getOptionsOrPrompt<TOptions extends OptionSpecifier>(
commandPrefix: string,
options: TOptions
): Promise<OptionValues<TOptions>> {
const main = program.version('5.0.0');
const cliValues = getOptions(main as any, options, process.argv);
if (areOptionsSatisfied(options, cliValues)) {
// areOptionsSatisfied could be a type predicate but I'm not quite sure how to do it
return cliValues as OptionValues<TOptions>;
}
const finalValues = await promptOptions(options, cliValues);
const command = getCommand(commandPrefix, options, finalValues);
console.log(`\nTo run this directly next time, use:\n ${command}\n`);
return finalValues;
}

View File

@ -3240,6 +3240,7 @@ __metadata:
"@cypress/webpack-preprocessor": ^5.9.1
"@emotion/babel-plugin": ^11.7.2
"@emotion/jest": ^11.8.0
"@jest/globals": ^26.6.2
"@linear/sdk": ^1.21.0
"@nicolo-ribaudo/chokidar-2": ^2.1.8
"@nrwl/cli": 12.3.4
@ -3269,6 +3270,7 @@ __metadata:
"@types/fs-extra": ^9.0.6
"@types/jest": ^26.0.16
"@types/js-yaml": ^3.12.6
"@types/lodash": ^4
"@types/node": ^14.14.20 || ^16.0.0
"@types/node-cleanup": ^2.1.1
"@types/node-fetch": ^2.5.7
@ -3327,6 +3329,7 @@ __metadata:
jest-watch-typeahead: ^0.6.1
js-yaml: ^3.14.1
lint-staged: ^10.5.4
lodash: ^4.17.21
mocha-list-tests: ^1.0.5
node-cleanup: ^2.1.2
node-fetch: ^2.6.1
@ -3814,6 +3817,13 @@ __metadata:
languageName: node
linkType: hard
"@types/lodash@npm:^4":
version: 4.14.182
resolution: "@types/lodash@npm:4.14.182"
checksum: d6bd4789dfb3be631d5e3277e6a1be5becb21440f3364f5d15b982c2e6b6bb1f8048d46fc5bff5ef0f90bebaf4d07c49b2919ba369d07af72d3beb3fea70c61a
languageName: node
linkType: hard
"@types/mdast@npm:^3.0.0":
version: 3.0.10
resolution: "@types/mdast@npm:3.0.10"