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);