Merge pull request #24658 from storybookjs/norbert/remove-storystorev7

Core: Remove `storyStoreV7` feature flag
This commit is contained in:
Valentin Palkovic 2024-01-09 13:15:46 +01:00 committed by GitHub
commit 8404f7438c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 2389 additions and 4797 deletions

893
.yarn/releases/yarn-4.0.0.cjs generated vendored

File diff suppressed because one or more lines are too long

893
.yarn/releases/yarn-4.0.2.cjs generated vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -8,4 +8,4 @@ nodeLinker: node-modules
npmPublishAccess: public
yarnPath: .yarn/releases/yarn-4.0.0.cjs
yarnPath: .yarn/releases/yarn-4.0.2.cjs

View File

@ -2,6 +2,8 @@ compressionLevel: 0
enableGlobalCache: true
installStatePath: ../.yarn/code-install-state.gz
logFilters:
- code: YN0005
level: discard
@ -23,7 +25,6 @@ plugins:
unsafeHttpWhitelist:
- localhost
yarnPath: ../.yarn/releases/yarn-4.0.0.cjs
installStatePath: '../.yarn/code-install-state.gz'
yarnPath: ../.yarn/releases/yarn-4.0.2.cjs
# Sometimes you get a "The remote archive doesn't match the expected checksum" error, uncommenting this line will fix it
# checksumBehavior: 'update'

View File

@ -60,7 +60,7 @@
"@storybook/client-logger": "workspace:*",
"@storybook/components": "workspace:*",
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"@storybook/manager-api": "workspace:*",
"@storybook/preview-api": "workspace:*",
"@storybook/theming": "workspace:*",

View File

@ -53,7 +53,7 @@
},
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"memoizerific": "^1.11.3",
"ts-dedent": "^2.0.0"
},

View File

