diff --git a/code/lib/builder-vite/src/list-stories.ts b/code/lib/builder-vite/src/list-stories.ts index 08be02004df1..de8c86d6ff62 100644 --- a/code/lib/builder-vite/src/list-stories.ts +++ b/code/lib/builder-vite/src/list-stories.ts @@ -13,7 +13,9 @@ export async function listStories(options: Options) { }).map(({ directory, files }) => { const pattern = path.join(directory, files); - return glob(path.isAbsolute(pattern) ? pattern : path.join(options.configDir, pattern)); + return glob(path.isAbsolute(pattern) ? pattern : path.join(options.configDir, pattern), { + follow: true, + }); }) ) ).reduce((carry, stories) => carry.concat(stories), []); diff --git a/code/renderers/react/template/stories/hooks.stories.tsx b/code/renderers/react/template/stories/hooks.stories.tsx new file mode 100644 index 000000000000..7d7388954eba --- /dev/null +++ b/code/renderers/react/template/stories/hooks.stories.tsx @@ -0,0 +1,16 @@ +import React, { FC, useState } from 'react'; + +const ButtonWithState: FC = () => { + const [count, setCount] = useState(0); + return ( + + ); +}; + +export default { + component: ButtonWithState, +}; + +export const Basic = {}; diff --git a/scripts/sandbox.ts b/scripts/sandbox.ts index bdedf10e15d1..697b1a721a99 100644 --- a/scripts/sandbox.ts +++ b/scripts/sandbox.ts @@ -25,6 +25,8 @@ import { ConfigFile, readConfig, writeConfig } from '../code/lib/csf-tools'; import { babelParse } from '../code/lib/csf-tools/src/babelParse'; import TEMPLATES from '../code/lib/cli/src/repro-templates'; import { servePackages } from './utils/serve-packages'; +import { filterExistsInCodeDir, codeDir } from './utils/filterExistsInCodeDir'; +import { JsPackageManagerFactory } from '../code/lib/cli/src/js-package-manager'; type Template = keyof typeof TEMPLATES; const templates: Template[] = Object.keys(TEMPLATES) as any; @@ -44,7 +46,6 @@ const defaultAddons = [ 'viewport', ]; const sandboxDir = path.resolve(__dirname, '../sandbox'); -const codeDir = path.resolve(__dirname, '../code'); const reprosDir = path.resolve(__dirname, '../repros'); export const options = createOptions({ @@ -221,7 +222,7 @@ function addEsbuildLoaderToStories(mainConfig: ConfigFile) { ...config.modules, rules: [ { - test: [/\\/code\\/[^/]*\\/[^/]*\\/template\\/stories\\//], + test: [/\\/template-stories\\//], loader: '${loaderPath}', options: { loader: 'tsx', @@ -263,43 +264,42 @@ function addPreviewAnnotations(mainConfig: ConfigFile, paths: string[]) { } // paths are of the form 'renderers/react', 'addons/actions' -async function addStories(paths: string[], { mainConfig }: { mainConfig: ConfigFile }) { - // Add `stories` entries of the form - // '../../../code/lib/store/template/stories/*.stories.@(js|jsx|ts|tsx)' +async function addStories( + packageDirs: string[], + { mainConfig, cwd }: { mainConfig: ConfigFile; cwd: string } +) { + // Link `stories` directories + // '../../../code/lib/store/template/stories' to 'src/templates/lib/store' // if the directory /lib/store/template/stories exists - const extraStoryDirsAndExistence = await Promise.all( - paths - .map((p) => path.join(p, 'template', 'stories')) - .map(async (p) => [p, await pathExists(path.resolve(codeDir, p))] as const) + // + // We link rather than reference relative dir to avoid Running two versions + // of React in react-based projects + await Promise.all( + packageDirs.map(async (p) => { + const source = path.join(codeDir, p, 'template', 'stories'); + await ensureSymlink(source, path.resolve(cwd, 'template-stories', p)); + }) ); const stories = mainConfig.getFieldValue(['stories']) as string[]; - const extraStories = extraStoryDirsAndExistence - .filter(([, exists]) => exists) - .map(([p]) => ({ - directory: path.join('..', '..', '..', 'code', p), - titlePrefix: p.split('/').slice(-4, -2).join('/'), - files: '**/*.stories.@(js|jsx|ts|tsx)', - })); - mainConfig.setFieldValue(['stories'], [...stories, ...extraStories]); + // FIXME: '*.@(mdx|stories.mdx|stories.tsx|stories.ts|stories.jsx|stories.js' + const linkedStories = path.join('..', 'template-stories', '**', '*.stories.@(js|jsx|ts|tsx|mdx)'); + mainConfig.setFieldValue(['stories'], [...stories, linkedStories]); // Add `config` entries of the form // '../../code/lib/store/template/stories/preview.ts' // if the file /lib/store/template/stories/preview.ts exists - const extraPreviewAndExistence = await Promise.all( - extraStoryDirsAndExistence - .filter(([, exists]) => exists) - .map(([storiesPath]) => path.join(storiesPath, 'preview.ts')) - .map( - async (previewPath) => - [previewPath, await pathExists(path.resolve(codeDir, previewPath))] as const - ) + const packageDirsWithPreview = await filterExistsInCodeDir( + packageDirs, + path.join('template', 'stories', 'preview.ts') ); - const extraConfig = extraPreviewAndExistence - .filter(([, exists]) => exists) - .map(([p]) => path.join('..', '..', 'code', p)); - addPreviewAnnotations(mainConfig, extraConfig); + const config = mainConfig.getFieldValue(['config']) as string[]; + const extraConfig = packageDirsWithPreview.map((p) => { + const previewFile = path.join('template-stories', p, 'preview.ts'); + return `./${previewFile}`; + }); + mainConfig.setFieldValue(['config'], [...(config || []), ...extraConfig]); } type Workspace = { name: string; location: string }; @@ -320,6 +320,23 @@ function workspacePath(type: string, packageName: string, workspaces: Workspace[ return workspace.location; } +function addExtraDependencies({ + cwd, + dryRun, + debug, +}: { + cwd: string; + dryRun: boolean; + debug: boolean; +}) { + const extraDeps = ['@storybook/jest']; + if (debug) console.log('🎁 Adding extra deps', extraDeps); + if (!dryRun) { + const packageManager = JsPackageManagerFactory.getPackageManager(false, cwd); + packageManager.addDependencies({ installAsDevDependencies: true }, extraDeps); + } +} + export async function sandbox(optionValues: OptionValues) { const { template, forceDelete, forceReuse, dryRun, debug, fromLocalRepro } = optionValues; @@ -405,7 +422,14 @@ export async function sandbox(optionValues: OptionValues) { for (const addon of [...defaultAddons, ...optionValues.addon]) { storiesToAdd.push(workspacePath('addon', `@storybook/addon-${addon}`, workspaces)); } - await addStories(storiesToAdd, { mainConfig }); + const existingStories = await filterExistsInCodeDir( + storiesToAdd, + path.join('template', 'stories') + ); + await addStories(existingStories, { + mainConfig, + cwd, + }); // Add some extra settings (see above for what these do) mainConfig.setFieldValue(['core', 'disableTelemetry'], true); @@ -451,6 +475,9 @@ export async function sandbox(optionValues: OptionValues) { ); } + // Some addon stories require extra dependencies + addExtraDependencies({ cwd, dryRun, debug }); + await addPackageScripts({ cwd, scripts: { diff --git a/scripts/utils/filterExistsInCodeDir.ts b/scripts/utils/filterExistsInCodeDir.ts new file mode 100644 index 000000000000..43204a227bb8 --- /dev/null +++ b/scripts/utils/filterExistsInCodeDir.ts @@ -0,0 +1,15 @@ +import path from 'path'; +import { pathExists } from 'fs-extra'; + +export const codeDir = path.resolve(__dirname, '../../code'); + +// packageDirs of the form `lib/store` +// paths to check of the form 'template/stories' +export const filterExistsInCodeDir = async (packageDirs: string[], pathToCheck: string) => + ( + await Promise.all( + packageDirs.map(async (p) => + (await pathExists(path.resolve(codeDir, path.join(p, pathToCheck)))) ? p : null + ) + ) + ).filter(Boolean);