Skip to content

Commit

Permalink
Merge pull request #20054 from storybookjs/tom/sb-1040-allow-creating…
Browse files Browse the repository at this point in the history
…-templates-that-augment

Allow creating templates that extend others and pass `main.js` options
  • Loading branch information
yannbf authored Jan 12, 2023
2 parents 60a40e9 + 643d4f3 commit e465ca4
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 104 deletions.
210 changes: 119 additions & 91 deletions code/lib/cli/src/repro-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import boxen from 'boxen';
import { dedent } from 'ts-dedent';
import { downloadTemplate } from 'giget';

import { existsSync } from 'fs-extra';
import { existsSync, readdir } from 'fs-extra';
import type { Template, TemplateKey } from './repro-templates';
import { allTemplates as TEMPLATES } from './repro-templates';

const logger = console;
Expand All @@ -26,85 +27,89 @@ export const reproNext = async ({
branch,
init,
}: ReproOptions) => {
const filterRegex = new RegExp(`^${filterValue || ''}`, 'i');
// Either get a direct match when users pass a template id, or filter through all templates
let selectedConfig: Template | undefined = TEMPLATES[filterValue as TemplateKey];
let selectedTemplate: Choice | null = selectedConfig ? (filterValue as TemplateKey) : null;

const keys = Object.keys(TEMPLATES) as Choice[];
// get value from template and reduce through TEMPLATES to filter out the correct template
const choices = keys.reduce<Choice[]>((acc, group) => {
const current = TEMPLATES[group];
if (!selectedConfig) {
const filterRegex = new RegExp(`^${filterValue || ''}`, 'i');

const keys = Object.keys(TEMPLATES) as Choice[];
// get value from template and reduce through TEMPLATES to filter out the correct template
const choices = keys.reduce<Choice[]>((acc, group) => {
const current = TEMPLATES[group];

if (!filterValue) {
acc.push(group);
return acc;
}

if (
current.name.match(filterRegex) ||
group.match(filterRegex) ||
current.expected.builder.match(filterRegex) ||
current.expected.framework.match(filterRegex) ||
current.expected.renderer.match(filterRegex)
) {
acc.push(group);
return acc;
}

if (!filterValue) {
acc.push(group);
return acc;
}, []);

if (choices.length === 0) {
logger.info(
boxen(
dedent`
🔎 You filtered out all templates. 🔍
After filtering all the templates with "${chalk.yellow(
filterValue
)}", we found no results. Please try again with a different filter.
Available templates:
${keys.map((key) => chalk.blue`- ${key}`).join('\n')}
`.trim(),
{ borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any
)
);
process.exit(1);
}

if (
current.name.match(filterRegex) ||
group.match(filterRegex) ||
current.expected.builder.match(filterRegex) ||
current.expected.framework.match(filterRegex) ||
current.expected.renderer.match(filterRegex)
) {
acc.push(group);
return acc;
}
if (choices.length === 1) {
[selectedTemplate] = choices;
} else {
logger.info(
boxen(
dedent`
🤗 Welcome to ${chalk.yellow('sb repro NEXT')}! 🤗
return acc;
}, []);
Create a ${chalk.green('new project')} to minimally reproduce Storybook issues.
if (choices.length === 0) {
logger.info(
boxen(
dedent`
🔎 You filtered out all templates. 🔍
1. select an environment that most closely matches your project setup.
2. select a location for the reproduction, outside of your project.
After filtering all the templates with "${chalk.yellow(
filterValue
)}", we found no results. Please try again with a different filter.
Available templates:
${keys.map((key) => chalk.blue`- ${key}`).join('\n')}
`.trim(),
{ borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any
)
);
process.exit(1);
}

let selectedTemplate: Choice | null = null;

if (choices.length === 1) {
[selectedTemplate] = choices;
} else {
logger.info(
boxen(
dedent`
🤗 Welcome to ${chalk.yellow('sb repro NEXT')}! 🤗
Create a ${chalk.green('new project')} to minimally reproduce Storybook issues.
1. select an environment that most closely matches your project setup.
2. select a location for the reproduction, outside of your project.
After the reproduction is ready, we'll guide you through the next steps.
`.trim(),
{ borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any
)
);
After the reproduction is ready, we'll guide you through the next steps.
`.trim(),
{ borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any
)
);

selectedTemplate = await promptSelectedTemplate(choices);
}
selectedTemplate = await promptSelectedTemplate(choices);
}

const hasSelectedTemplate = !!(selectedTemplate ?? null);
if (!hasSelectedTemplate) {
logger.error('Somehow we got no templates. Please rerun this command!');
return;
}
const hasSelectedTemplate = !!(selectedTemplate ?? null);
if (!hasSelectedTemplate) {
logger.error('Somehow we got no templates. Please rerun this command!');
return;
}

const selectedConfig = TEMPLATES[selectedTemplate];
selectedConfig = TEMPLATES[selectedTemplate];

if (!selectedConfig) {
throw new Error('🚨 Repro: please specify a valid template type');
if (!selectedConfig) {
throw new Error('🚨 Repro: please specify a valid template type');
}
}

let selectedDirectory = outputDirectory;
Expand All @@ -114,16 +119,24 @@ export const reproNext = async ({
}

if (!selectedDirectory) {
const { directory } = await prompts({
type: 'text',
message: 'Enter the output directory',
name: 'directory',
initial: outputDirectoryName,
validate: async (directoryName) =>
existsSync(directoryName)
? `${directoryName} already exists. Please choose another name.`
: true,
});
const { directory } = await prompts(
{
type: 'text',
message: 'Enter the output directory',
name: 'directory',
initial: outputDirectoryName,
validate: async (directoryName) =>
existsSync(directoryName)
? `${directoryName} already exists. Please choose another name.`
: true,
},
{
onCancel: () => {
logger.log('Command cancelled by the user. Exiting...');
process.exit(1);
},
}
);
selectedDirectory = directory;
}

Expand All @@ -138,13 +151,20 @@ export const reproNext = async ({
try {
const templateType = init ? 'after-storybook' : 'before-storybook';
// Download the repro based on subfolder "after-storybook" and selected branch
await downloadTemplate(
`github:storybookjs/repro-templates-temp/${selectedTemplate}/${templateType}#${branch}`,
{
force: true,
dir: templateDestination,
}
);
const gitPath = `github:storybookjs/repro-templates-temp/${selectedTemplate}/${templateType}#${branch}`;
await downloadTemplate(gitPath, {
force: true,
dir: templateDestination,
});
// throw an error if templateDestination is an empty directory using fs-extra
if ((await readdir(templateDestination)).length === 0) {
throw new Error(
dedent`Template downloaded from ${chalk.blue(gitPath)} is empty.
Are you use it exists? Or did you want to set ${chalk.yellow(
selectedTemplate
)} to inDevelopment first?`
);
}
} catch (err) {
logger.error(`🚨 Failed to download repro template: ${err.message}`);
throw err;
Expand Down Expand Up @@ -180,12 +200,20 @@ export const reproNext = async ({
};

async function promptSelectedTemplate(choices: Choice[]): Promise<Choice | null> {
const { template } = await prompts({
type: 'select',
message: '🌈 Select the template',
name: 'template',
choices: choices.map(toChoices),
});
const { template } = await prompts(
{
type: 'select',
message: '🌈 Select the template',
name: 'template',
choices: choices.map(toChoices),
},
{
onCancel: () => {
logger.log('Command cancelled by the user. Exiting...');
process.exit(1);
},
}
);

return template || null;
}
60 changes: 58 additions & 2 deletions code/lib/cli/src/repro-templates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { StorybookConfig } from '@storybook/types';

export type SkippableTask = 'smoke-test' | 'test-runner' | 'chromatic' | 'e2e-tests';
export type TemplateKey = keyof typeof allTemplates;
export type TemplateKey = keyof typeof baseTemplates | keyof typeof internalTemplates;
export type Cadence = keyof typeof templatesByCadence;

export type Template = {
/**
* Readable name for the template, which will be used for feedback and the status page
Expand Down Expand Up @@ -41,9 +44,21 @@ export type Template = {
* NOTE: Make sure to always add a TODO comment to remove this flag in a subsequent PR.
*/
inDevelopment?: boolean;
/**
* Some sandboxes might need extra modifications in the initialized Storybook,
* such as extend main.js, for setting specific feature flags like storyStoreV7, etc.
*/
modifications?: {
mainConfig?: Partial<StorybookConfig>;
};
/**
* Flag to indicate that this template is a secondary template, which is used mainly to test rather specific features.
* This means the template might be hidden from the Storybook status page or the repro CLI command.
* */
isInternal?: boolean;
};

export const allTemplates = {
const baseTemplates = {
'cra/default-js': {
name: 'Create React App (Javascript)',
script: 'npx create-react-app .',
Expand Down Expand Up @@ -351,6 +366,45 @@ export const allTemplates = {
},
} satisfies Record<string, Template>;

/**
* Internal templates reuse config from other templates and add extra config on top.
* They must contain an id that starts with 'internal/' and contain "isInternal: true".
* They will be hidden by default in the Storybook status page.
*/
const internalTemplates = {
'internal/ssv6-vite': {
...baseTemplates['react-vite/default-ts'],
name: 'StoryStore v6 (react-vite/default-ts)',
inDevelopment: true,
isInternal: true,
modifications: {
mainConfig: {
features: {
storyStoreV7: false,
},
},
},
},
'internal/ssv6-webpack': {
...baseTemplates['react-webpack/18-ts'],
name: 'StoryStore v6 (react-webpack/18-ts)',
inDevelopment: true,
isInternal: true,
modifications: {
mainConfig: {
features: {
storyStoreV7: false,
},
},
},
},
} satisfies Record<`internal/${string}`, Template & { isInternal: true }>;

export const allTemplates: Record<TemplateKey, Template> = {
...baseTemplates,
...internalTemplates,
};

export const ci: TemplateKey[] = ['cra/default-ts', 'react-vite/default-ts'];
export const pr: TemplateKey[] = [
...ci,
Expand All @@ -371,6 +425,8 @@ export const merged: TemplateKey[] = [
'preact-webpack5/default-ts',
'preact-vite/default-ts',
'html-webpack/default',
'internal/ssv6-vite',
'internal/ssv6-webpack',
];
export const daily: TemplateKey[] = [
...merged,
Expand Down
1 change: 1 addition & 0 deletions scripts/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ async function run() {
const finalTask = tasks[taskKey];
const { template: templateKey } = optionValues;
const template = TEMPLATES[templateKey];

const templateSandboxDir = templateKey && join(sandboxDir, templateKey.replace('/', '-'));
const details = {
key: templateKey,
Expand Down
28 changes: 18 additions & 10 deletions scripts/tasks/sandbox-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export const create: Task['run'] = async (

export const install: Task['run'] = async ({ sandboxDir, template }, { link, dryRun, debug }) => {
const cwd = sandboxDir;

await installYarn2({ cwd, dryRun, debug });

if (link) {
Expand Down Expand Up @@ -110,15 +109,6 @@ export const install: Task['run'] = async ({ sandboxDir, template }, { link, dry
debug,
});

const mainConfig = await readMainConfig({ cwd });
// Enable or disable Storybook features
mainConfig.setFieldValue(['features'], {
interactionsDebugger: true,
});

if (template.expected.builder === '@storybook/builder-vite') setSandboxViteFinal(mainConfig);
await writeConfig(mainConfig);

logger.info(`🔢 Adding package scripts:`);
await updatePackageScripts({
cwd,
Expand Down Expand Up @@ -428,3 +418,21 @@ export const addStories: Task['run'] = async (

await writeConfig(mainConfig);
};

export const extendMain: Task['run'] = async ({ template, sandboxDir }) => {
logger.log('📝 Extending main.js');
const mainConfig = await readMainConfig({ cwd: sandboxDir });
const templateConfig = template.modifications?.mainConfig || {};
const configToAdd = {
...templateConfig,
features: {
interactionsDebugger: true,
...templateConfig.features,
},
};

Object.entries(configToAdd).forEach(([field, value]) => mainConfig.setFieldValue([field], value));

if (template.expected.builder === '@storybook/builder-vite') setSandboxViteFinal(mainConfig);
await writeConfig(mainConfig);
};
Loading

0 comments on commit e465ca4

Please sign in to comment.