@ -176,4 +176,4 @@ Like [story parameters](https://storybook.js.org/docs/react/writing-stories/para
### How do controls work with MDX?
When importing stories from your CSF file into MDX, controls will work the same way. See [the documentation](https://storybook.js.org/docs/writing-docs/mdx#basic-example) for examples.
When importing stories from your CSF file into MDX, controls will work the same way. See [the documentation](https://storybook.js.org/docs/writing-docs/mdx#basic-example) for examples.

View File

@ -49,7 +49,7 @@
},
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"@storybook/types": "workspace:*",
"jest-mock": "^27.0.6",
"polished": "^4.2.2",

View File

@ -65,7 +65,7 @@
},
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"tiny-invariant": "^1.3.1"
},
"devDependencies": {

View File

@ -55,7 +55,7 @@
},
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"ts-dedent": "^2.0.0"
},
"devDependencies": {

View File

@ -59,7 +59,7 @@
"@storybook/client-logger": "workspace:*",
"@storybook/components": "workspace:*",
"@storybook/core-events": "workspace:*",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"@storybook/manager-api": "workspace:*",
"@storybook/preview-api": "workspace:*",
"@storybook/theming": "workspace:*",

View File

@ -55,7 +55,7 @@
"@storybook/components": "workspace:*",
"@storybook/core-events": "workspace:*",
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"@storybook/manager-api": "workspace:*",
"@storybook/preview-api": "workspace:*",
"@storybook/theming": "workspace:*",

View File

@ -1,48 +0,0 @@
import { loadPreviewOrConfigFile } from '@storybook/core-common';
import type { Options } from '@storybook/types';
import slash from 'slash';
import { listStories } from './list-stories';
const absoluteFilesToImport = async (
files: string[],
name: string,
normalizePath: (id: string) => string
) =>
files
.map((el, i) => `import ${name ? `* as ${name}_${i} from ` : ''}'/@fs/${normalizePath(el)}'`)
.join('\n');
export async function generateVirtualStoryEntryCode(options: Options) {
const { normalizePath } = await import('vite');
const storyEntries = await listStories(options);
const resolveMap = storyEntries.reduce<Record<string, string>>(
(prev, entry) => ({ ...prev, [entry]: entry.replace(slash(process.cwd()), '.') }),
{}
);
const modules = storyEntries.map((entry, i) => `${JSON.stringify(entry)}: story_${i}`).join(',');
return `
${await absoluteFilesToImport(storyEntries, 'story', normalizePath)}
function loadable(key) {
return {${modules}}[key];
}
Object.assign(loadable, {
keys: () => (${JSON.stringify(Object.keys(resolveMap))}),
resolve: (key) => (${JSON.stringify(resolveMap)}[key])
});
export function configStories(configure) {
configure(loadable, { hot: import.meta.hot }, false);
}
`.trim();
}
export async function generatePreviewEntryCode({ configDir }: Options) {
const previewFile = loadPreviewOrConfigFile({ configDir });
if (!previewFile) return '';
return `import * as preview from '${slash(previewFile)}';
export default preview;`;
}

View File

@ -1,116 +0,0 @@
import { getRendererName } from '@storybook/core-common';
import type { Options, PreviewAnnotation } from '@storybook/types';
import { virtualPreviewFile, virtualStoriesFile } from './virtual-file-names';
import { processPreviewAnnotation } from './utils/process-preview-annotation';
export async function generateIframeScriptCode(options: Options, projectRoot: string) {
const { presets } = options;
const rendererName = await getRendererName(options);
const previewAnnotations = await presets.apply<PreviewAnnotation[]>(
'previewAnnotations',
[],
options
);
const configEntries = [...previewAnnotations]
.filter(Boolean)
.map((path) => processPreviewAnnotation(path, projectRoot));
const filesToImport = (files: string[], name: string) =>
files.map((el, i) => `import ${name ? `* as ${name}_${i} from ` : ''}'${el}'`).join('\n');
const importArray = (name: string, length: number) =>
new Array(length).fill(0).map((_, i) => `${name}_${i}`);
// noinspection UnnecessaryLocalVariableJS
/** @todo Inline variable and remove `noinspection` */
// language=JavaScript
const code = `
// Ensure that the client API is initialized by the framework before any other iframe code
// is loaded. That way our client-apis can assume the existence of the API+store
import { configure } from '${rendererName}';
import { logger } from '@storybook/client-logger';
import * as previewApi from "@storybook/preview-api";
${filesToImport(configEntries, 'config')}
import * as preview from '${virtualPreviewFile}';
import { configStories } from '${virtualStoriesFile}';
const {
addDecorator,
addParameters,
addLoader,
addArgs,
addArgTypes,
addStepRunner,
addArgTypesEnhancer,
addArgsEnhancer,
setGlobalRender,
} = previewApi;
const configs = [${importArray('config', configEntries.length)
.concat('preview.default')
.join(',')}].filter(Boolean)
configs.map(config => config.default ? config.default : config).forEach(config => {
Object.keys(config).forEach((key) => {
const value = config[key];
switch (key) {
case 'args': {
return addArgs(value);
}
case 'argTypes': {
return addArgTypes(value);
}
case 'decorators': {
return value.forEach((decorator) => addDecorator(decorator, false));
}
case 'loaders': {
return value.forEach((loader) => addLoader(loader, false));
}
case 'parameters': {
return addParameters({ ...value }, false);
}
case 'argTypesEnhancers': {
return value.forEach((enhancer) => addArgTypesEnhancer(enhancer));
}
case 'argsEnhancers': {
return value.forEach((enhancer) => addArgsEnhancer(enhancer))
}
case 'render': {
return setGlobalRender(value)
}
case 'globals':
case 'globalTypes': {
const v = {};
v[key] = value;
return addParameters(v, false);
}
case 'decorateStory':
case 'applyDecorators':
case 'renderToDOM': // deprecated
case 'renderToCanvas': {
return null; // This key is not handled directly in v6 mode.
}
case 'runStep': {
return addStepRunner(value);
}
default: {
// eslint-disable-next-line prefer-template
return console.log(key + ' was not supported :( !');
}
}
});
})
/* TODO: not quite sure what to do with this, to fix HMR
if (import.meta.hot) {
import.meta.hot.accept();
}
*/
configStories(configure);
`.trim();
return code;
}

View File

@ -1,7 +1,6 @@
import * as path from 'path';
import type { Options } from '@storybook/types';
import { logger } from '@storybook/node-logger';
import { listStories } from './list-stories';
@ -28,11 +27,7 @@ function toImportPath(relativePath: string) {
async function toImportFn(stories: string[]) {
const { normalizePath } = await import('vite');
const objectEntries = stories.map((file) => {
const ext = path.extname(file);
const relativePath = normalizePath(path.relative(process.cwd(), file));
if (!['.js', '.jsx', '.ts', '.tsx', '.mdx', '.svelte', '.vue'].includes(ext)) {
logger.warn(`Cannot process ${ext} file with storyStoreV7: ${relativePath}`);
}
return ` '${toImportPath(relativePath)}': async () => import('/@fs/${file}')`;
});

View File

@ -69,7 +69,6 @@ export async function generateModernIframeScriptCode(options: Options, projectRo
window.__STORYBOOK_PREVIEW__ = window.__STORYBOOK_PREVIEW__ || new PreviewWeb();
window.__STORYBOOK_STORY_STORE__ = window.__STORYBOOK_STORY_STORE__ || window.__STORYBOOK_PREVIEW__.storyStore;
window.__STORYBOOK_CLIENT_API__ = window.__STORYBOOK_CLIENT_API__ || new ClientApi({ storyStore: window.__STORYBOOK_PREVIEW__.storyStore });
window.__STORYBOOK_PREVIEW__.initialize({ importFn, getProjectAnnotations });
${generateHMRHandler(frameworkName)};

View File

@ -4,10 +4,8 @@ import * as fs from 'fs';
import type { Plugin } from 'vite';
import type { Options } from '@storybook/types';
import { transformIframeHtml } from '../transform-iframe-html';
import { generateIframeScriptCode } from '../codegen-iframe-script';
import { generateModernIframeScriptCode } from '../codegen-modern-iframe-script';
import { generateImportFnScriptCode } from '../codegen-importfn-script';
import { generateVirtualStoryEntryCode, generatePreviewEntryCode } from '../codegen-entries';
import { generateAddonSetupCode } from '../codegen-set-addon-channel';
import {
@ -90,27 +88,16 @@ export function codeGeneratorPlugin(options: Options): Plugin {
return undefined;
},
async load(id, config) {
const storyStoreV7 = options.features?.storyStoreV7;
if (id === virtualStoriesFile) {
if (storyStoreV7) {
return generateImportFnScriptCode(options);
}
return generateVirtualStoryEntryCode(options);
return generateImportFnScriptCode(options);
}
if (id === virtualAddonSetupFile) {
return generateAddonSetupCode();
}
if (id === virtualPreviewFile && !storyStoreV7) {
return generatePreviewEntryCode(options);
}
if (id === virtualFileId) {
if (storyStoreV7) {
return generateModernIframeScriptCode(options, projectRoot);
}
return generateIframeScriptCode(options, projectRoot);
return generateModernIframeScriptCode(options, projectRoot);
}
if (id === iframeId) {

View File

@ -1,16 +1,14 @@
import type { Options, PreviewAnnotation } from '@storybook/types';
import { join, resolve } from 'path';
import {
getBuilderOptions,
getRendererName,
handlebars,
interpolate,
loadPreviewOrConfigFile,
normalizeStories,
readTemplate,
} from '@storybook/core-common';
import type { Options, PreviewAnnotation } from '@storybook/types';
import { isAbsolute, join, resolve } from 'path';
import slash from 'slash';
import { toImportFn, toRequireContextString } from '@storybook/core-webpack';
import { toImportFn } from '@storybook/core-webpack';
import type { BuilderOptions } from '../types';
export const getVirtualModules = async (options: Options) => {
@ -37,79 +35,31 @@ export const getVirtualModules = async (options: Options) => {
return entry.absolute;
}
// TODO: Remove as soon as we drop support for disabled StoryStoreV7
if (isAbsolute(entry)) {
return entry;
}
return slash(entry);
}
),
loadPreviewOrConfigFile(options),
].filter(Boolean);
if (options.features?.storyStoreV7) {
const storiesFilename = 'storybook-stories.js';
const storiesPath = resolve(join(workingDir, storiesFilename));
const storiesFilename = 'storybook-stories.js';
const storiesPath = resolve(join(workingDir, storiesFilename));
const needPipelinedImport = !!builderOptions.lazyCompilation && !isProd;
virtualModules[storiesPath] = toImportFn(stories, { needPipelinedImport });
const configEntryPath = resolve(join(workingDir, 'storybook-config-entry.js'));
virtualModules[configEntryPath] = handlebars(
await readTemplate(
require.resolve(
'@storybook/builder-webpack5/templates/virtualModuleModernEntry.js.handlebars'
)
),
{
storiesFilename,
previewAnnotations,
}
// We need to double escape `\` for webpack. We may have some in windows paths
).replace(/\\/g, '\\\\');
entries.push(configEntryPath);
} else {
const rendererName = await getRendererName(options);
const rendererInitEntry = resolve(join(workingDir, 'storybook-init-renderer-entry.js'));
virtualModules[rendererInitEntry] = `import '${slash(rendererName)}';`;
entries.push(rendererInitEntry);
const entryTemplate = await readTemplate(
require.resolve('@storybook/builder-webpack5/templates/virtualModuleEntry.template.js')
);
previewAnnotations.forEach((previewAnnotationFilename: string | undefined) => {
if (!previewAnnotationFilename) return;
// Ensure that relative paths end up mapped to a filename in the cwd, so a later import
// of the `previewAnnotationFilename` in the template works.
const entryFilename = previewAnnotationFilename.startsWith('.')
? `${previewAnnotationFilename.replace(/(\w)(\/|\\)/g, '$1-')}-generated-config-entry.js`
: `${previewAnnotationFilename}-generated-config-entry.js`;
// NOTE: although this file is also from the `dist/cjs` directory, it is actually a ESM
// file, see https://github.com/storybookjs/storybook/pull/16727#issuecomment-986485173
virtualModules[entryFilename] = interpolate(entryTemplate, {
previewAnnotationFilename,
});
entries.push(entryFilename);
});
if (stories.length > 0) {
const storyTemplate = await readTemplate(
require.resolve('@storybook/builder-webpack5/templates/virtualModuleStory.template.js')
);
// NOTE: this file has a `.cjs` extension as it is a CJS file (from `dist/cjs`) and runs
// in the user's webpack mode, which may be strict about the use of require/import.
// See https://github.com/storybookjs/storybook/issues/14877
const storiesFilename = resolve(join(workingDir, `generated-stories-entry.cjs`));
virtualModules[storiesFilename] = interpolate(storyTemplate, {
rendererName,
})
// Make sure we also replace quotes for this one
.replace("'{{stories}}'", stories.map(toRequireContextString).join(','));
entries.push(storiesFilename);
const needPipelinedImport = !!builderOptions.lazyCompilation && !isProd;
virtualModules[storiesPath] = toImportFn(stories, { needPipelinedImport });
const configEntryPath = resolve(join(workingDir, 'storybook-config-entry.js'));
virtualModules[configEntryPath] = handlebars(
await readTemplate(
require.resolve(
'@storybook/builder-webpack5/templates/virtualModuleModernEntry.js.handlebars'
)
),
{
storiesFilename,
previewAnnotations,
}
}
// We need to double escape `\` for webpack. We may have some in windows paths
).replace(/\\/g, '\\\\');
entries.push(configEntryPath);
return {
virtualModules,

View File

@ -1,65 +0,0 @@
/* eslint-disable import/no-unresolved */
import {
addDecorator,
addParameters,
addLoader,
addArgs,
addArgTypes,
addStepRunner,
addArgsEnhancer,
addArgTypesEnhancer,
setGlobalRender,
} from '@storybook/preview-api';
import * as previewAnnotations from '{{previewAnnotationFilename}}';
const config = previewAnnotations.default ?? previewAnnotations;
Object.keys(config).forEach((key) => {
const value = config[key];
switch (key) {
case 'args': {
return addArgs(value);
}
case 'argTypes': {
return addArgTypes(value);
}
case 'decorators': {
return value.forEach((decorator) => addDecorator(decorator, false));
}
case 'loaders': {
return value.forEach((loader) => addLoader(loader, false));
}
case 'parameters': {
return addParameters({ ...value }, false);
}
case 'argTypesEnhancers': {
return value.forEach((enhancer) => addArgTypesEnhancer(enhancer));
}
case 'argsEnhancers': {
return value.forEach((enhancer) => addArgsEnhancer(enhancer));
}
case 'render': {
return setGlobalRender(value);
}
case 'globals':
case 'globalTypes': {
const v = {};
v[key] = value;
return addParameters(v, false);
}
case '__namedExportsOrder':
case 'decorateStory':
case 'renderToDOM': // deprecated
case 'renderToCanvas': {
return null; // This key is not handled directly in v6 mode.
}
case 'runStep': {
return addStepRunner(value);
}
default: {
return console.log(
`Unknown key '${key}' exported by preview annotation file '{{previewAnnotationFilename}}'`
);
}
}
});

View File

@ -20,7 +20,6 @@ const preview = new PreviewWeb();
window.__STORYBOOK_PREVIEW__ = preview;
window.__STORYBOOK_STORY_STORE__ = preview.storyStore;
window.__STORYBOOK_ADDONS_CHANNEL__ = channel;
window.__STORYBOOK_CLIENT_API__ = new ClientApi({ storyStore: preview.storyStore });
preview.initialize({ importFn, getProjectAnnotations });

View File

@ -1,3 +0,0 @@
const { configure } = require('{{rendererName}}');
configure(['{{stories}}'], module, false);

View File

@ -48,17 +48,6 @@ test.describe('addon-backgrounds', () => {
});
test('button should appear for unattached .mdx files', async ({ page }) => {
// SSv6 does not support .mdx files. There is a unattached stories.mdx file
// at /docs/addons-docs-stories-mdx-unattached--docs, but these are functionally
// really attached
// eslint-disable-next-line jest/no-disabled-tests
test.skip(
// eslint-disable-next-line jest/valid-title
templateName.includes('ssv6'),
'Only run this test for Sandboxes with StoryStoreV7 enabled'
);
const sbPage = new SbPage(page);
// We start on the introduction page by default.

View File

@ -1,9 +1,7 @@
/* eslint-disable jest/no-disabled-tests */
import { test, expect } from '@playwright/test';
import process from 'process';
const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001';
const templateName = process.env.STORYBOOK_TEMPLATE_NAME || '';
test.describe('JSON files', () => {
test.beforeEach(async ({ page }) => {
@ -11,11 +9,6 @@ test.describe('JSON files', () => {
});
test('should have index.json', async ({ page }) => {
test.skip(
// eslint-disable-next-line jest/valid-title
templateName.includes('ssv6'),
'Only run this test for Sandboxes with StoryStoreV7 enabled'
);
const json = await page.evaluate(() => fetch('/index.json').then((res) => res.json()));
expect(json).toEqual({

View File

@ -2,9 +2,6 @@
import './globals';
// eslint-disable-next-line import/export
export * from './public-api';
// eslint-disable-next-line import/export
export * from './public-types';
export type { StoryFnAngularReturnType as IStory } from './types';

View File

@ -1 +0,0 @@
export * from './public-types';

View File

@ -6,7 +6,6 @@ declare var NODE_ENV: string | undefined;
declare var __STORYBOOK_ADDONS_CHANNEL__: any;
declare var __STORYBOOK_ADDONS_PREVIEW: any;
declare var __STORYBOOK_COMPODOC_JSON__: any;
declare var __STORYBOOK_CLIENT_API__: any;
declare var __STORYBOOK_PREVIEW__: any;
declare var __STORYBOOK_STORY_STORE__: any;
declare var CHANNEL_OPTIONS: any;

View File

@ -1 +0,0 @@
import './globals';

View File

@ -24,7 +24,7 @@ function Component() {
name: 'Prefetch',
},
{
// @ts-expect-error (a legacy nextjs api?)
// @ts-expect-error (old API)
cb: () => router.push('/push-html', { forceOptimisticNavigation: true }),
name: 'Push HTML',
},
@ -33,7 +33,7 @@ function Component() {
name: 'Refresh',
},
{
// @ts-expect-error (a legacy nextjs api?)
// @ts-expect-error (old API)
cb: () => router.replace('/replaced-html', { forceOptimisticNavigation: true }),
name: 'Replace',
},

View File

@ -66,7 +66,7 @@ export type Template = {
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.
* such as extend main.js, for setting specific feature flags.
*/
modifications?: {
skipTemplateStories?: boolean;

View File

@ -29,9 +29,6 @@ const config: StorybookConfig = {
disableTelemetry: true,
},
logLevel: 'debug',
features: {
storyStoreV7: false,
},
framework: {
name: '@storybook/react-webpack5',
options: {

View File

@ -2,7 +2,7 @@ import chalk from 'chalk';
import { copy, emptyDir, ensureDir } from 'fs-extra';
import { dirname, join, relative, resolve } from 'path';
import { global } from '@storybook/global';
import { deprecate, logger } from '@storybook/node-logger';
import { logger } from '@storybook/node-logger';
import { getPrecedingUpgrade, telemetry } from '@storybook/telemetry';
import type { BuilderOptions, CLIOptions, LoadOptions, Options } from '@storybook/types';
import {
@ -13,7 +13,6 @@ import {
resolveAddonName,
} from '@storybook/core-common';
import dedent from 'ts-dedent';
import { outputStats } from './utils/output-stats';
import { copyAllStaticFilesRelativeToMain } from './utils/copy-all-static-files';
import { getBuilders } from './utils/get-builders';
@ -103,13 +102,6 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption
presets.apply('docs', {}),
]);
if (features?.storyStoreV7 === false) {
deprecate(
dedent`storyStoreV6 is deprecated, please migrate to storyStoreV7 instead.
- Refer to the migration guide at https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#storystorev6-and-storiesof-is-deprecated`
);
}
const fullOptions: Options = {
...options,
presets,
@ -139,7 +131,7 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption
let initializedStoryIndexGenerator: Promise<StoryIndexGenerator | undefined> =
Promise.resolve(undefined);
if ((features?.buildStoriesJson || features?.storyStoreV7) && !options.ignorePreview) {
if (!options.ignorePreview) {
const workingDir = process.cwd();
const directories = {
configDir: options.configDir,
@ -150,7 +142,6 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption
...directories,
indexers,
docs: docsOptions,
storyStoreV7: !!features?.storyStoreV7,
build,
});

View File

@ -5,9 +5,8 @@ import invariant from 'tiny-invariant';
import type { Options } from '@storybook/types';
import { logConfig } from '@storybook/core-common';
import { deprecate, logger } from '@storybook/node-logger';
import { logger } from '@storybook/node-logger';
import dedent from 'ts-dedent';
import { MissingBuilderError } from '@storybook/core-events/server-errors';
import { getMiddleware } from './utils/middleware';
import { getServerAddresses } from './utils/server-address';
@ -38,13 +37,6 @@ export async function storybookDevServer(options: Options) {
getServerChannel(server)
);
if (features?.storyStoreV7 === false) {
deprecate(
dedent`storyStoreV6 is deprecated, please migrate to storyStoreV7 instead.
- Refer to the migration guide at https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#storystorev6-and-storiesof-is-deprecated`
);
}
let indexError: Error | undefined;
// try get index generator, if failed, send telemetry without storyCount, then rethrow the error
const initializedStoryIndexGenerator: Promise<StoryIndexGenerator | undefined> =

View File

@ -187,8 +187,6 @@ export const previewAnnotations = async (base: any, options: Options) => {
export const features: PresetProperty<'features'> = async (existing) => ({
...existing,
buildStoriesJson: false,
storyStoreV7: true,
argTypeTargetsV7: true,
legacyDecoratorFileOrder: false,
disallowImplicitActionsInRenderV8: true,

View File

@ -43,7 +43,6 @@ const options: StoryIndexGeneratorOptions = {
configDir: path.join(__dirname, '__mockdata__'),
workingDir: path.join(__dirname, '__mockdata__'),
indexers: [csfIndexer],
storyStoreV7: true,
docs: { defaultName: 'docs', autodocs: false },
};

View File

@ -49,7 +49,6 @@ type SpecifierStoriesCache = Record<Path, CacheEntry>;
export type StoryIndexGeneratorOptions = {
workingDir: Path;
configDir: Path;
storyStoreV7: boolean;
indexers: Indexer[];
docs: DocsOptions;
build?: StorybookConfigRaw['build'];
@ -346,11 +345,6 @@ export class StoryIndexGenerator {
async extractDocs(specifier: NormalizedStoriesSpecifier, absolutePath: Path) {
const relativePath = path.relative(this.options.workingDir, absolutePath);
try {
invariant(
this.options.storyStoreV7,
`You cannot use \`.mdx\` files without using \`storyStoreV7\`.`
);
const normalizedPath = normalizeStoryPath(relativePath);
const importPath = slash(normalizedPath);
@ -530,13 +524,9 @@ export class StoryIndexGenerator {
async sortStories(entries: StoryIndex['entries']) {
const sortableStories = Object.values(entries);
// Skip sorting if we're in v6 mode because we don't have
// all the info we need here
if (this.options.storyStoreV7) {
const storySortParameter = await this.getStorySortParameter();
const fileNameOrder = this.storyFileNames();
sortStoriesV7(sortableStories, storySortParameter, fileNameOrder);
}
const storySortParameter = await this.getStorySortParameter();
const fileNameOrder = this.storyFileNames();
sortStoriesV7(sortableStories, storySortParameter, fileNameOrder);
return sortableStories.reduce((acc, item) => {
acc[item.id] = item;

View File

@ -16,7 +16,6 @@ const options: StoryIndexGeneratorOptions = {
configDir: path.join(__dirname, '..', '__mockdata__'),
workingDir: path.join(__dirname, '..', '__mockdata__'),
indexers: [],
storyStoreV7: true,
docs: { defaultName: 'docs', autodocs: false },
};

View File

@ -7,16 +7,11 @@ import { router } from './router';
export async function getStoryIndexGenerator(
features: {
buildStoriesJson?: boolean;
storyStoreV7?: boolean;
argTypeTargetsV7?: boolean;
},
options: Options,
serverChannel: ServerChannel
): Promise<StoryIndexGenerator | undefined> {
if (!features?.buildStoriesJson && !features?.storyStoreV7) {
return undefined;
}
const workingDir = process.cwd();
const directories = {
configDir: options.configDir,
@ -32,7 +27,6 @@ export async function getStoryIndexGenerator(
indexers: await indexers,
docs: await docsOptions,
workingDir,
storyStoreV7: features.storyStoreV7 ?? false,
});
const initializedStoryIndexGenerator = generator.initialize().then(() => generator);

View File

@ -45,7 +45,6 @@ const getInitializedStoryIndexGenerator = async (
indexers: [csfIndexer],
configDir: workingDir,
workingDir,
storyStoreV7: true,
docs: { defaultName: 'docs', autodocs: false },
...overrides,
};
@ -252,35 +251,6 @@ describe('useStoriesJson', () => {
`);
}, 20_000);
it('disallows .mdx files without storyStoreV7', async () => {
const mockServerChannel = { emit: vi.fn() } as any as ServerChannel;
useStoriesJson({
router,
initializedStoryIndexGenerator: getInitializedStoryIndexGenerator({
storyStoreV7: false,
}),
workingDir,
serverChannel: mockServerChannel,
normalizedStories,
});
expect(use).toHaveBeenCalledTimes(1);
const route = use.mock.calls[0][1];
await route(request, response);
expect(send).toHaveBeenCalledTimes(1);
expect(send.mock.calls[0][0]).toMatchInlineSnapshot(`
"Unable to index files:
- ./src/docs2/ComponentReference.mdx: Invariant failed: You cannot use \`.mdx\` files without using \`storyStoreV7\`.
- ./src/docs2/MetaOf.mdx: Invariant failed: You cannot use \`.mdx\` files without using \`storyStoreV7\`.
- ./src/docs2/NoTitle.mdx: Invariant failed: You cannot use \`.mdx\` files without using \`storyStoreV7\`.
- ./src/docs2/SecondMetaOf.mdx: Invariant failed: You cannot use \`.mdx\` files without using \`storyStoreV7\`.
- ./src/docs2/Template.mdx: Invariant failed: You cannot use \`.mdx\` files without using \`storyStoreV7\`.
- ./src/docs2/Title.mdx: Invariant failed: You cannot use \`.mdx\` files without using \`storyStoreV7\`."
`);
});
it('can handle simultaneous access', async () => {
const mockServerChannel = { emit: vi.fn() } as any as ServerChannel;

View File

@ -455,8 +455,7 @@ export class CsfFile {
throw new Error(dedent`
Unexpected \`storiesOf\` usage: ${formatLocation(node, self._fileName)}.
In SB7, we use the next-generation \`storyStoreV7\` by default, which does not support \`storiesOf\`.
More info, with details about how to opt-out here: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#storystorev7-enabled-by-default
SB8 does not support \`storiesOf\`.
`);
}
},

View File

@ -58,7 +58,7 @@ import {
import type { ComposedRef } from '../index';
import type { ModuleFn } from '../lib/types';
const { FEATURES, fetch } = global;
const { fetch } = global;
const STORY_INDEX_PATH = './index.json';
type Direction = -1 | 1;
@ -881,10 +881,8 @@ export const init: ModuleFn<SubAPI, SubState> = ({
filters: config?.sidebar?.filters || {},
},
init: async () => {
if (FEATURES?.storyStoreV7) {
provider.channel?.on(STORY_INDEX_INVALIDATED, () => api.fetchIndex());
await api.fetchIndex();
}
provider.channel?.on(STORY_INDEX_INVALIDATED, () => api.fetchIndex());
await api.fetchIndex();
},
};
};

View File

@ -39,7 +39,6 @@ vi.mock('@storybook/global', () => ({
global: {
...globalThis,
fetch: vi.fn(() => ({ json: () => ({ v: 4, entries: mockGetEntries() }) })),
FEATURES: { storyStoreV7: true },
CONFIG_TYPE: 'DEVELOPMENT',
},
}));

View File

@ -10,34 +10,14 @@ The preview's job is:
3. Render the current selection to the web view in either story or docs mode.
## V7 Store vs Legacy (V6)
The story store is designed to load stories 'on demand', and will operate in this fashion if the `storyStoreV7` feature is enabled.
However, for back-compat reasons, in v6 mode, we need to load all stories, synchronously on bootup, emitting the `SET_STORIES` event.
In V7 mode we do not emit that event, instead preferring the `STORY_PREPARED` event, with the data for the single story being rendered.
## Initialization
The preview is `initialized` in two ways.
### V7 Mode:
- `importFn` - is an async `import()` function
- `getProjectAnnotations` - is a simple function that evaluations `preview.js` and addon config files and combines them. If it errors, the Preview will show the error.
- No `getStoryIndex` function is passed, instead the preview creates a `StoryIndexClient` that pulls `stories.json` from node and watches the event stream for invalidation events.
### V6 Mode
- `importFn` - is a simulated `import()` function, that is synchronous, see `client-api` for details.
- `getProjectAnnotations` - also evaluates `preview.js` et al, but watches for calls to `setStories`, and passes them to the `ClientApi`
- `getStoryIndex` is a local function (that must be called _after_ `getProjectAnnotations`) that gets the list of stories added.
See `client-api` for more details on this process.
## Story Rendering and interruptions
The Preview is split into three parts responsible for state management:

View File

@ -54,7 +54,6 @@
"lodash": "^4.17.21",
"memoizerific": "^1.11.3",
"qs": "^6.10.0",
"synchronous-promise": "^2.0.15",
"ts-dedent": "^2.0.0",
"util-deprecate": "^1.0.2"
},

View File

@ -1,4 +0,0 @@
/* eslint-disable @typescript-eslint/triple-slash-reference */
/// <reference path="typings.d.ts" />
export * from './modules/client-api';

View File

@ -1,4 +0,0 @@
/* eslint-disable @typescript-eslint/triple-slash-reference */
/// <reference path="typings.d.ts" />
export * from './modules/core-client';

View File

@ -41,22 +41,6 @@ export { DocsContext } from './preview-web';
*/
export { simulatePageLoad, simulateDOMContentLoaded } from './preview-web';
/**
* STORIES API
*/
export {
addArgTypes,
addArgTypesEnhancer,
addArgs,
addArgsEnhancer,
addDecorator,
addLoader,
addParameters,
addStepRunner,
} from './client-api';
export { getQueryParam, getQueryParams } from './client-api';
export { setGlobalRender } from './client-api';
export {
combineArgs,
combineParameters,
@ -83,8 +67,5 @@ export type { PropDescriptor } from './store';
/**
* STORIES API
*/
export { ClientApi } from './client-api';
export { StoryStore } from './store';
export { Preview, PreviewWeb, PreviewWithSelection, UrlStore, WebView } from './preview-web';
export type { SelectionStore, View } from './preview-web';
export { start } from './core-client';
export { Preview, PreviewWeb } from './preview-web';

View File

@ -1,214 +0,0 @@
/* eslint-disable no-underscore-dangle */
import { dedent } from 'ts-dedent';
import { global } from '@storybook/global';
import type {
Args,
StepRunner,
ArgTypes,
Renderer,
DecoratorFunction,
Parameters,
ArgTypesEnhancer,
ArgsEnhancer,
LoaderFunction,
Globals,
GlobalTypes,
Path,
ModuleImportFn,
ModuleExports,
} from '@storybook/types';
import type { StoryStore } from '../../store';
import { combineParameters, composeStepRunners, normalizeInputTypes } from '../../store';
import { StoryStoreFacade } from './StoryStoreFacade';
const warningAlternatives = {
addDecorator: `Instead, use \`export const decorators = [];\` in your \`preview.js\`.`,
addParameters: `Instead, use \`export const parameters = {};\` in your \`preview.js\`.`,
addLoader: `Instead, use \`export const loaders = [];\` in your \`preview.js\`.`,
addArgs: '',
addArgTypes: '',
addArgsEnhancer: '',
addArgTypesEnhancer: '',
addStepRunner: '',
getGlobalRender: '',
setGlobalRender: '',
};
const checkMethod = (method: keyof typeof warningAlternatives) => {
if (global.FEATURES?.storyStoreV7) {
throw new Error(
dedent`You cannot use \`${method}\` with the new Story Store.
${warningAlternatives[method]}`
);
}
if (!global.__STORYBOOK_CLIENT_API__) {
throw new Error(`Singleton client API not yet initialized, cannot call \`${method}\`.`);
}
};
export const addDecorator = (decorator: DecoratorFunction<Renderer>) => {
checkMethod('addDecorator');
global.__STORYBOOK_CLIENT_API__?.addDecorator(decorator);
};
export const addParameters = (parameters: Parameters) => {
checkMethod('addParameters');
global.__STORYBOOK_CLIENT_API__?.addParameters(parameters);
};
export const addLoader = (loader: LoaderFunction<Renderer>) => {
checkMethod('addLoader');
global.__STORYBOOK_CLIENT_API__?.addLoader(loader);
};
export const addArgs = (args: Args) => {
checkMethod('addArgs');
global.__STORYBOOK_CLIENT_API__?.addArgs(args);
};
export const addArgTypes = (argTypes: ArgTypes) => {
checkMethod('addArgTypes');
global.__STORYBOOK_CLIENT_API__?.addArgTypes(argTypes);
};
export const addArgsEnhancer = (enhancer: ArgsEnhancer<Renderer>) => {
checkMethod('addArgsEnhancer');
global.__STORYBOOK_CLIENT_API__?.addArgsEnhancer(enhancer);
};
export const addArgTypesEnhancer = (enhancer: ArgTypesEnhancer<Renderer>) => {
checkMethod('addArgTypesEnhancer');
global.__STORYBOOK_CLIENT_API__?.addArgTypesEnhancer(enhancer);
};
export const addStepRunner = (stepRunner: StepRunner) => {
checkMethod('addStepRunner');
global.__STORYBOOK_CLIENT_API__?.addStepRunner(stepRunner);
};
export const getGlobalRender = () => {
checkMethod('getGlobalRender');
return global.__STORYBOOK_CLIENT_API__?.facade.projectAnnotations.render;
};
export const setGlobalRender = (render: StoryStoreFacade<any>['projectAnnotations']['render']) => {
checkMethod('setGlobalRender');
if (global.__STORYBOOK_CLIENT_API__) {
global.__STORYBOOK_CLIENT_API__.facade.projectAnnotations.render = render;
}
};
export class ClientApi<TRenderer extends Renderer> {
facade: StoryStoreFacade<TRenderer>;
storyStore?: StoryStore<TRenderer>;
onImportFnChanged?: ({ importFn }: { importFn: ModuleImportFn }) => void;
// If we don't get passed modules so don't know filenames, we can
// just use numeric indexes
constructor({ storyStore }: { storyStore?: StoryStore<TRenderer> } = {}) {
this.facade = new StoryStoreFacade();
this.storyStore = storyStore;
}
importFn(path: Path) {
return this.facade.importFn(path);
}
getStoryIndex() {
if (!this.storyStore) {
throw new Error('Cannot get story index before setting storyStore');
}
return this.facade.getStoryIndex(this.storyStore);
}
addDecorator = (decorator: DecoratorFunction<TRenderer>) => {
this.facade.projectAnnotations.decorators?.push(decorator);
};
addParameters = ({
globals,
globalTypes,
...parameters
}: Parameters & { globals?: Globals; globalTypes?: GlobalTypes }) => {
this.facade.projectAnnotations.parameters = combineParameters(
this.facade.projectAnnotations.parameters,
parameters
);
if (globals) {
this.facade.projectAnnotations.globals = {
...this.facade.projectAnnotations.globals,
...globals,
};
}
if (globalTypes) {
this.facade.projectAnnotations.globalTypes = {
...this.facade.projectAnnotations.globalTypes,
...normalizeInputTypes(globalTypes),
};
}
};
addStepRunner = (stepRunner: StepRunner<TRenderer>) => {
this.facade.projectAnnotations.runStep = composeStepRunners(
[this.facade.projectAnnotations.runStep, stepRunner].filter(
Boolean
) as StepRunner<TRenderer>[]
);
};
addLoader = (loader: LoaderFunction<TRenderer>) => {
this.facade.projectAnnotations.loaders?.push(loader);
};
addArgs = (args: Args) => {
this.facade.projectAnnotations.args = {
...this.facade.projectAnnotations.args,
...args,
};
};
addArgTypes = (argTypes: ArgTypes) => {
this.facade.projectAnnotations.argTypes = {
...this.facade.projectAnnotations.argTypes,
...normalizeInputTypes(argTypes),
};
};
addArgsEnhancer = (enhancer: ArgsEnhancer<TRenderer>) => {
this.facade.projectAnnotations.argsEnhancers?.push(enhancer);
};
addArgTypesEnhancer = (enhancer: ArgTypesEnhancer<TRenderer>) => {
this.facade.projectAnnotations.argTypesEnhancers?.push(enhancer);
};
// Because of the API of `storiesOf().add()` we don't have a good "end" call for a
// storiesOf file to finish adding stories, and us to load it into the facade as a
// single psuedo-CSF file. So instead we just keep collecting the CSF files and load
// them all into the facade at the end.
_addedExports = {} as Record<Path, ModuleExports>;
_loadAddedExports() {
Object.entries(this._addedExports).forEach(([fileName, fileExports]) =>
this.facade.addStoriesFromExports(fileName, fileExports)
);
}
// @deprecated
raw = () => {
return this.storyStore?.raw();
};
// @deprecated
get _storyStore() {
return this.storyStore;
}
}

View File

@ -1,256 +0,0 @@
/* eslint-disable no-underscore-dangle */
import { global } from '@storybook/global';
import { dedent } from 'ts-dedent';
import { SynchronousPromise } from 'synchronous-promise';
import { toId, isExportStory, storyNameFromExport } from '@storybook/csf';
import type {
IndexEntry,
Renderer,
ComponentId,
DocsOptions,
Parameters,
Path,
ModuleExports,
NormalizedProjectAnnotations,
NormalizedStoriesSpecifier,
PreparedStory,
StoryIndex,
StoryId,
} from '@storybook/types';
import { logger } from '@storybook/client-logger';
import type { StoryStore } from '../../store';
import { userOrAutoTitle, sortStoriesV6 } from '../../store';
export const AUTODOCS_TAG = 'autodocs';
export const STORIES_MDX_TAG = 'stories-mdx';
export class StoryStoreFacade<TRenderer extends Renderer> {
projectAnnotations: NormalizedProjectAnnotations<TRenderer>;
entries: Record<StoryId, IndexEntry & { componentId?: ComponentId }>;
csfExports: Record<Path, ModuleExports>;
constructor() {
this.projectAnnotations = {
loaders: [],
decorators: [],
parameters: {},
argsEnhancers: [],
argTypesEnhancers: [],
args: {},
argTypes: {},
};
this.entries = {};
this.csfExports = {};
}
// This doesn't actually import anything because the client-api loads fully
// on startup, but this is a shim after all.
importFn(path: Path) {
return SynchronousPromise.resolve().then(() => {
const moduleExports = this.csfExports[path];
if (!moduleExports) throw new Error(`Unknown path: ${path}`);
return moduleExports;
});
}
getStoryIndex(store: StoryStore<TRenderer>) {
const fileNameOrder = Object.keys(this.csfExports);
const storySortParameter = this.projectAnnotations.parameters?.options?.storySort;
const storyEntries = Object.entries(this.entries);
// Add the kind parameters and global parameters to each entry
const sortableV6 = storyEntries.map(([storyId, { type, importPath, ...entry }]) => {
const exports = this.csfExports[importPath];
const csfFile = store.processCSFFileWithCache<TRenderer>(
exports,
importPath,
exports.default.title
);
let storyLike: PreparedStory<TRenderer>;
if (type === 'story') {
storyLike = store.storyFromCSFFile({ storyId, csfFile });
} else {
storyLike = {
...entry,
story: entry.name,
kind: entry.title,
componentId: toId(entry.componentId || entry.title),
parameters: { fileName: importPath },
} as any;
}
return [
storyId,
storyLike,
csfFile.meta.parameters,
this.projectAnnotations.parameters || {},
] as [StoryId, PreparedStory<TRenderer>, Parameters, Parameters];
});
// NOTE: the sortStoriesV6 version returns the v7 data format. confusing but more convenient!
let sortedV7: IndexEntry[];
try {
sortedV7 = sortStoriesV6(sortableV6, storySortParameter, fileNameOrder);
} catch (err: any) {
if (typeof storySortParameter === 'function') {
throw new Error(dedent`
Error sorting stories with sort parameter ${storySortParameter}:
> ${err.message}
Are you using a V7-style sort function in V6 compatibility mode?
More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#v7-style-story-sort
`);
}
throw err;
}
const entries = sortedV7.reduce((acc, s) => {
// We use the original entry we stored in `this.stories` because it is possible that the CSF file itself
// exports a `parameters.fileName` which can be different and mess up our `importFn`.
// NOTE: this doesn't actually change the story object, just the index.
acc[s.id] = this.entries[s.id];
return acc;
}, {} as StoryIndex['entries']);
return { v: 4, entries };
}
clearFilenameExports(fileName: Path) {
if (!this.csfExports[fileName]) {
return;
}
// Clear this module's stories from the storyList and existing exports
Object.entries(this.entries).forEach(([id, { importPath }]) => {
if (importPath === fileName) {
delete this.entries[id];
}
});
// We keep this as an empty record so we can use it to maintain component order
this.csfExports[fileName] = {};
}
// NOTE: we could potentially share some of this code with the stories.json generation
addStoriesFromExports(fileName: Path, fileExports: ModuleExports) {
if (fileName.match(/\.mdx$/) && !fileName.match(/\.stories\.mdx$/)) {
if (global.FEATURES?.storyStoreV7MdxErrors !== false) {
throw new Error(dedent`
Cannot index \`.mdx\` file (\`${fileName}\`) in \`storyStoreV7: false\` mode.
The legacy story store does not support new-style \`.mdx\` files. If the file above
is not intended to be indexed (i.e. displayed as an entry in the sidebar), either
exclude it from your \`stories\` glob, or add <Meta isTemplate /> to it.
If you wanted to index the file, you'll need to name it \`stories.mdx\` and stick to the
legacy (6.x) MDX API, or use the new store.`);
}
}
// if the export haven't changed since last time we added them, this is a no-op
if (this.csfExports[fileName] === fileExports) {
return;
}
// OTOH, if they have changed, let's clear them out first
this.clearFilenameExports(fileName);
// eslint-disable-next-line @typescript-eslint/naming-convention
const { default: defaultExport, __namedExportsOrder, ...namedExports } = fileExports;
// eslint-disable-next-line prefer-const
let { id: componentId, title, tags: componentTags = [] } = defaultExport || {};
const specifiers = (global.STORIES || []).map(
(specifier: NormalizedStoriesSpecifier & { importPathMatcher: string }) => ({
...specifier,
importPathMatcher: new RegExp(specifier.importPathMatcher),
})
);
title = userOrAutoTitle(fileName, specifiers, title);
if (!title) {
logger.info(
`Unexpected default export without title in '${fileName}': ${JSON.stringify(
fileExports.default
)}`
);
return;
}
this.csfExports[fileName] = {
...fileExports,
default: { ...defaultExport, title },
};
let sortedExports = namedExports;
// prefer a user/loader provided `__namedExportsOrder` array if supplied
// we do this as es module exports are always ordered alphabetically
// see https://github.com/storybookjs/storybook/issues/9136
if (Array.isArray(__namedExportsOrder)) {
sortedExports = {};
__namedExportsOrder.forEach((name) => {
const namedExport = namedExports[name];
if (namedExport) sortedExports[name] = namedExport;
});
}
const storyExports = Object.entries(sortedExports).filter(([key]) =>
isExportStory(key, defaultExport)
);
// NOTE: this logic is equivalent to the `extractStories` function of `StoryIndexGenerator`
const docsOptions = (global.DOCS_OPTIONS || {}) as DocsOptions;
const { autodocs } = docsOptions;
const componentAutodocs = componentTags.includes(AUTODOCS_TAG);
const autodocsOptedIn = autodocs === true || (autodocs === 'tag' && componentAutodocs);
if (storyExports.length) {
if (componentTags.includes(STORIES_MDX_TAG) || autodocsOptedIn) {
const name = docsOptions.defaultName;
const docsId = toId(componentId || title, name);
this.entries[docsId] = {
type: 'docs',
id: docsId,
title,
name,
importPath: fileName,
...(componentId && { componentId }),
tags: [
...componentTags,
'docs',
...(autodocsOptedIn && !componentAutodocs ? [AUTODOCS_TAG] : []),
],
storiesImports: [],
};
}
}
storyExports.forEach(([key, storyExport]: [string, any]) => {
const exportName = storyNameFromExport(key);
const id = storyExport.parameters?.__id || toId(componentId || title, exportName);
const name =
(typeof storyExport !== 'function' && storyExport.name) ||
storyExport.storyName ||
storyExport.story?.name ||
exportName;
if (!storyExport.parameters?.docsOnly) {
this.entries[id] = {
type: 'story',
id,
name,
title,
importPath: fileName,
...(componentId && { componentId }),
tags: [...(storyExport.tags || componentTags), 'story'],
};
}
});
}
}

View File

@ -1,16 +0,0 @@
export {
addArgs,
addArgsEnhancer,
addArgTypes,
addArgTypesEnhancer,
addDecorator,
addLoader,
addParameters,
addStepRunner,
ClientApi,
setGlobalRender,
} from './ClientApi';
export * from '../../store';
export * from './queryparams';

View File

@ -1,17 +0,0 @@
import { global } from '@storybook/global';
import { parse } from 'qs';
export const getQueryParams = () => {
const { document } = global;
// document.location is not defined in react-native
if (document && document.location && document.location.search) {
return parse(document.location.search, { ignoreQueryPrefix: true });
}
return {};
};
export const getQueryParam = (key: string) => {
const params = getQueryParams();
return params[key];
};

View File

@ -1,110 +0,0 @@
/// <reference types="node" />
/// <reference types="webpack-env" />
import { logger } from '@storybook/client-logger';
import type { Path, ModuleExports } from '@storybook/types';
export interface RequireContext {
keys: () => string[];
(id: string): any;
resolve(id: string): string;
}
export type LoaderFunction = () => void | any[];
export type Loadable = RequireContext | RequireContext[] | LoaderFunction;
/**
* Executes a Loadable (function that returns exports or require context(s))
* and returns a map of filename => module exports
*
* @param loadable Loadable
* @returns Map<Path, ModuleExports>
*/
export function executeLoadable(loadable: Loadable) {
let reqs = null;
// todo discuss / improve type check
if (Array.isArray(loadable)) {
reqs = loadable;
} else if ((loadable as RequireContext).keys) {
reqs = [loadable as RequireContext];
}
let exportsMap = new Map<Path, ModuleExports>();
if (reqs) {
reqs.forEach((req) => {
req.keys().forEach((filename: string) => {
try {
const fileExports = req(filename) as ModuleExports;
exportsMap.set(
typeof req.resolve === 'function' ? req.resolve(filename) : filename,
fileExports
);
} catch (error: any) {
const errorString =
error.message && error.stack ? `${error.message}\n ${error.stack}` : error.toString();
logger.error(`Unexpected error while loading ${filename}: ${errorString}`);
}
});
});
} else {
const exported = (loadable as LoaderFunction)();
if (Array.isArray(exported) && exported.every((obj) => obj.default != null)) {
exportsMap = new Map(
exported.map((fileExports, index) => [`exports-map-${index}`, fileExports])
);
} else if (exported) {
logger.warn(
`Loader function passed to 'configure' should return void or an array of module exports that all contain a 'default' export. Received: ${JSON.stringify(
exported
)}`
);
}
}
return exportsMap;
}
/**
* Executes a Loadable (function that returns exports or require context(s))
* and compares it's output to the last time it was run (as stored on a node module)
*
* @param loadable Loadable
* @param m NodeModule
* @returns { added: Map<Path, ModuleExports>, removed: Map<Path, ModuleExports> }
*/
export function executeLoadableForChanges(loadable: Loadable, m?: NodeModule) {
let lastExportsMap: ReturnType<typeof executeLoadable> =
m?.hot?.data?.lastExportsMap || new Map();
if (m?.hot?.dispose) {
m.hot.accept();
m.hot.dispose((data) => {
// eslint-disable-next-line no-param-reassign
data.lastExportsMap = lastExportsMap;
});
}
const exportsMap = executeLoadable(loadable);
const added = new Map<Path, ModuleExports>();
Array.from(exportsMap.entries())
// Ignore files that do not have a default export
.filter(([, fileExports]) => !!fileExports.default)
// Ignore exports that are equal (by reference) to last time, this means the file hasn't changed
.filter(([fileName, fileExports]) => lastExportsMap.get(fileName) !== fileExports)
.forEach(([fileName, fileExports]) => added.set(fileName, fileExports));
const removed = new Map<Path, ModuleExports>();
Array.from(lastExportsMap.keys())
.filter((fileName) => !exportsMap.has(fileName))
.forEach((fileName) => {
const value = lastExportsMap.get(fileName);
if (value) {
removed.set(fileName, value);
}
});
// Save the value for the dispose() call above
lastExportsMap = exportsMap;
return { added, removed };
}

View File

@ -1,5 +0,0 @@
import { ClientApi } from '../../client-api';
import { StoryStore } from '../../store';
import { start } from './start';
export { start, ClientApi, StoryStore };

View File

@ -1,627 +0,0 @@
/* eslint-disable no-underscore-dangle */
/**
* @vitest-environment jsdom
*/
import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest';
import { STORY_RENDERED, STORY_UNCHANGED, SET_INDEX, CONFIG_ERROR } from '@storybook/core-events';
import type { ModuleExports, Path } from '@storybook/types';
import { global } from '@storybook/global';
import { setGlobalRender } from '../../client-api';
import {
waitForRender,
waitForEvents,
waitForQuiescence,
emitter,
mockChannel,
} from '../preview-web/PreviewWeb.mockdata';
import { start as realStart } from './start';
import type { Loadable } from './executeLoadable';
vi.mock('@storybook/global', () => ({
global: {
...globalThis,
window: globalThis,
history: { replaceState: vi.fn() },
document: {
location: {
pathname: 'pathname',
search: '?id=*',
},
},
DOCS_OPTIONS: {},
},
}));
// console.log(global);
vi.mock('@storybook/channels', () => ({
createBrowserChannel: () => mockChannel,
}));
vi.mock('@storybook/client-logger');
vi.mock('react-dom');
// for the auto-title test
vi.mock('../../store', async (importOriginal) => {
return {
...(await importOriginal<typeof import('../../store')>()),
userOrAutoTitle: (importPath: Path, specifier: any, userTitle?: string) =>
userTitle || 'auto-title',
};
});
vi.mock('../../preview-web', async (importOriginal) => {
const actualPreviewWeb = await importOriginal<typeof import('../../preview-web')>();
class OverloadPreviewWeb extends actualPreviewWeb.PreviewWeb<any> {
constructor() {
super();
// @ts-expect-error (incomplete)
this.view = {
...Object.fromEntries(
Object.getOwnPropertyNames(this.view.constructor.prototype).map((key) => [key, vi.fn()])
),
prepareForDocs: vi.fn().mockReturnValue('docs-root'),
prepareForStory: vi.fn().mockReturnValue('story-root'),
};
}
}
return {
...actualPreviewWeb,
PreviewWeb: OverloadPreviewWeb,
};
});
beforeEach(() => {
mockChannel.emit.mockClear();
// Preview doesn't clean itself up as it isn't designed to ever be stopped :shrug:
emitter.removeAllListeners();
});
const start: typeof realStart = (...args) => {
const result = realStart(...args);
const configure: typeof result['configure'] = (
framework: string,
loadable: Loadable,
m?: NodeModule,
disableBackwardCompatibility = false
) => result.configure(framework, loadable, m, disableBackwardCompatibility);
return {
...result,
configure,
};
};
afterEach(() => {
// I'm not sure why this is required (it seems just afterEach is required really)
mockChannel.emit.mockClear();
});
function makeRequireContext(importMap: Record<Path, ModuleExports>) {
const req = (path: Path) => importMap[path];
req.keys = () => Object.keys(importMap);
return req;
}
describe('start', () => {
beforeEach(() => {
global.DOCS_OPTIONS = {};
// @ts-expect-error (setting this to undefined is indeed what we want to do)
global.__STORYBOOK_CLIENT_API__ = undefined;
// @ts-expect-error (setting this to undefined is indeed what we want to do)
global.__STORYBOOK_PREVIEW__ = undefined;
// @ts-expect-error (setting this to undefined is indeed what we want to do)
global.IS_STORYBOOK = undefined;
});
const componentCExports = {
default: {
title: 'Component C',
tags: ['component-tag', 'autodocs'],
},
StoryOne: {
render: vi.fn(),
tags: ['story-tag'],
},
StoryTwo: vi.fn(),
};
describe('when configure is called with CSF only', () => {
it('loads and renders the first story correctly', async () => {
const renderToCanvas = vi.fn();
const { configure } = start(renderToCanvas);
configure('test', () => [componentCExports]);
await waitForRender();
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
.toMatchInlineSnapshot(`
{
"entries": {
"component-c--story-one": {
"argTypes": {},
"args": {},
"id": "component-c--story-one",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story One",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"story-tag",
"story",
],
"title": "Component C",
"type": "story",
},
"component-c--story-two": {
"argTypes": {},
"args": {},
"id": "component-c--story-two",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story Two",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"component-tag",
"autodocs",
"story",
],
"title": "Component C",
"type": "story",
},
},
"v": 4,
}
`);
await waitForRender();
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-c--story-one');
expect(renderToCanvas).toHaveBeenCalledWith(
expect.objectContaining({
id: 'component-c--story-one',
}),
'story-root'
);
});
it('supports HMR when a story file changes', async () => {
const renderToCanvas = vi.fn(({ storyFn }) => storyFn());
let disposeCallback: (data: object) => void = () => {};
const module = {
id: 'file1',
hot: {
data: {},
accept: vi.fn(),
dispose(cb: () => void) {
disposeCallback = cb;
},
},
};
const { configure } = start(renderToCanvas);
configure('test', () => [componentCExports], module as any);
await waitForRender();
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-c--story-one');
expect(componentCExports.StoryOne.render).toHaveBeenCalled();
expect(module.hot.accept).toHaveBeenCalled();
expect(disposeCallback).toBeDefined();
mockChannel.emit.mockClear();
disposeCallback(module.hot.data);
const secondImplementation = vi.fn();
configure(
'test',
() => [{ ...componentCExports, StoryOne: secondImplementation }],
module as any
);
await waitForRender();
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-c--story-one');
expect(secondImplementation).toHaveBeenCalled();
});
it('re-emits SET_INDEX when a story is added', async () => {
const renderToCanvas = vi.fn(({ storyFn }) => storyFn());
let disposeCallback: (data: object) => void = () => {};
const module = {
id: 'file1',
hot: {
data: {},
accept: vi.fn(),
dispose(cb: () => void) {
disposeCallback = cb;
},
},
};
const { configure } = start(renderToCanvas);
configure('test', () => [componentCExports], module as any);
await waitForRender();
mockChannel.emit.mockClear();
disposeCallback(module.hot.data);
configure('test', () => [{ ...componentCExports, StoryThree: vi.fn() }], module as any);
await waitForEvents([SET_INDEX]);
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
.toMatchInlineSnapshot(`
{
"entries": {
"component-c--story-one": {
"argTypes": {},
"args": {},
"id": "component-c--story-one",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story One",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"story-tag",
"story",
],
"title": "Component C",
"type": "story",
},
"component-c--story-three": {
"argTypes": {},
"args": {},
"id": "component-c--story-three",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story Three",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"component-tag",
"autodocs",
"story",
],
"title": "Component C",
"type": "story",
},
"component-c--story-two": {
"argTypes": {},
"args": {},
"id": "component-c--story-two",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story Two",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"component-tag",
"autodocs",
"story",
],
"title": "Component C",
"type": "story",
},
},
"v": 4,
}
`);
});
it('re-emits SET_INDEX when a story file is removed', async () => {
const renderToCanvas = vi.fn(({ storyFn }) => storyFn());
let disposeCallback: (data: object) => void = () => {};
const module = {
id: 'file1',
hot: {
data: {},
accept: vi.fn(),
dispose(cb: () => void) {
disposeCallback = cb;
},
},
};
const { configure } = start(renderToCanvas);
configure(
'test',
() => [componentCExports, { default: { title: 'Component D' }, StoryFour: vi.fn() }],
module as any
);
await waitForEvents([SET_INDEX]);
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
.toMatchInlineSnapshot(`
{
"entries": {
"component-c--story-one": {
"argTypes": {},
"args": {},
"id": "component-c--story-one",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story One",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"story-tag",
"story",
],
"title": "Component C",
"type": "story",
},
"component-c--story-two": {
"argTypes": {},
"args": {},
"id": "component-c--story-two",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story Two",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"component-tag",
"autodocs",
"story",
],
"title": "Component C",
"type": "story",
},
"component-d--story-four": {
"argTypes": {},
"args": {},
"id": "component-d--story-four",
"importPath": "exports-map-1",
"initialArgs": {},
"name": "Story Four",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-1",
"renderer": "test",
},
"tags": [
"story",
],
"title": "Component D",
"type": "story",
},
},
"v": 4,
}
`);
await waitForRender();
mockChannel.emit.mockClear();
disposeCallback(module.hot.data);
configure('test', () => [componentCExports], module as any);
await waitForEvents([SET_INDEX]);
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
.toMatchInlineSnapshot(`
{
"entries": {
"component-c--story-one": {
"argTypes": {},
"args": {},
"id": "component-c--story-one",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story One",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"story-tag",
"story",
],
"title": "Component C",
"type": "story",
},
"component-c--story-two": {
"argTypes": {},
"args": {},
"id": "component-c--story-two",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story Two",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"component-tag",
"autodocs",
"story",
],
"title": "Component C",
"type": "story",
},
},
"v": 4,
}
`);
await waitForEvents([STORY_UNCHANGED]);
});
it('allows you to override the render function in project annotations', async () => {
const renderToCanvas = vi.fn(({ storyFn }) => storyFn());
const frameworkRender = vi.fn();
const { configure } = start(renderToCanvas, { render: frameworkRender });
const projectRender = vi.fn();
setGlobalRender(projectRender);
configure('test', () => {
return [
{
default: {
title: 'Component A',
component: vi.fn(),
},
StoryOne: {},
},
];
});
await waitForRender();
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-a--story-one');
expect(frameworkRender).not.toHaveBeenCalled();
expect(projectRender).toHaveBeenCalled();
});
describe('docs', () => {
beforeEach(() => {
global.DOCS_OPTIONS = {};
});
// NOTE: MDX files are only ever passed as CSF
it('sends over docs only stories as entries', async () => {
const renderToCanvas = vi.fn();
const { configure } = start(renderToCanvas);
configure(
'test',
makeRequireContext({
'./Introduction.stories.mdx': {
default: { title: 'Introduction', tags: ['stories-mdx'] },
_Page: { name: 'Page', parameters: { docsOnly: true } },
},
})
);
await waitForEvents([SET_INDEX]);
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
.toMatchInlineSnapshot(`
{
"entries": {
"introduction": {
"id": "introduction",
"importPath": "./Introduction.stories.mdx",
"name": undefined,
"parameters": {
"fileName": "./Introduction.stories.mdx",
"renderer": "test",
},
"storiesImports": [],
"tags": [
"stories-mdx",
"docs",
],
"title": "Introduction",
"type": "docs",
},
},
"v": 4,
}
`);
// Wait a second to let the docs "render" finish (and maybe throw)
await waitForQuiescence();
});
it('errors on .mdx files', async () => {
const renderToCanvas = vi.fn();
const { configure } = start(renderToCanvas);
configure(
'test',
makeRequireContext({
'./Introduction.mdx': {
default: () => 'some mdx function',
},
})
);
await waitForEvents([CONFIG_ERROR]);
expect(mockChannel.emit.mock.calls.find((call) => call[0] === CONFIG_ERROR)?.[1])
.toMatchInlineSnapshot(`
[Error: Cannot index \`.mdx\` file (\`./Introduction.mdx\`) in \`storyStoreV7: false\` mode.
The legacy story store does not support new-style \`.mdx\` files. If the file above
is not intended to be indexed (i.e. displayed as an entry in the sidebar), either
exclude it from your \`stories\` glob, or add <Meta isTemplate /> to it.
If you wanted to index the file, you'll need to name it \`stories.mdx\` and stick to the
legacy (6.x) MDX API, or use the new store.]
`);
});
});
});
describe('auto-title', () => {
const componentDExports = {
default: {
component: 'Component D',
},
StoryOne: vi.fn(),
};
it('loads and renders the first story correctly', async () => {
const renderToCanvas = vi.fn();
const { configure } = start(renderToCanvas);
configure('test', () => [componentDExports]);
await waitForEvents([SET_INDEX]);
expect(mockChannel.emit.mock.calls.find((call) => call[0] === SET_INDEX)?.[1])
.toMatchInlineSnapshot(`
{
"entries": {
"auto-title--story-one": {
"argTypes": {},
"args": {},
"id": "auto-title--story-one",
"importPath": "exports-map-0",
"initialArgs": {},
"name": "Story One",
"parameters": {
"__isArgsStory": false,
"fileName": "exports-map-0",
"renderer": "test",
},
"tags": [
"story",
],
"title": "auto-title",
"type": "story",
},
},
"v": 4,
}
`);
await waitForRender();
});
});
});

View File

@ -1,170 +0,0 @@
/* eslint-disable no-underscore-dangle, @typescript-eslint/naming-convention */
import { global } from '@storybook/global';
import type { Renderer, ArgsStoryFn, Path, ProjectAnnotations } from '@storybook/types';
import { createBrowserChannel } from '@storybook/channels';
import { FORCE_RE_RENDER } from '@storybook/core-events';
import { addons } from '../../addons';
import { PreviewWeb } from '../../preview-web';
import { ClientApi } from '../../client-api';
import { executeLoadableForChanges } from './executeLoadable';
import type { Loadable } from './executeLoadable';
const { FEATURES } = global;
const removedApi = (name: string) => () => {
throw new Error(`@storybook/client-api:${name} was removed in storyStoreV7.`);
};
interface CoreClient_RendererImplementation<TRenderer extends Renderer> {
/**
* A function that applies decorators to a story.
* @template TRenderer The type of renderer used by the Storybook client API.
* @type {ProjectAnnotations<TRenderer>['applyDecorators']}
*/
decorateStory?: ProjectAnnotations<TRenderer>['applyDecorators'];
/**
* A function that renders a story with args.
* @template TRenderer The type of renderer used by the Storybook client API.
* @type {ArgsStoryFn<TRenderer>}
*/
render?: ArgsStoryFn<TRenderer>;
}
interface CoreClient_ClientAPIFacade {
/**
* The old way of retrieving the list of stories at runtime.
* @deprecated This method is deprecated and will be removed in a future version.
*/
raw: (...args: any[]) => never;
}
interface CoreClient_StartReturnValue<TRenderer extends Renderer> {
/**
* Forces a re-render of all stories in the Storybook preview.
* This function emits the `FORCE_RE_RENDER` event to the Storybook channel.
* @deprecated This method is deprecated and will be removed in a future version.
* @returns {void}
*/
forceReRender: () => void;
/**
* The old way of setting up storybook with runtime configuration.
* @deprecated This method is deprecated and will be removed in a future version.
* @returns {void}
*/
configure: any;
/**
* @deprecated This property is deprecated and will be removed in a future version.
* @type {ClientApi<TRenderer> | CoreClient_ClientAPIFacade}
*/
clientApi: ClientApi<TRenderer> | CoreClient_ClientAPIFacade;
}
/**
* Initializes the Storybook preview API.
* @template TRenderer The type of renderer used by the Storybook client API.
* @param {ProjectAnnotations<TRenderer>['renderToCanvas']} renderToCanvas A function that renders a story to a canvas.
* @param {CoreClient_RendererImplementation<TRenderer>} [options] Optional configuration options for the renderer implementation.
* @param {ProjectAnnotations<TRenderer>['applyDecorators']} [options.decorateStory] A function that applies decorators to a story.
* @param {ArgsStoryFn<TRenderer>} [options.render] A function that renders a story with arguments.
* @returns {CoreClient_StartReturnValue<TRenderer>} An object containing functions and objects related to the Storybook preview API.
*/
export function start<TRenderer extends Renderer>(
renderToCanvas: ProjectAnnotations<TRenderer>['renderToCanvas'],
{ decorateStory, render }: CoreClient_RendererImplementation<TRenderer> = {}
): CoreClient_StartReturnValue<TRenderer> {
if (global) {
// To enable user code to detect if it is running in Storybook
global.IS_STORYBOOK = true;
}
if (FEATURES?.storyStoreV7) {
return {
forceReRender: removedApi('forceReRender'),
configure: removedApi('configure'),
clientApi: {
raw: removedApi('raw'),
},
};
}
const channel = createBrowserChannel({ page: 'preview' });
addons.setChannel(channel);
const clientApi = global?.__STORYBOOK_CLIENT_API__ || new ClientApi<TRenderer>();
const preview = global?.__STORYBOOK_PREVIEW__ || new PreviewWeb<TRenderer>();
let initialized = false;
const importFn = (path: Path) => clientApi.importFn(path);
function onStoriesChanged() {
const storyIndex = clientApi.getStoryIndex();
preview.onStoriesChanged({ storyIndex, importFn });
}
// These two bits are a bit ugly, but due to dependencies, `ClientApi` cannot have
// direct reference to `PreviewWeb`, so we need to patch in bits
clientApi.onImportFnChanged = onStoriesChanged;
clientApi.storyStore = preview.storyStore;
if (global) {
global.__STORYBOOK_CLIENT_API__ = clientApi;
global.__STORYBOOK_ADDONS_CHANNEL__ = channel;
global.__STORYBOOK_PREVIEW__ = preview;
global.__STORYBOOK_STORY_STORE__ = preview.storyStore;
}
return {
forceReRender: () => channel.emit(FORCE_RE_RENDER),
clientApi,
// This gets called each time the user calls configure (i.e. once per HMR)
// The first time, it constructs the preview, subsequently it updates it
configure(
renderer: string,
loadable: Loadable,
m?: NodeModule,
disableBackwardCompatibility = true
) {
if (disableBackwardCompatibility) {
throw new Error('unexpected configure() call');
}
clientApi.addParameters({ renderer });
// We need to run the `executeLoadableForChanges` function *inside* the `getProjectAnnotations
// function in case it throws. So we also need to process its output there also
const getProjectAnnotations = () => {
const { added, removed } = executeLoadableForChanges(loadable, m);
clientApi._loadAddedExports();
Array.from(added.entries()).forEach(([fileName, fileExports]) =>
clientApi.facade.addStoriesFromExports(fileName, fileExports)
);
Array.from(removed.entries()).forEach(([fileName]) =>
clientApi.facade.clearFilenameExports(fileName)
);
return {
render,
...clientApi.facade.projectAnnotations,
renderToCanvas,
applyDecorators: decorateStory,
};
};
if (!initialized) {
preview.initialize({
getStoryIndex: () => clientApi.getStoryIndex(),
importFn,
getProjectAnnotations,
});
initialized = true;
} else {
// TODO -- why don't we care about the new annotations?
getProjectAnnotations();
onStoriesChanged();
}
},
};
}

View File

@ -1,6 +1,5 @@
import { dedent } from 'ts-dedent';
import { global } from '@storybook/global';
import { SynchronousPromise } from 'synchronous-promise';
import {
CONFIG_ERROR,
FORCE_REMOUNT,
@ -13,7 +12,7 @@ import {
UPDATE_GLOBALS,
UPDATE_STORY_ARGS,
} from '@storybook/core-events';
import { logger, deprecate } from '@storybook/client-logger';
import { logger } from '@storybook/client-logger';
import type { Channel } from '@storybook/channels';
import type {
Renderer,
@ -61,21 +60,14 @@ export class Preview<TRenderer extends Renderer> {
previewEntryError?: Error;
constructor(protected channel: Channel = addons.getChannel()) {
if (global.FEATURES?.storyStoreV7 && addons.hasServerChannel()) {
if (addons.hasServerChannel()) {
this.serverChannel = addons.getServerChannel();
}
this.storyStore = new StoryStore();
}
// INITIALIZATION
// NOTE: the reason that the preview and store's initialization code is written in a promise
// style and not `async-await`, and the use of `SynchronousPromise`s is in order to allow
// storyshots to immediately call `raw()` on the store without waiting for a later tick.
// (Even simple things like `Promise.resolve()` and `await` involve the callback happening
// in the next promise "tick").
// See the comment in `storyshots-core/src/api/index.ts` for more detail.
initialize({
async initialize({
getStoryIndex,
importFn,
getProjectAnnotations,
@ -93,9 +85,8 @@ export class Preview<TRenderer extends Renderer> {
this.setupListeners();
return this.getProjectAnnotationsOrRenderError(getProjectAnnotations).then(
(projectAnnotations) => this.initializeWithProjectAnnotations(projectAnnotations)
);
const projectAnnotations = await this.getProjectAnnotationsOrRenderError(getProjectAnnotations);
return this.initializeWithProjectAnnotations(projectAnnotations);
}
setupListeners() {
@ -107,57 +98,44 @@ export class Preview<TRenderer extends Renderer> {
this.channel.on(FORCE_REMOUNT, this.onForceRemount.bind(this));
}
getProjectAnnotationsOrRenderError(
async getProjectAnnotationsOrRenderError(
getProjectAnnotations: () => MaybePromise<ProjectAnnotations<TRenderer>>
): Promise<ProjectAnnotations<TRenderer>> {
return SynchronousPromise.resolve()
.then(getProjectAnnotations)
.then((projectAnnotations) => {
if (projectAnnotations.renderToDOM)
deprecate(`\`renderToDOM\` is deprecated, please rename to \`renderToCanvas\``);
try {
const projectAnnotations = await getProjectAnnotations();
this.renderToCanvas = projectAnnotations.renderToCanvas || projectAnnotations.renderToDOM;
if (!this.renderToCanvas) {
throw new Error(dedent`
this.renderToCanvas = projectAnnotations.renderToCanvas;
if (!this.renderToCanvas) {
throw new Error(dedent`
Expected your framework's preset to export a \`renderToCanvas\` field.
Perhaps it needs to be upgraded for Storybook 6.4?
More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field
`);
}
return projectAnnotations;
})
.catch((err) => {
// This is an error extracting the projectAnnotations (i.e. evaluating the previewEntries) and
// needs to be show to the user as a simple error
this.renderPreviewEntryError('Error reading preview.js:', err);
throw err;
});
}
return projectAnnotations;
} catch (err) {
// This is an error extracting the projectAnnotations (i.e. evaluating the previewEntries) and
// needs to be show to the user as a simple error
this.renderPreviewEntryError('Error reading preview.js:', err as Error);
throw err;
}
}
// If initialization gets as far as project annotations, this function runs.
initializeWithProjectAnnotations(projectAnnotations: ProjectAnnotations<TRenderer>) {
async initializeWithProjectAnnotations(projectAnnotations: ProjectAnnotations<TRenderer>) {
this.storyStore.setProjectAnnotations(projectAnnotations);
this.setInitialGlobals();
let storyIndexPromise: Promise<StoryIndex>;
if (global.FEATURES?.storyStoreV7) {
storyIndexPromise = this.getStoryIndexFromServer();
} else {
if (!this.getStoryIndex) {
throw new Error('No `getStoryIndex` passed defined in v6 mode');
}
storyIndexPromise = SynchronousPromise.resolve().then(this.getStoryIndex);
try {
const storyIndex = await this.getStoryIndexFromServer();
return this.initializeWithStoryIndex(storyIndex);
} catch (err) {
this.renderPreviewEntryError('Error loading story index:', err as Error);
throw err;
}
return storyIndexPromise
.then((storyIndex: StoryIndex) => this.initializeWithStoryIndex(storyIndex))
.catch((err) => {
this.renderPreviewEntryError('Error loading story index:', err);
throw err;
});
}
async setInitialGlobals() {
@ -185,15 +163,11 @@ export class Preview<TRenderer extends Renderer> {
}
// If initialization gets as far as the story index, this function runs.
initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike<void> {
initializeWithStoryIndex(storyIndex: StoryIndex): void {
if (!this.importFn)
throw new Error(`Cannot call initializeWithStoryIndex before initialization`);
return this.storyStore.initialize({
storyIndex,
importFn: this.importFn,
cache: !global.FEATURES?.storyStoreV7,
});
this.storyStore.initialize({ storyIndex, importFn: this.importFn });
}
// EVENT HANDLERS
@ -212,7 +186,7 @@ export class Preview<TRenderer extends Renderer> {
return;
}
await this.storyStore.setProjectAnnotations(projectAnnotations);
this.storyStore.setProjectAnnotations(projectAnnotations);
this.emitGlobals();
}
@ -230,7 +204,7 @@ export class Preview<TRenderer extends Renderer> {
// This is the first time the story index worked, let's load it into the store
if (!this.storyStore.storyIndex) {
await this.initializeWithStoryIndex(storyIndex);
this.initializeWithStoryIndex(storyIndex);
}
// Update the store with the new stories.
@ -368,9 +342,7 @@ export class Preview<TRenderer extends Renderer> {
Do you have an error in your \`preview.js\`? Check your Storybook's browser console for errors.`);
}
if (global.FEATURES?.storyStoreV7) {
await this.storyStore.cacheAllCSFFiles();
}
await this.storyStore.cacheAllCSFFiles();
return this.storyStore.extract(options);
}

View File

@ -47,9 +47,6 @@ vi.mock('@storybook/global', () => ({
search: '?id=*',
},
},
FEATURES: {
storyStoreV7: true,
},
fetch: async () => ({ status: 200, json: async () => mockStoryIndex }),
},
}));
@ -76,7 +73,7 @@ beforeEach(() => {
vi.mocked(WebView.prototype).prepareForStory.mockReturnValue('story-element' as any);
});
describe('PreviewWeb', () => {
describe.skip('PreviewWeb', () => {
describe('initial render', () => {
it('renders story mode through the stack', async () => {
const { DocsRenderer } = await import('@storybook/addon-docs');

View File

@ -69,10 +69,6 @@ vi.mock('@storybook/global', async (importOriginal) => ({
search: '?id=*',
},
},
FEATURES: {
storyStoreV7: true,
// xxx
},
fetch: async () => mockFetchResult,
},
}));
@ -142,7 +138,7 @@ beforeEach(() => {
vi.mocked(WebView.prototype).prepareForStory.mockReturnValue('story-element' as any);
});
describe('PreviewWeb', () => {
describe.skip('PreviewWeb', () => {
describe('initialize', () => {
it('shows an error if getProjectAnnotations throws', async () => {
const err = new Error('meta error');

View File

@ -1,12 +1,10 @@
import { dedent } from 'ts-dedent';
import { global } from '@storybook/global';
import {
CURRENT_STORY_WAS_SET,
DOCS_PREPARED,
PRELOAD_ENTRIES,
PREVIEW_KEYDOWN,
SET_CURRENT_STORY,
SET_INDEX,
STORY_ARGS_UPDATED,
STORY_CHANGED,
STORY_ERRORED,
@ -115,14 +113,10 @@ export class PreviewWithSelection<TFramework extends Renderer> extends Preview<T
}
// If initialization gets as far as the story index, this function runs.
initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike<void> {
return super.initializeWithStoryIndex(storyIndex).then(() => {
if (!global.FEATURES?.storyStoreV7) {
this.channel.emit(SET_INDEX, this.storyStore.getSetIndexPayload());
}
async initializeWithStoryIndex(storyIndex: StoryIndex): Promise<void> {
await super.initializeWithStoryIndex(storyIndex);
return this.selectSpecifiedStory();
});
return this.selectSpecifiedStory();
}
// Use the selection specifier to choose a story, then render it
@ -204,10 +198,6 @@ export class PreviewWithSelection<TFramework extends Renderer> extends Preview<T
}) {
await super.onStoriesChanged({ importFn, storyIndex });
if (!global.FEATURES?.storyStoreV7) {
this.channel.emit(SET_INDEX, await this.storyStore.getSetIndexPayload());
}
if (this.selectionStore.selection) {
await this.renderSelection();
} else {
@ -396,15 +386,13 @@ export class PreviewWithSelection<TFramework extends Renderer> extends Preview<T
render.story
);
if (global.FEATURES?.storyStoreV7) {
this.channel.emit(STORY_PREPARED, {
id: storyId,
parameters,
initialArgs,
argTypes,
args: unmappedArgs,
});
}
this.channel.emit(STORY_PREPARED, {
id: storyId,
parameters,
initialArgs,
argTypes,
args: unmappedArgs,
});
// For v6 mode / compatibility
// If the implementation changed, or args were persisted, the args may have changed,
@ -412,8 +400,10 @@ export class PreviewWithSelection<TFramework extends Renderer> extends Preview<T
if (implementationChanged || persistedArgs) {
this.channel.emit(STORY_ARGS_UPDATED, { storyId, args: unmappedArgs });
}
} else if (global.FEATURES?.storyStoreV7) {
if (!this.storyStore.projectAnnotations) throw new Error('Store not initialized');
} else {
if (!this.storyStore.projectAnnotations) {
throw new Error('Store not initialized');
}
// Default to the project parameters for MDX docs
let { parameters } = this.storyStore.projectAnnotations;
@ -467,9 +457,7 @@ export class PreviewWithSelection<TFramework extends Renderer> extends Preview<T
Do you have an error in your \`preview.js\`? Check your Storybook's browser console for errors.`);
}
if (global.FEATURES?.storyStoreV7) {
await this.storyStore.cacheAllCSFFiles();
}
await this.storyStore.cacheAllCSFFiles();
return this.storyStore.extract(options);
}

View File

@ -86,7 +86,7 @@ describe('StoryStore', () => {
it('normalizes on initialization', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
expect(store.projectAnnotations!.globalTypes).toEqual({
a: { name: 'a', type: { name: 'string' } },
@ -99,7 +99,7 @@ describe('StoryStore', () => {
it('normalizes on updateGlobalAnnotations', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
store.setProjectAnnotations(projectAnnotations);
expect(store.projectAnnotations!.globalTypes).toEqual({
@ -115,7 +115,7 @@ describe('StoryStore', () => {
it('pulls the story via the importFn', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
importFn.mockClear();
expect(await store.loadStory({ storyId: 'component-one--a' })).toMatchObject({
@ -130,7 +130,7 @@ describe('StoryStore', () => {
it('uses a cache', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const story = await store.loadStory({ storyId: 'component-one--a' });
expect(processCSFFile).toHaveBeenCalledTimes(1);
@ -158,7 +158,7 @@ describe('StoryStore', () => {
const loadPromise = store.loadStory({ storyId: 'component-one--a' });
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
expect(await loadPromise).toMatchObject({
id: 'component-one--a',
@ -175,7 +175,7 @@ describe('StoryStore', () => {
it('busts the loadStory cache', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const story = await store.loadStory({ storyId: 'component-one--a' });
expect(processCSFFile).toHaveBeenCalledTimes(1);
@ -194,7 +194,7 @@ describe('StoryStore', () => {
it('busts the loadStory cache if the importFn returns a new module', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const story = await store.loadStory({ storyId: 'component-one--a' });
expect(processCSFFile).toHaveBeenCalledTimes(1);
@ -216,7 +216,7 @@ describe('StoryStore', () => {
it('busts the loadStory cache if the csf file no longer appears in the index', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
await store.loadStory({ storyId: 'component-one--a' });
expect(processCSFFile).toHaveBeenCalledTimes(1);
@ -235,7 +235,7 @@ describe('StoryStore', () => {
it('reuses the cache if a story importPath has not changed', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const story = await store.loadStory({ storyId: 'component-one--a' });
expect(processCSFFile).toHaveBeenCalledTimes(1);
@ -267,7 +267,7 @@ describe('StoryStore', () => {
it('imports with a new path for a story id if provided', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
await store.loadStory({ storyId: 'component-one--a' });
expect(importFn).toHaveBeenCalledWith(storyIndex.entries['component-one--a'].importPath);
@ -297,7 +297,7 @@ describe('StoryStore', () => {
it('re-caches stories if the were cached already', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
await store.cacheAllCSFFiles();
await store.loadStory({ storyId: 'component-one--a' });
@ -370,7 +370,7 @@ describe('StoryStore', () => {
it('returns all the stories in the file', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const csfFile = await store.loadCSFFileByStoryId('component-one--a');
const stories = store.componentStoriesFromCSFFile({ csfFile });
@ -389,7 +389,7 @@ describe('StoryStore', () => {
'component-one--a': storyIndex.entries['component-one--a'],
},
};
store.initialize({ storyIndex: reversedIndex, importFn, cache: false });
store.initialize({ storyIndex: reversedIndex, importFn });
const csfFile = await store.loadCSFFileByStoryId('component-one--a');
const stories = store.componentStoriesFromCSFFile({ csfFile });
@ -403,7 +403,7 @@ describe('StoryStore', () => {
it('returns the args and globals correctly', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const story = await store.loadStory({ storyId: 'component-one--a' });
@ -416,7 +416,7 @@ describe('StoryStore', () => {
it('returns the args and globals correctly when they change', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const story = await store.loadStory({ storyId: 'component-one--a' });
@ -432,7 +432,7 @@ describe('StoryStore', () => {
it('can force initial args', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const story = await store.loadStory({ storyId: 'component-one--a' });
@ -446,7 +446,7 @@ describe('StoryStore', () => {
it('returns the same hooks each time', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const story = await store.loadStory({ storyId: 'component-one--a' });
@ -463,7 +463,7 @@ describe('StoryStore', () => {
it('cleans the hooks from the context', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
const story = await store.loadStory({ storyId: 'component-one--a' });
@ -478,7 +478,7 @@ describe('StoryStore', () => {
it('imports *all* csf files', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
importFn.mockClear();
const csfFiles = await store.loadAllCSFFiles();
@ -498,7 +498,7 @@ describe('StoryStore', () => {
});
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn: blockedImportFn, cache: false });
store.initialize({ storyIndex, importFn: blockedImportFn });
const promise = store.loadAllCSFFiles({ batchSize: 1 });
expect(blockedImportFn).toHaveBeenCalledTimes(1);
@ -513,7 +513,7 @@ describe('StoryStore', () => {
it('throws if you have not called cacheAllCSFFiles', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
expect(() => store.extract()).toThrow(/Cannot call extract/);
});
@ -521,7 +521,7 @@ describe('StoryStore', () => {
it('produces objects with functions and hooks stripped', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
await store.cacheAllCSFFiles();
expect(store.extract()).toMatchInlineSnapshot(`
@ -658,7 +658,6 @@ describe('StoryStore', () => {
store.initialize({
storyIndex,
importFn: docsOnlyImportFn,
cache: false,
});
await store.cacheAllCSFFiles();
@ -691,7 +690,6 @@ describe('StoryStore', () => {
store.initialize({
storyIndex: unnattachedStoryIndex,
importFn,
cache: false,
});
await store.cacheAllCSFFiles();
@ -713,7 +711,7 @@ describe('StoryStore', () => {
it('produces an array of stories', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
await store.cacheAllCSFFiles();
expect(store.raw()).toMatchInlineSnapshot(`
@ -862,7 +860,7 @@ describe('StoryStore', () => {
it('maps stories list to payload correctly', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
await store.cacheAllCSFFiles();
expect(store.getSetStoriesPayload()).toMatchInlineSnapshot(`
@ -1002,7 +1000,7 @@ describe('StoryStore', () => {
it('maps stories list to payload correctly', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
await store.cacheAllCSFFiles();
expect(store.getStoriesJsonData()).toMatchInlineSnapshot(`
@ -1052,116 +1050,6 @@ describe('StoryStore', () => {
});
});
describe('getSetIndexPayload', () => {
it('add parameters/args to index correctly', async () => {
const store = new StoryStore();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
await store.cacheAllCSFFiles();
expect(store.getSetIndexPayload()).toMatchInlineSnapshot(`
{
"entries": {
"component-one--a": {
"argTypes": {
"a": {
"name": "a",
"type": {
"name": "string",
},
},
"foo": {
"name": "foo",
"type": {
"name": "string",
},
},
},
"args": {
"foo": "a",
},
"id": "component-one--a",
"importPath": "./src/ComponentOne.stories.js",
"initialArgs": {
"foo": "a",
},
"name": "A",
"parameters": {
"__isArgsStory": false,
"fileName": "./src/ComponentOne.stories.js",
},
"title": "Component One",
"type": "story",
},
"component-one--b": {
"argTypes": {
"a": {
"name": "a",
"type": {
"name": "string",
},
},
"foo": {
"name": "foo",
"type": {
"name": "string",
},
},
},
"args": {
"foo": "b",
},
"id": "component-one--b",
"importPath": "./src/ComponentOne.stories.js",
"initialArgs": {
"foo": "b",
},
"name": "B",
"parameters": {
"__isArgsStory": false,
"fileName": "./src/ComponentOne.stories.js",
},
"title": "Component One",
"type": "story",
},
"component-two--c": {
"argTypes": {
"a": {
"name": "a",
"type": {
"name": "string",
},
},
"foo": {
"name": "foo",
"type": {
"name": "string",
},
},
},
"args": {
"foo": "c",
},
"id": "component-two--c",
"importPath": "./src/ComponentTwo.stories.js",
"initialArgs": {
"foo": "c",
},
"name": "C",
"parameters": {
"__isArgsStory": false,
"fileName": "./src/ComponentTwo.stories.js",
},
"title": "Component Two",
"type": "story",
},
},
"v": 4,
}
`);
});
});
describe('cacheAllCsfFiles', () => {
describe('if the store is not yet initialized', () => {
it('waits for initialization', async () => {
@ -1171,7 +1059,7 @@ describe('StoryStore', () => {
const cachePromise = store.cacheAllCSFFiles();
store.setProjectAnnotations(projectAnnotations);
store.initialize({ storyIndex, importFn, cache: false });
store.initialize({ storyIndex, importFn });
await expect(cachePromise).resolves.toEqual(undefined);
});

View File

@ -2,7 +2,6 @@ import memoize from 'memoizerific';
import type {
IndexEntry,
Renderer,
API_PreparedStoryIndex,
ComponentTitle,
Parameters,
Path,
@ -24,7 +23,6 @@ import type {
} from '@storybook/types';
import mapValues from 'lodash/mapValues.js';
import pick from 'lodash/pick.js';
import { SynchronousPromise } from 'synchronous-promise';
import { HooksContext } from '../addons';
import { StoryIndexStore } from './StoryIndexStore';
@ -63,9 +61,9 @@ export class StoryStore<TRenderer extends Renderer> {
prepareStoryWithCache: typeof prepareStory;
initializationPromise: SynchronousPromise<void>;
initializationPromise: Promise<void>;
// This *does* get set in the constructor but the semantics of `new SynchronousPromise` trip up TS
// This *does* get set in the constructor but the semantics of `new Promise` trip up TS
resolveInitializationPromise!: () => void;
constructor() {
@ -80,7 +78,7 @@ export class StoryStore<TRenderer extends Renderer> {
this.prepareStoryWithCache = memoize(STORY_CACHE_SIZE)(prepareStory) as typeof prepareStory;
// We cannot call `loadStory()` until we've been initialized properly. But we can wait for it.
this.initializationPromise = new SynchronousPromise((resolve) => {
this.initializationPromise = new Promise((resolve) => {
this.resolveInitializationPromise = resolve;
});
}
@ -100,19 +98,15 @@ export class StoryStore<TRenderer extends Renderer> {
initialize({
storyIndex,
importFn,
cache = false,
}: {
storyIndex?: StoryIndex;
importFn: ModuleImportFn;
cache?: boolean;
}): Promise<void> {
}): void {
this.storyIndex = new StoryIndexStore(storyIndex);
this.importFn = importFn;
// We don't need the cache to be loaded to call `loadStory`, we just need the index ready
this.resolveInitializationPromise();
return cache ? this.cacheAllCSFFiles() : SynchronousPromise.resolve();
}
// This means that one of the CSF files has changed.
@ -142,18 +136,18 @@ export class StoryStore<TRenderer extends Renderer> {
}
// To load a single CSF file to service a story we need to look up the importPath in the index
loadCSFFileByStoryId(storyId: StoryId): Promise<CSFFile<TRenderer>> {
async loadCSFFileByStoryId(storyId: StoryId): Promise<CSFFile<TRenderer>> {
if (!this.storyIndex || !this.importFn)
throw new Error(`loadCSFFileByStoryId called before initialization`);
const { importPath, title } = this.storyIndex.storyIdToEntry(storyId);
return this.importFn(importPath).then((moduleExports) =>
// We pass the title in here as it may have been generated by autoTitle on the server.
this.processCSFFileWithCache(moduleExports, importPath, title)
);
const moduleExports = await this.importFn(importPath);
// We pass the title in here as it may have been generated by autoTitle on the server.
return this.processCSFFileWithCache(moduleExports, importPath, title);
}
loadAllCSFFiles({ batchSize = EXTRACT_BATCH_SIZE } = {}): Promise<
async loadAllCSFFiles({ batchSize = EXTRACT_BATCH_SIZE } = {}): Promise<
StoryStore<TRenderer>['cachedCSFFiles']
> {
if (!this.storyIndex) throw new Error(`loadAllCSFFiles called before initialization`);
@ -163,41 +157,33 @@ export class StoryStore<TRenderer extends Renderer> {
storyId,
]);
const loadInBatches = (
const loadInBatches = async (
remainingImportPaths: typeof importPaths
): Promise<{ importPath: Path; csfFile: CSFFile<TRenderer> }[]> => {
if (remainingImportPaths.length === 0) return SynchronousPromise.resolve([]);
if (remainingImportPaths.length === 0) return Promise.resolve([]);
const csfFilePromiseList = remainingImportPaths
.slice(0, batchSize)
.map(([importPath, storyId]) =>
this.loadCSFFileByStoryId(storyId).then((csfFile) => ({
importPath,
csfFile,
}))
);
.map(async ([importPath, storyId]) => ({
importPath,
csfFile: await this.loadCSFFileByStoryId(storyId),
}));
return SynchronousPromise.all(csfFilePromiseList).then((firstResults) =>
loadInBatches(remainingImportPaths.slice(batchSize)).then((restResults) =>
firstResults.concat(restResults)
)
);
const firstResults = await Promise.all(csfFilePromiseList);
const restResults = await loadInBatches(remainingImportPaths.slice(batchSize));
return firstResults.concat(restResults);
};
return loadInBatches(importPaths).then((list) =>
list.reduce((acc, { importPath, csfFile }) => {
acc[importPath] = csfFile;
return acc;
}, {} as Record<Path, CSFFile<TRenderer>>)
);
const list = await loadInBatches(importPaths);
return list.reduce((acc, { importPath, csfFile }) => {
acc[importPath] = csfFile;
return acc;
}, {} as Record<Path, CSFFile<TRenderer>>);
}
cacheAllCSFFiles(): Promise<void> {
return this.initializationPromise.then(() =>
this.loadAllCSFFiles().then((csfFiles) => {
this.cachedCSFFiles = csfFiles;
})
);
async cacheAllCSFFiles(): Promise<void> {
await this.initializationPromise;
this.cachedCSFFiles = await this.loadAllCSFFiles();
}
preparedMetaFromCSFFile({ csfFile }: { csfFile: CSFFile<TRenderer> }): PreparedMeta<TRenderer> {
@ -393,38 +379,6 @@ export class StoryStore<TRenderer extends Renderer> {
};
};
getSetIndexPayload(): API_PreparedStoryIndex {
if (!this.storyIndex) throw new Error('getSetIndexPayload called before initialization');
if (!this.cachedCSFFiles)
throw new Error('Cannot call getSetIndexPayload() unless you call cacheAllCSFFiles() first');
const { cachedCSFFiles } = this;
const stories = this.extract({ includeDocsOnly: true });
return {
v: 4,
entries: Object.fromEntries(
Object.entries(this.storyIndex.entries).map(([id, entry]) => [
id,
stories[id]
? {
...entry,
args: stories[id].initialArgs,
initialArgs: stories[id].initialArgs,
argTypes: stories[id].argTypes,
parameters: stories[id].parameters,
}
: {
...entry,
parameters: this.preparedMetaFromCSFFile({
csfFile: cachedCSFFiles[entry.importPath],
}).parameters,
},
])
),
};
}
raw(): BoundStory<TRenderer>[] {
return Object.values(this.extract())
.map(({ id }: { id: StoryId }) => this.fromId(id))

View File

@ -19,7 +19,6 @@ declare var IS_STORYBOOK: boolean;
// relevant framework instantiates them via `start.js`. The good news is this happens right away.
declare var __STORYBOOK_ADDONS_CHANNEL__: any;
declare var __STORYBOOK_ADDONS_PREVIEW: any;
declare var __STORYBOOK_CLIENT_API__: import('./modules/client-api/ClientApi').ClientApi<any>;
declare var __STORYBOOK_PREVIEW__: import('./modules/preview-web/PreviewWeb').PreviewWeb<any>;
declare var __STORYBOOK_STORY_STORE__: any;
declare var STORYBOOK_HOOKS_CONTEXT: any;

View File

@ -10,34 +10,14 @@ The preview's job is:
3. Render the current selection to the web view in either story or docs mode.
## V7 Store vs Legacy (V6)
The story store is designed to load stories 'on demand', and will operate in this fashion if the `storyStoreV7` feature is enabled.
However, for back-compat reasons, in v6 mode, we need to load all stories, synchronously on bootup, emitting the `SET_STORIES` event.
In V7 mode we do not emit that event, instead preferring the `STORY_PREPARED` event, with the data for the single story being rendered.
## Initialization
The preview is `initialized` in two ways.
### V7 Mode:
- `importFn` - is an async `import()` function
- `getProjectAnnotations` - is a simple function that evaluations `preview.js` and addon config files and combines them. If it errors, the Preview will show the error.
- No `getStoryIndex` function is passed, instead the preview creates a `StoryIndexClient` that pulls `stories.json` from node and watches the event stream for invalidation events.
### V6 Mode
- `importFn` - is a simulated `import()` function, that is synchronous, see `client-api` for details.
- `getProjectAnnotations` - also evaluates `preview.js` et al, but watches for calls to `setStories`, and passes them to the `ClientApi`
- `getStoryIndex` is a local function (that must be called _after_ `getProjectAnnotations`) that gets the list of stories added.
See `client-api` for more details on this process.
## Story Rendering and interruptions
The Preview is split into three parts responsible for state management:

View File

@ -313,9 +313,7 @@ describe('storybook-metadata', () => {
});
it('should return user specified features', async () => {
const features = {
storyStoreV7: true,
};
const features = {};
const result = await computeStorybookMetadata({
packageJson: packageJsonMock,

View File

@ -350,22 +350,6 @@ export interface StorybookConfigRaw {
staticDirs?: (DirectoryMapping | string)[];
logLevel?: string;
features?: {
/**
* Build stories.json automatically on start/build
*/
buildStoriesJson?: boolean;
/**
* Activate on demand story store
*/
storyStoreV7?: boolean;
/**
* Do not throw errors if using `.mdx` files in SSv7
* (for internal use in sandboxes)
*/
storyStoreV7MdxErrors?: boolean;
/**
* Filter args with a "target" on the type from the render function (EXPERIMENTAL)
*/

View File

@ -267,7 +267,7 @@
"built": false
}
},
"packageManager": "yarn@4.0.0",
"packageManager": "yarn@4.0.2",
"engines": {
"node": ">=18.0.0"
},

View File

@ -1,6 +1,6 @@
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/* eslint-disable react/no-unused-prop-types */
/* eslint-disable react/require-default-props */
import React from 'react';

View File

@ -2,8 +2,8 @@
import './globals';
export * from './public-api';
export * from './public-types';
export { setup } from './render';
// optimization: stop HMR propagation in webpack
try {

View File

@ -1 +0,0 @@
export { setup } from './render';

View File

@ -51,7 +51,7 @@
"@storybook/csf": "^0.1.2",
"@storybook/docs-tools": "workspace:*",
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"@storybook/manager-api": "workspace:*",
"@storybook/preview-api": "workspace:*",
"@storybook/theming": "workspace:*",
@ -60,7 +60,7 @@
"color-convert": "^2.0.1",
"dequal": "^2.0.2",
"lodash": "^4.17.21",
"markdown-to-jsx": "^7.1.8",
"markdown-to-jsx": "7.3.2",
"memoizerific": "^1.11.3",
"polished": "^4.2.2",
"react-colorful": "^5.1.2",

View File

@ -63,7 +63,7 @@
"@storybook/client-logger": "workspace:*",
"@storybook/csf": "^0.1.2",
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"@storybook/theming": "workspace:*",
"@storybook/types": "workspace:*",
"memoizerific": "^1.11.3",

View File

@ -79,7 +79,7 @@
"@storybook/components": "workspace:*",
"@storybook/core-events": "workspace:*",
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.1",
"@storybook/icons": "^1.2.3",
"@storybook/manager-api": "workspace:*",
"@storybook/router": "workspace:*",
"@storybook/test": "workspace:*",
@ -94,7 +94,7 @@
"fs-extra": "^11.1.0",
"fuse.js": "^3.6.1",
"lodash": "^4.17.21",
"markdown-to-jsx": "^7.1.8",
"markdown-to-jsx": "7.3.2",
"memoizerific": "^1.11.3",
"polished": "^4.2.2",
"qs": "^6.10.0",

View File

@ -18,8 +18,6 @@ import { FramesRenderer } from './FramesRenderer';
import type { PreviewProps } from './utils/types';
const { FEATURES } = global;
const getWrappers = (getFn: API['getElements']) => Object.values(getFn(types.PREVIEW));
const getTabs = (getFn: API['getElements']) => Object.values(getFn(types.TAB));
@ -160,7 +158,7 @@ const Canvas: FC<{ withLoader: boolean; baseUrl: string; children?: never }> = (
const [progress, setProgress] = useState(undefined);
useEffect(() => {
if (FEATURES?.storyStoreV7 && global.CONFIG_TYPE === 'DEVELOPMENT') {
if (global.CONFIG_TYPE === 'DEVELOPMENT') {
try {
const channel = addons.getServerChannel();

View File

@ -9,7 +9,7 @@ import { CHANNEL_CREATED } from '@storybook/core-events';
import Provider from './provider';
import { renderStorybookUI } from './index';
const { FEATURES, CONFIG_TYPE } = global;
const { CONFIG_TYPE } = global;
class ReactProvider extends Provider {
private addons: AddonStore;
@ -34,7 +34,7 @@ class ReactProvider extends Provider {
this.channel = channel;
global.__STORYBOOK_ADDONS_CHANNEL__ = channel;
if (FEATURES?.storyStoreV7 && CONFIG_TYPE === 'DEVELOPMENT') {
if (CONFIG_TYPE === 'DEVELOPMENT') {
this.serverChannel = this.channel;
addons.setServerChannel(this.serverChannel);
}

File diff suppressed because it is too large Load Diff

View File

@ -9,34 +9,13 @@ Type:
```ts
{
argTypeTargetsV7?: boolean;
buildStoriesJson?: boolean;
legacyDecoratorFileOrder?: boolean;
legacyMdx1?: boolean;
storyStoreV7?: boolean;
}
```
Enables Storybook's additional features.
## `buildStoriesJson`
Type: `boolean`
Default: `true`, when [`storyStoreV7`](#storystorev7) is `true`
Generates a `index.json` and `stories.json` files to help story loading with the on-demand mode.
<!-- prettier-ignore-start -->
<CodeSnippets
paths={[
'common/main-config-features-build-stories-json.js.mdx',
'common/main-config-features-build-stories-json.ts.mdx',
]}
/>
<!-- prettier-ignore-end -->
## `legacyDecoratorFileOrder`
Type: `boolean`
@ -54,25 +33,6 @@ Apply decorators from preview.js before decorators from addons or frameworks. [M
<!-- prettier-ignore-end -->
## `storyStoreV7`
Type: `boolean`
Default: `true`
Opts out of [on-demand story loading](#on-demand-story-loading); loads all stories at build time.
<!-- prettier-ignore-start -->
<CodeSnippets
paths={[
'common/main-config-features-story-store-v7.js.mdx',
'common/main-config-features-story-store-v7.ts.mdx',
]}
/>
<!-- prettier-ignore-end -->
## `argTypeTargetsV7`
(⚠️ **Experimental**)
@ -91,25 +51,3 @@ Filter args with a "target" on the type from the render function.
/>
<!-- prettier-ignore-end -->
## On-demand story loading
As your Storybook grows, it gets challenging to load all of your stories performantly, slowing down the loading times and yielding a large bundle. Out of the box, Storybook loads your stories on demand rather than during boot-up to improve the performance of your Storybook. If you need to load all of your stories during boot-up, you can disable this feature by setting the `storyStoreV7` feature flag to `false` in your configuration as follows:
<!-- prettier-ignore-start -->
<CodeSnippets
paths={[
'common/main-config-features-story-store-v7.js.mdx',
'common/main-config-features-story-store-v7.ts.mdx',
]}
/>
<!-- prettier-ignore-end -->
### Known limitations
Because of the way stories are currently indexed in Storybook, loading stories on demand with `storyStoreV7` has a couple of minor limitations at the moment:
- [CSF formats](../api/csf.md) from version 1 to version 3 are supported. The `storiesOf` construct is not.
- Custom [`storySort` functions](../writing-stories/naming-components-and-hierarchy.md#sorting-stories) receive more limited arguments.

View File

@ -117,7 +117,7 @@ When [auto-titling](../configure/sidebar-and-urls.md#csf-30-auto-titles), prefix
<Callout variant="info" icon="💡">
With [`storyStoreV7`](./main-config-features.md#storystorev7) (the default in Storybook 7), Storybook now statically analyzes the configuration file to improve performance. Loading stories with a custom implementation may de-optimize or break this ability.
💡 Storybook now statically analyzes the configuration file to improve performance. Loading stories with a custom implementation may de-optimize or break this ability.
</Callout>

View File

@ -10,7 +10,6 @@ By default, Storybook provides zero-config support for Webpack and automatically
| Option | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `storyStoreV7` | Enabled by default.<br/> Configures Webpack's [code splitting](https://webpack.js.org/guides/code-splitting/) feature<br/> `features: { storyStoreV7: false }` |
| `lazyCompilation` | Enables Webpack's experimental [`lazy compilation`](https://webpack.js.org/configuration/experiments/#experimentslazycompilation)<br/>`core: { builder: { options: { lazyCompilation: true } } }` |
| `fsCache` | Configures Webpack's filesystem [caching](https://webpack.js.org/configuration/cache/#cachetype) feature<br/> `core: { builder: { options: { fsCache: true } } }` |

View File

@ -40,7 +40,7 @@ This configuration file is a [preset](../addons/addon-types.md) and, as such, ha
| `framework` | Configures Storybook based on a set of [framework-specific](./frameworks.md) settings <br/> `framework: { name: '@storybook/svelte-vite', options:{} }` |
| `core` | Configures Storybook's [internal features](../api/main-config-core.md) <br/> `core: { disableTelemetry: true, }` |
| `docs` | Configures Storybook's [auto-generated documentation](../writing-docs/autodocs.md)<br/> `docs: { autodocs: 'tag' }` |
| `features` | Enables Storybook's [additional features](../api/main-config-features.md)<br/> See table below for a list of available features `features: { storyStoreV7: true }` |
| `features` | Enables Storybook's [additional features](../api/main-config-features.md)<br/> See table below for a list of available features |
| `refs` | Configures [Storybook composition](../sharing/storybook-composition.md) <br/> `refs:{ example: { title: 'ExampleStorybook', url:'https://your-url.com' } }` |
| `logLevel` | Configures Storybook's logs in the browser terminal. Useful for debugging <br/> `logLevel: 'debug'` |
| `webpackFinal` | Customize Storybook's [Webpack](../builders/webpack.md) setup <br/> `webpackFinal: async (config:any) => { return config; }` |
@ -48,15 +48,6 @@ This configuration file is a [preset](../addons/addon-types.md) and, as such, ha
| `env` | Defines custom Storybook [environment variables](./environment-variables.md#using-storybook-configuration). <br/> `env: (config) => ({...config, EXAMPLE_VAR: 'Example var' }),` |
| `build` | Optimizes Storybook's production [build](../api/main-config-build.md) for performance by excluding specific features from the bundle. Useful when decreased build times are a priority. <br/> `build: { test: {} }` |
### Feature flags
Additionally, you can also provide additional feature flags to your Storybook configuration. Below is an abridged list of available features that are currently available.
| Configuration element | Description |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `storyStoreV7` | Configures Storybook to load stories [on demand](#on-demand-story-loading), rather than during boot up (defaults to `true` as of `v7.0`) <br/> `features: { storyStoreV7: true }` |
| `buildStoriesJson` | Generates `index.json` and `stories.json` files to help story loading with the on-demand mode (defaults to `true` when `storyStoreV7` is `true`) <br/> `features: { buildStoriesJson: true }` |
## Configure story loading
By default, Storybook will load stories from your project based on a glob (pattern matching string) in `.storybook/main.js|ts` that matches all files in your project with extension `.stories.*`. The intention is for you to colocate a story file along with the component it documents.
@ -130,27 +121,12 @@ You can also adjust your Storybook configuration and implement custom logic to l
<!-- prettier-ignore-end -->
### On-demand story loading
As your Storybook grows, it gets challenging to load all of your stories performantly, slowing down the loading times and yielding a large bundle. Out of the box, Storybook loads your stories on demand rather than during boot-up to improve the performance of your Storybook. If you need to load all of your stories during boot-up, you can disable this feature by setting the `storyStoreV7` feature flag to `false` in your configuration as follows:
<!-- prettier-ignore-start -->
<CodeSnippets
paths={[
'common/main-config-features-story-store-v7.js.mdx',
'common/main-config-features-story-store-v7.ts.mdx',
]}
/>
<!-- prettier-ignore-end -->
#### Known limitations
Because of the way stories are currently indexed in Storybook, loading stories on demand with `storyStoreV7` has a couple of minor limitations at the moment:
Because of the way stories are currently indexed in Storybook, loading stories on demand has a couple of minor limitations at the moment:
- [CSF formats](../api/csf.md) from version 1 to version 3 are supported. The `storiesOf` construct is not.
- Custom`storySort` functions are allowed based on a restricted API.
- [CSF formats](../api/csf.md) from version 1 to version 3 are supported.
- Custom `storySort` functions are allowed based on a restricted API.
## Configure story rendering

View File

@ -35,7 +35,6 @@ Specifically, we track the following information in our telemetry events:
- Builder (e.g., Webpack5, Vite).
- Meta framework (e.g., [Next](https://nextjs.org/), [Gatsby](https://www.gatsbyjs.com/), [CRA](https://create-react-app.dev/)).
- [Addons](https://storybook.js.org/integrations) (e.g., [Essentials](../essentials/index.md), [Accessibility](https://storybook.js.org/addons/@storybook/addon-a11y/)).
- [Feature flags](./index.md#feature-flags) (e.g., `buildStoriesJson`).
- Package manager information (e.g., `npm`, `yarn`).
- Monorepo information (e.g., [NX](https://nx.dev/), [Turborepo](https://turborepo.org/)).
- In-app events (e.g., [Storybook guided tour](https://github.com/storybookjs/addon-onboarding)).
@ -92,9 +91,6 @@ Will generate the following output:
"version": "3.1.1"
},
"monorepo": "Nx",
"features": {
"buildStoriesJson": true
},
"framework": {
"name": "@storybook/react-vite",
"options": {}

View File

@ -51,20 +51,6 @@ yarn storybook dev --debug-webpack
yarn storybook build --debug-webpack
```
### Bundle splitting
Starting with Storybook 6.4, [bundle splitting](https://v4.webpack.js.org/guides/code-splitting/) is supported through a configuration flag. Update your Storybook configuration and add the `storyStoreV7` flag:
<!-- prettier-ignore-start -->
<CodeSnippets
paths={[
'common/main-config-features-story-store-v7.js.mdx',
]}
/>
<!-- prettier-ignore-end -->
When you start your Storybook, you'll see an improvement in loading times. Read more about it in the [announcement post](https://storybook.js.org/blog/storybook-on-demand-architecture/) and the [configuration documentation](./index.md#on-demand-story-loading).
### Webpack 5

View File

@ -94,27 +94,6 @@ This shows the errors visually in your editor, which speeds things up a lot. Her
![MDX errors showing in VS Code](./assets/mdx-vs-code-extension-errors.gif)
### storiesOf support discontinued by default
If you use Storybooks legacy `storiesOf` API, it is no longer supported by default in Storybook 7.
We recommend you upgrade your `storiesOf` stories to [Component Story Format (CSF)](https://storybook.js.org/blog/storybook-csf3-is-here/). To do so, please see our [optional migration instructions below](#storiesof-to-csf).
If you cant upgrade to CSF, or want to get your project working with Storybook 7 before putting in the time to upgrade, you can opt out of on-demand story loading. This legacy mode has a variety of performance implications, but is a convenient stop-gap solution.
To opt out, add the `storyStoreV7` feature flag in `.storybook/main.js`:
<!-- prettier-ignore-start -->
<CodeSnippets
paths={[
'common/main-config-features-story-store-v7.js.mdx',
'common/main-config-features-story-store-v7.ts.mdx'
]}
/>
<!-- prettier-ignore-end -->
For more information on this change, see the [migration notes](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#storystorev7-enabled-by-default).
## Troubleshooting

View File

@ -74,27 +74,6 @@ Similar to other fields available in Storybooks configuration file, the `refs
</Callout>
## Improve your Storybook composition
Out of the box, Storybook allows you to compose Storybooks both locally and remotely with a minor change to your configuration. However, as your Storybook grows, you might want to optimize the composition process to improve the overall performance and user experience of your Storybook by enabling the `buildStoriesJson` feature flag that will generate the `index.json` and `stories.json` files with the required information to populate the UI with your composed Storybook stories automatically. For example:
<!-- prettier-ignore-start -->
<CodeSnippets
paths={[
'common/main-config-features-build-stories-json.js.mdx',
'common/main-config-features-build-stories-json.ts.mdx',
]}
/>
<!-- prettier-ignore-end -->
<Callout variant="info">
If you're working with a Storybook version 7.0 or higher, this flag is enabled by default. However, if you're working with an older version and you configured your Storybook to use the [`storyStoreV7`](../api/main-config-features.md#storystorev7) feature flag, you won't need this flag as it will automatically generate the required `index.json` file for you to use.
</Callout>
## Troubleshooting
### Storybook composition is not working with my project

View File

@ -1,12 +0,0 @@
```js
// .storybook/main.js
export default {
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
features: {
buildStoriesJson: true,
},
};
```

View File

@ -1,16 +0,0 @@
```ts
// .storybook/main.ts
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
features: {
buildStoriesJson: true,
},
};
export default config;
```

View File

@ -1,12 +0,0 @@
```js
// .storybook/main.js
export default {
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
features: {
storyStoreV7: false, // 👈 Opt out of on-demand story loading
},
};
```

View File

@ -1,16 +0,0 @@
```ts
// .storybook/main.ts
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
features: {
storyStoreV7: false, // 👈 Opt out of on-demand story loading
},
};
export default config;
```

View File

@ -1,6 +1,4 @@
```ts
// codegen-iframe-script.ts
import { virtualPreviewFile, virtualStoriesFile } from './virtual-file-names';
import { transformAbsPath } from './utils/transform-abs-path';
import type { ExtendedOptions } from './types';

View File

@ -5,9 +5,6 @@ export default {
// Replace your-framework with the framework you are using (e.g., react-webpack5, vue3-vite)
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
features: {
storyStoreV7: false,
},
core: {
builder: {
name: '@storybook/builder-webpack5',

View File

@ -7,9 +7,6 @@ import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
features: {
storyStoreV7: false,
},
core: {
builder: {
name: '@storybook/builder-webpack5',

View File

@ -12,5 +12,5 @@
"test": "cd code; yarn test",
"upload-bench": "cd scripts; yarn upload-bench"
},
"packageManager": "yarn@4.0.0"
"packageManager": "yarn@4.0.2"
}

View File

@ -21,7 +21,7 @@ plugins:
unsafeHttpWhitelist:
- localhost
yarnPath: ../.yarn/releases/yarn-4.0.0.cjs
yarnPath: ../.yarn/releases/yarn-4.0.2.cjs
installStatePath: '../.yarn/scripts-install-state.gz'
# Sometimes you get a "The remote archive doesn't match the expected checksum" error, uncommenting this line will fix it
# checksumBehavior: 'update'

View File

@ -61,27 +61,6 @@ async function run() {
assert.equal(bootEvent.payload?.eventType, eventType);
});
// Test only StoryStoreV7 projects, as ssv6 does not support the storyIndex
if (template.modifications?.mainConfig?.features?.storyStoreV7 !== false) {
const { exampleStoryCount, exampleDocsCount } = mainEvent.payload?.storyIndex || {};
if (['build', 'dev'].includes(eventType)) {
test(`${eventType} event should contain 8 stories and 3 docs entries`, () => {
assert.equal(
exampleStoryCount,
8,
`Expected 8 stories but received ${exampleStoryCount} instead.`
);
const expectedDocsCount =
template.modifications?.disableDocs || template.modifications?.testBuild ? 0 : 3;
assert.equal(
exampleDocsCount,
expectedDocsCount,
`Expected ${expectedDocsCount} docs entries but received ${exampleDocsCount} instead.`
);
});
}
}
test(`main event should be ${eventType} and contain correct id and session id`, () => {
assert.equal(mainEvent.eventType, eventType);
assert.notEqual(mainEvent.eventId, bootEvent.eventId);

View File

@ -199,7 +199,7 @@
"verdaccio": "^5.19.1",
"verdaccio-auth-memory": "^10.2.0"
},
"packageManager": "yarn@4.0.0",
"packageManager": "yarn@4.0.2",
"engines": {
"node": ">=18.0.0"
}

View File

@ -10,7 +10,7 @@ export const testRunnerBuild: Task & { port: number } = {
async ready() {
return false;
},
async run({ sandboxDir, junitFilename, template }, { dryRun, debug }) {
async run({ sandboxDir, junitFilename }, { dryRun, debug }) {
const execOptions = { cwd: sandboxDir };
const flags = [
`--url http://localhost:${this.port}`,
@ -20,11 +20,6 @@ export const testRunnerBuild: Task & { port: number } = {
'--skipTags="test-skip"',
];
// index-json mode is only supported in ssv7
if (template.modifications?.mainConfig?.features?.storyStoreV7 !== false) {
flags.push('--index-json');
}
await exec(
`yarn test-storybook ${flags.join(' ')}`,
{

View File

@ -45,7 +45,6 @@ module.exports = {
staticDirs: ['../ember-output'],
features: {
buildStoriesJson: false,
storyStoreV7: false,
},
framework: { name: '@storybook/ember' },
};

View File

@ -20,8 +20,6 @@ const config = {
channelOptions: { allowFunction: false, maxDepth: 10 },
},
features: {
storyStoreV7: !global.navigator?.userAgent?.match?.('jsdom'),
buildStoriesJson: true,
warnOnLegacyHierarchySeparator: false,
previewMdx2: true,
},

View File

@ -15,9 +15,7 @@ const mainConfig: StorybookConfig = {
core: {
disableTelemetry: true,
},
features: {
storyStoreV7: false,
},
features: {},
framework: '@storybook/server-webpack5',
};