Skip to content

Commit

Permalink
Merge pull request #18781 from storybookjs/tom/sb-568-script-asks-the…
Browse files Browse the repository at this point in the history
…-maintainer-which

Create a new `yarn example` command that will drive our new dev experience
  • Loading branch information
tmeasday committed Jul 31, 2022
2 parents 1bb8d80 + 93f3e0e commit d3aa364
Show file tree
Hide file tree
Showing 16 changed files with 777 additions and 22 deletions.
17 changes: 17 additions & 0 deletions .circleci/config.yml
Expand Up @@ -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:
Expand Down Expand Up @@ -462,6 +476,9 @@ workflows:
- unit-tests:
requires:
- build
- script-unit-tests:
requires:
- build
- coverage:
requires:
- unit-tests
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -16,4 +16,5 @@ junit.xml
!/**/.yarn/sdks
!/**/.yarn/versions
/**/.pnp.*
/yarn.lock
/yarn.lock
./examples/
2 changes: 1 addition & 1 deletion code/lib/blocks/package.json
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions code/lib/cli/src/generate.ts
Expand Up @@ -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);
})
Expand Down
9 changes: 6 additions & 3 deletions code/lib/cli/src/link.ts
Expand Up @@ -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'));
Expand Down Expand Up @@ -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 });
}
};
14 changes: 11 additions & 3 deletions code/lib/cli/src/repro-generators/scripts.ts
Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions code/package.json
Expand Up @@ -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",
Expand Down
11 changes: 1 addition & 10 deletions code/yarn.lock
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion examples/react
Submodule react deleted from b7ef5b
235 changes: 235 additions & 0 deletions scripts/example.ts
@@ -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 change: 1 addition & 0 deletions scripts/jest.config.js
@@ -0,0 +1 @@
module.exports = {};

0 comments on commit d3aa364

Please sign in to comment.