Merge branch 'next' into docs_adds_doctor

This commit is contained in:
jonniebigodes 2024-01-13 14:19:28 +00:00 committed by GitHub
commit 6376e9d809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
194 changed files with 1661 additions and 1865 deletions

4
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,4 @@
34e364a0ca1d93555d36a7367d78e8e229493de8
c0896915fb7fb9a8dd416b9aebca17abd909d1c1
a41c227037e7e7249b8b376f838f4f8bcc3e3e59
13c46e6c0b7f3dd8cf4ba42d1cfd6714f4777d54

View File

@ -1,3 +1,8 @@
## 7.6.8
- Addon-actions: Fix module resolution for react-native - [#25296](https://github.com/storybookjs/storybook/pull/25296), thanks [@dannyhw](https://github.com/dannyhw)!
- Storysource: Fix import error - [#25391](https://github.com/storybookjs/storybook/pull/25391), thanks [@unional](https://github.com/unional)!
## 7.6.7
- Core: Skip no-framework error when ignorePreview=true - [#25286](https://github.com/storybookjs/storybook/pull/25286), thanks [@ndelangen](https://github.com/ndelangen)!

View File

@ -59,6 +59,8 @@
- [Description Doc block properties](#description-doc-block-properties)
- [Story Doc block properties](#story-doc-block-properties)
- [Manager API expandAll and collapseAll methods](#manager-api-expandall-and-collapseall-methods)
- [Source Doc block properties](#source-doc-block-properties)
- [Canvas Doc block properties](#canvas-doc-block-properties)
- [`Primary` Doc block properties](#primary-doc-block-properties)
- [`createChannel` from `@storybook/postmessage` and `@storybook/channel-websocket`](#createchannel-from-storybookpostmessage-and--storybookchannel-websocket)
- [From version 7.5.0 to 7.6.0](#from-version-750-to-760)
@ -950,6 +952,22 @@ api.collapseAll() // becomes api.emit(STORIES_COLLAPSE_ALL)
api.expandAll() // becomes api.emit(STORIES_EXPAND_ALL)
```
#### Source Doc block properties
`id` and `ids` are now removed in favor of the `of` property. [More info](#doc-blocks).
#### Canvas Doc block properties
The following properties were removed from the Canvas Doc block:
- children
- isColumn
- columns
- withSource
- mdxSource
[More info](#doc-blocks).
#### `Primary` Doc block properties
The `name` prop is now removed in favor of the `of` property. [More info](#doc-blocks).

View File

@ -27,6 +27,7 @@ module.exports = {
'jest/no-standalone-expect': 'off',
'jest/no-done-callback': 'off',
'jest/no-deprecated-functions': 'off',
'jest/valid-expect': 'off',
'eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
'eslint-comments/no-unused-disable': 'error',

View File

@ -1,8 +1,8 @@
diff --git a/dist/index.js b/dist/index.js
index 5a61947ad50426d27390b4e82533179323ad3ba1..32bfc45909b645cb31cec2e204c8baa23f21fdd2 100644
index 974d6b26f626024fc9904908100c9ecaa54f43e1..5be2d35267e7f0525c6588758dbebe72599f88a9 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -6,23 +6,29 @@ import { processError } from '@vitest/utils/error';
@@ -6,31 +6,37 @@ import { processError } from '@vitest/utils/error';
import { util } from 'chai';
const MATCHERS_OBJECT = Symbol.for("matchers-object");
@ -11,15 +11,19 @@ index 5a61947ad50426d27390b4e82533179323ad3ba1..32bfc45909b645cb31cec2e204c8baa2
+// Otherwise, vitest will override global jest matchers, and crash.
+const JEST_MATCHERS_OBJECT = Symbol.for("$$jest-matchers-object-storybook");
const GLOBAL_EXPECT = Symbol.for("expect-global");
const ASYMMETRIC_MATCHERS_OBJECT = Symbol.for("asymmetric-matchers-object");
if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) {
const globalState = /* @__PURE__ */ new WeakMap();
- const matchers = /* @__PURE__ */ Object.create(null);
const assymetricMatchers = /* @__PURE__ */ Object.create(null);
Object.defineProperty(globalThis, MATCHERS_OBJECT, {
get: () => globalState
});
+ Object.defineProperty(globalThis, ASYMMETRIC_MATCHERS_OBJECT, {
+ get: () => assymetricMatchers
+ });
+}
+
+if (!Object.prototype.hasOwnProperty.call(globalThis, JEST_MATCHERS_OBJECT)) {
+ const matchers = /* @__PURE__ */ Object.create(null);
Object.defineProperty(globalThis, JEST_MATCHERS_OBJECT, {
@ -30,8 +34,15 @@ index 5a61947ad50426d27390b4e82533179323ad3ba1..32bfc45909b645cb31cec2e204c8baa2
matchers
})
});
- Object.defineProperty(globalThis, ASYMMETRIC_MATCHERS_OBJECT, {
- get: () => assymetricMatchers
- });
}
+
function getState(expect) {
return globalThis[MATCHERS_OBJECT].get(expect);
}
+
function setState(state, expect) {
const map = globalThis[MATCHERS_OBJECT];
const current = map.get(expect) || {};

View File

@ -31,8 +31,8 @@ function areAllRequiredElementsHighlighted(
return highlightedCount === 0
? CheckBoxStates.UNCHECKED
: highlightedCount === elementsToHighlight.length
? CheckBoxStates.CHECKED
: CheckBoxStates.INDETERMINATE;
? CheckBoxStates.CHECKED
: CheckBoxStates.INDETERMINATE;
}
const HighlightToggle: React.FC<ToggleProps> = ({ toggleId, elementsToHighlight = [] }) => {

View File

@ -40,6 +40,7 @@
},
"main": "dist/index.js",
"module": "dist/index.mjs",
"react-native": "dist/index.mjs",
"types": "dist/index.d.ts",
"typesVersions": {
"*": {

View File

@ -37,40 +37,24 @@ const createBackgroundSelectorItem = memoize(1000)(
})
);
const getDisplayedItems = memoize(10)(
(
backgrounds: Background[],
selectedBackgroundColor: string | null,
change: (arg: { selected: string; name: string }) => void
) => {
const backgroundSelectorItems = backgrounds.map(({ name, value }) =>
createBackgroundSelectorItem(
null,
name,
value,
true,
change,
value === selectedBackgroundColor
)
);
const getDisplayedItems = memoize(10)((
backgrounds: Background[],
selectedBackgroundColor: string | null,
change: (arg: { selected: string; name: string }) => void
) => {
const backgroundSelectorItems = backgrounds.map(({ name, value }) =>
createBackgroundSelectorItem(null, name, value, true, change, value === selectedBackgroundColor)
);
if (selectedBackgroundColor !== 'transparent') {
return [
createBackgroundSelectorItem(
'reset',
'Clear background',
'transparent',
null,
change,
false
),
...backgroundSelectorItems,
];
}
return backgroundSelectorItems;
if (selectedBackgroundColor !== 'transparent') {
return [
createBackgroundSelectorItem('reset', 'Clear background', 'transparent', null, change, false),
...backgroundSelectorItems,
];
}
);
return backgroundSelectorItems;
});
const DEFAULT_BACKGROUNDS_CONFIG: BackgroundsParameter = {
default: null,

View File

@ -1,8 +1,27 @@
import type { PreparedStory } from '@storybook/types';
import { global } from '@storybook/global';
const excludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce((acc, entry) => {
const [tag, option] = entry;
if ((option as any).excludeFromDocsStories) {
acc[tag] = true;
}
return acc;
}, {} as Record<string, boolean>);
export const parameters: any = {
docs: {
renderer: async () => {
const { DocsRenderer } = (await import('./DocsRenderer')) as any;
return new DocsRenderer();
},
stories: {
filter: (story: PreparedStory) => {
const tags = story.tags || [];
return (
tags.filter((tag) => excludeTags[tag]).length === 0 && !story.parameters.docs?.disable
);
},
},
},
};

View File

@ -11,3 +11,5 @@ declare module 'sveltedoc-parser' {
declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features'];
declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined;
declare var TAGS_OPTIONS: import('@storybook/types').TagsOptions;

View File

@ -35,21 +35,24 @@ const responsiveViewport: ViewportItem = {
const baseViewports: ViewportItem[] = [responsiveViewport];
const toLinks = memoize(50)(
(list: ViewportItem[], active: LinkBase, updateGlobals, close): Link[] => {
return list
.filter((i) => i.id !== responsiveViewport.id || active.id !== i.id)
.map((i) => {
return {
...i,
onClick: () => {
updateGlobals({ viewport: i.id });
close();
},
};
});
}
);
const toLinks = memoize(50)((
list: ViewportItem[],
active: LinkBase,
updateGlobals,
close
): Link[] => {
return list
.filter((i) => i.id !== responsiveViewport.id || active.id !== i.id)
.map((i) => {
return {
...i,
onClick: () => {
updateGlobals({ viewport: i.id });
close();
},
};
});
});
interface LinkBase {
id: string;

View File

@ -138,6 +138,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({
title,
logLevel,
docsOptions,
tagsOptions,
} = await getData(options);
yield;
@ -175,6 +176,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({
refs,
logLevel,
docsOptions,
tagsOptions,
options
);
@ -222,6 +224,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime,
title,
logLevel,
docsOptions,
tagsOptions,
} = await getData(options);
yield;
@ -262,6 +265,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime,
refs,
logLevel,
docsOptions,
tagsOptions,
options
);

View File

@ -14,6 +14,7 @@ export const getData = async (options: Options) => {
const logLevel = options.presets.apply<string>('logLevel');
const title = options.presets.apply<string>('title');
const docsOptions = options.presets.apply('docs', {});
const tagsOptions = options.presets.apply('tags', {});
const template = readTemplate('template.ejs');
const customHead = options.presets.apply<string>('managerHead');
@ -35,5 +36,6 @@ export const getData = async (options: Options) => {
config,
logLevel,
favicon,
tagsOptions,
};
};

View File

@ -3,7 +3,7 @@ import fs from 'fs-extra';
import { render } from 'ejs';
import type { DocsOptions, Options, Ref } from '@storybook/types';
import type { DocsOptions, TagsOptions, Options, Ref } from '@storybook/types';
export const getTemplatePath = async (template: string) => {
return join(
@ -34,6 +34,7 @@ export const renderHTML = async (
refs: Promise<Record<string, Ref>>,
logLevel: Promise<string>,
docsOptions: Promise<DocsOptions>,
tagsOptions: Promise<TagsOptions>,
{ versionCheck, previewUrl, configType, ignorePreview }: Options
) => {
const titleRef = await title;
@ -52,6 +53,7 @@ export const renderHTML = async (
// These two need to be double stringified because the UI expects a string
VERSIONCHECK: JSON.stringify(JSON.stringify(versionCheck), null, 2),
PREVIEW_URL: JSON.stringify(previewUrl, null, 2), // global preview URL
TAGS_OPTIONS: JSON.stringify(await tagsOptions, null, 2),
},
head: (await customHead) || '',
ignorePreview,

View File

@ -21,6 +21,7 @@
window.FEATURES = '[FEATURES HERE]';
window.STORIES = '[STORIES HERE]';
window.DOCS_OPTIONS = '[DOCS_OPTIONS HERE]';
window.TAGS_OPTIONS = '[TAGS_OPTIONS HERE]';
('OTHER_GLOBLALS HERE');

View File

@ -1,5 +1,5 @@
import { normalizeStories } from '@storybook/core-common';
import type { DocsOptions, Options } from '@storybook/types';
import type { DocsOptions, TagsOptions, Options } from '@storybook/types';
export type PreviewHtml = string | undefined;
@ -11,6 +11,7 @@ export async function transformIframeHtml(html: string, options: Options) {
const bodyHtmlSnippet = await presets.apply<PreviewHtml>('previewBody');
const logLevel = await presets.apply('logLevel', undefined);
const docsOptions = await presets.apply<DocsOptions>('docs');
const tagsOptions = await presets.apply<TagsOptions>('tags');
const coreOptions = await presets.apply('core');
const stories = normalizeStories(await options.presets.apply('stories', [], options), {
@ -42,6 +43,7 @@ export async function transformIframeHtml(html: string, options: Options) {
.replace(`'[FEATURES HERE]'`, JSON.stringify(features || {}))
.replace(`'[STORIES HERE]'`, JSON.stringify(stories || {}))
.replace(`'[DOCS_OPTIONS HERE]'`, JSON.stringify(docsOptions || {}))
.replace(`'[TAGS_OPTIONS HERE]'`, JSON.stringify(tagsOptions || {}))
.replace('<!-- [HEAD HTML SNIPPET HERE] -->', headHtmlSnippet || '')
.replace('<!-- [BODY HTML SNIPPET HERE] -->', bodyHtmlSnippet || '');
}

View File

@ -25,7 +25,7 @@ const dummyOptions: Options = {
builder: {},
},
options: {},
}[key]),
})[key],
} as Presets,
presetsList: [],
};

View File

@ -82,6 +82,7 @@ export default async (
nonNormalizedStories,
modulesCount = 1000,
build,
tagsOptions,
] = await Promise.all([
presets.apply('core'),
presets.apply('frameworkOptions'),
@ -95,6 +96,7 @@ export default async (
presets.apply('stories', []),
options.cache?.get('modulesCount').catch(() => {}),
options.presets.apply('build'),
presets.apply('tags', {}),
]);
const stories = normalizeStories(nonNormalizedStories, {
@ -129,9 +131,8 @@ export default async (
externals['@storybook/blocks'] = '__STORYBOOK_BLOCKS_EMPTY_MODULE__';
}
const { virtualModules: virtualModuleMapping, entries: dynamicEntries } = await getVirtualModules(
options
);
const { virtualModules: virtualModuleMapping, entries: dynamicEntries } =
await getVirtualModules(options);
return {
name: 'preview',
@ -185,6 +186,7 @@ export default async (
importPathMatcher: specifier.importPathMatcher.source,
})),
DOCS_OPTIONS: docsOptions,
TAGS_OPTIONS: tagsOptions,
...(build?.test?.disableBlocks ? { __STORYBOOK_BLOCKS_EMPTY_MODULE__: {} } : {}),
},
headHtmlSnippet,
@ -215,6 +217,7 @@ export default async (
rules: [
{
test: /\.stories\.([tj])sx?$|(stories|story)\.mdx$/,
exclude: /node_modules/,
enforce: 'post',
use: [
{

View File

@ -0,0 +1,62 @@
import { test, expect } from '@playwright/test';
import { SbPage } from './util';
const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001';
test.describe('tags', () => {
test.beforeEach(async ({ page }) => {
await page.goto(storybookUrl);
await new SbPage(page).waitUntilLoaded();
});
test('should correctly filter dev-only, docs-only, test-only stories', async ({ page }) => {
const sbPage = new SbPage(page);
await sbPage.navigateToStory('lib/preview-api/tags', 'docs');
// Sidebar should include dev-only and exclude docs-only and test-only
const devOnlyEntry = await page.locator('#lib-preview-api-tags--dev-only').all();
expect(devOnlyEntry.length).toBe(1);
const docsOnlyEntry = await page.locator('#lib-preview-api-tags--docs-only').all();
expect(docsOnlyEntry.length).toBe(0);
const testOnlyEntry = await page.locator('#lib-preview-api-tags--test-only').all();
expect(testOnlyEntry.length).toBe(0);
// Autodocs should include docs-only and exclude dev-only and test-only
const root = sbPage.previewRoot();
const devOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--dev-only').all();
expect(devOnlyAnchor.length).toBe(0);
const docsOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--docs-only').all();
expect(docsOnlyAnchor.length).toBe(1);
const testOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--test-only').all();
expect(testOnlyAnchor.length).toBe(0);
});
test('should correctly filter out test-only autodocs pages', async ({ page }) => {
const sbPage = new SbPage(page);
await sbPage.selectToolbar('#lib-preview-api');
// Sidebar should exclude test-only stories and their docs
const componentEntry = await page.locator('#lib-preview-api-test-only-tag').all();
expect(componentEntry.length).toBe(0);
// Even though test-only autodocs not sidebar, it is still in the preview
// Even though the test-only story is filtered out of the stories, it is still the primary story (should it be?)
await sbPage.deepLinkToStory(storybookUrl, 'lib/preview-api/test-only-tag', 'docs');
await sbPage.waitUntilLoaded();
const docsButton = await sbPage.previewRoot().locator('button', { hasText: 'Button' });
await expect(docsButton).toBeVisible();
// Even though test-only story not sidebar, it is still in the preview
await sbPage.deepLinkToStory(storybookUrl, 'lib/preview-api/test-only-tag', 'default');
await sbPage.waitUntilLoaded();
const storyButton = await sbPage.previewRoot().locator('button', { hasText: 'Button' });
await expect(storyButton).toBeVisible();
});
});

View File

@ -33,6 +33,9 @@ export class SbPage {
const storyLinkId = `${titleId}--${storyId}`;
const viewMode = name === 'docs' ? 'docs' : 'story';
await this.page.goto(`${baseURL}/?path=/${viewMode}/${storyLinkId}`);
await this.page.waitForURL((url) => url.search.includes(`path=/${viewMode}/${storyLinkId}`));
await this.previewRoot();
}
/**

View File

@ -172,7 +172,7 @@ const buildTemplate = (
const firstSelector = selector.split(',')[0];
const templateReplacers: [
string | RegExp,
string | ((substring: string, ...args: any[]) => string)
string | ((substring: string, ...args: any[]) => string),
][] = [
[/(^.*?)(?=[,])/, '$1'],
[/(^\..+)/, 'div$1'],

View File

@ -41,7 +41,10 @@ export class PropertyExtractor implements NgModuleMetadata {
applicationProviders?: Array<Provider | ReturnType<typeof importProvidersFrom>>;
/* eslint-enable @typescript-eslint/lines-between-class-members */
constructor(private metadata: NgModuleMetadata, private component?: any) {
constructor(
private metadata: NgModuleMetadata,
private component?: any
) {
this.init();
}

View File

@ -11,7 +11,10 @@ export class AttributeSelectorComponent {
selectors!: string;
constructor(public el: ElementRef, private resolver: ComponentFactoryResolver) {
constructor(
public el: ElementRef,
private resolver: ComponentFactoryResolver
) {
const factory = this.resolver.resolveComponentFactory(AttributeSelectorComponent);
this.selectors = factory.selector;
this.generatedTemplate = el.nativeElement.outerHTML;

View File

@ -11,7 +11,10 @@ export class ClassSelectorComponent {
selectors!: string;
constructor(public el: ElementRef, private resolver: ComponentFactoryResolver) {
constructor(
public el: ElementRef,
private resolver: ComponentFactoryResolver
) {
const factory = this.resolver.resolveComponentFactory(ClassSelectorComponent);
this.selectors = factory.selector;
this.generatedTemplate = el.nativeElement.outerHTML;

View File

@ -11,7 +11,10 @@ export class MultipleSelectorComponent {
selectors!: string;
constructor(public el: ElementRef, private resolver: ComponentFactoryResolver) {
constructor(
public el: ElementRef,
private resolver: ComponentFactoryResolver
) {
const factory = this.resolver.resolveComponentFactory(MultipleClassSelectorComponent);
this.selectors = factory.selector;
this.generatedTemplate = el.nativeElement.outerHTML;
@ -29,7 +32,10 @@ export class MultipleClassSelectorComponent {
selectors!: string;
constructor(public el: ElementRef, private resolver: ComponentFactoryResolver) {
constructor(
public el: ElementRef,
private resolver: ComponentFactoryResolver
) {
const factory = this.resolver.resolveComponentFactory(MultipleClassSelectorComponent);
this.selectors = factory.selector;
this.generatedTemplate = el.nativeElement.outerHTML;

View File

@ -99,7 +99,7 @@ npx storybook@latest init
This framework is designed to work with Storybook 7. If youre not already using v7, upgrade with this command:
```bash
npx storybook@latest upgrade --prerelease
npx storybook@latest upgrade
```
#### Automatic migration

View File

@ -78,9 +78,9 @@ export default function jsxPragma({ types: t }: { types: typeof BabelTypes }): P
? // import { $import as _pragma } from '$module'
t.importSpecifier(importAs, t.identifier(state.opts.import))
: state.opts.importNamespace
? t.importNamespaceSpecifier(importAs)
: // import _pragma from '$module'
t.importDefaultSpecifier(importAs),
? t.importNamespaceSpecifier(importAs)
: // import _pragma from '$module'
t.importDefaultSpecifier(importAs),
],
t.stringLiteral(state.opts.module || 'react')
);

View File

@ -198,10 +198,10 @@ export default function nextTransformSsg({
p.node.type === 'ObjectProperty'
? 'value'
: p.node.type === 'RestElement'
? 'argument'
: (function () {
throw new Error('invariant');
})()
? 'argument'
: (function () {
throw new Error('invariant');
})()
) as NodePath<BabelTypes.Identifier>;
if (isIdentifierReferenced(local)) {
variableState.refs.add(local);
@ -360,10 +360,10 @@ export default function nextTransformSsg({
p.node.type === 'ObjectProperty'
? 'value'
: p.node.type === 'RestElement'
? 'argument'
: (function () {
throw new Error('invariant');
})()
? 'argument'
: (function () {
throw new Error('invariant');
})()
) as NodePath<BabelTypes.Identifier>;
if (refs.has(local) && !isIdentifierReferenced(local)) {

View File

@ -22,7 +22,7 @@ npx storybook@latest init
This framework is designed to work with Storybook 7. If youre not already using v7, upgrade with this command:
```bash
npx storybook@latest upgrade --prerelease
npx storybook@latest upgrade
```
#### Manual migration

View File

@ -17,7 +17,6 @@ Check out our [Frameworks API](https://storybook.js.org/blog/framework-api/) ann
- [Mocking links](#mocking-links)
- [Troubleshooting](#troubleshooting)
- [Error: `ERR! SyntaxError: Identifier '__esbuild_register_import_meta_url__' has already been declared` when starting Storybook](#error-err-syntaxerror-identifier-__esbuild_register_import_meta_url__-has-already-been-declared-when-starting-storybook)
- [Error: `Cannot read properties of undefined (reading 'disable_scroll_handling')` in preview](#error-cannot-read-properties-of-undefined-reading-disable_scroll_handling-in-preview)
- [Acknowledgements](#acknowledgements)
## Supported features
@ -64,7 +63,7 @@ npx storybook@latest init
This framework is designed to work with Storybook 7. If youre not already using v7, upgrade with this command:
```bash
npx storybook@latest upgrade --prerelease
npx storybook@latest upgrade
```
#### Automatic migration

View File

@ -85,7 +85,7 @@
"jscodeshift": "^0.15.1",
"leven": "^3.1.0",
"ora": "^5.4.1",
"prettier": "^2.8.0",
"prettier": "^3.1.1",
"prompts": "^2.4.0",
"read-pkg-up": "^7.0.1",
"semver": "^7.3.7",

View File

@ -104,7 +104,7 @@ describe('is not Nx project', () => {
() =>
({
hasStorybookBuilder: true,
} as any)
}) as any
);
});
@ -129,7 +129,7 @@ describe('is not Nx project', () => {
project1: { root: 'project1', architect: {} },
},
rootProject: 'project1',
} as any)
}) as any
);
});
@ -155,7 +155,7 @@ describe('is not Nx project', () => {
project2: { root: 'project2', architect: {} },
},
rootProject: null,
} as any)
}) as any
);
});

View File

@ -107,7 +107,7 @@ describe('is not Nx project', () => {
() =>
({
hasStorybookBuilder: true,
} as any)
}) as any
);
});
@ -133,7 +133,7 @@ describe('is not Nx project', () => {
project2: { root: 'project2', architect: {} },
},
rootProject: null,
} as any)
}) as any
);
});
@ -158,7 +158,7 @@ describe('is not Nx project', () => {
project1: { root: 'project1', architect: {} },
},
rootProject: 'project1',
} as any)
}) as any
);
});

View File

@ -6,6 +6,7 @@ import { writeConfig } from '@storybook/csf-tools';
import type { Fix } from '../types';
import type { PackageJson } from '../../js-package-manager';
import { updateMainConfig } from '../helpers/mainConfigFile';
import { getStorybookVersionSpecifier } from '../../helpers';
const logger = console;
@ -68,8 +69,11 @@ export const builderVite: Fix<BuilderViteOptions> = {
logger.info(`✅ Adding '@storybook/builder-vite' as dev dependency`);
if (!dryRun) {
const versionToInstall = getStorybookVersionSpecifier(
await packageManager.retrievePackageJson()
);
await packageManager.addDependencies({ installAsDevDependencies: true }, [
'@storybook/builder-vite',
`@storybook/builder-vite@${versionToInstall}`,
]);
}

View File

@ -218,10 +218,10 @@ export const newFrameworks: Fix<NewFrameworkRunOptions> = {
Your project should be upgraded to use the framework package ${chalk.bold(
newFrameworkPackage
)}, but we detected that you are using Vite ${chalk.bold(
viteVersion
)}, which is unsupported in ${chalk.bold(
'Storybook 7.0'
)}. Please upgrade Vite to ${chalk.bold('3.0.0 or higher')} and rerun this migration.
viteVersion
)}, which is unsupported in ${chalk.bold(
'Storybook 7.0'
)}. Please upgrade Vite to ${chalk.bold('3.0.0 or higher')} and rerun this migration.
`);
}
@ -351,8 +351,8 @@ export const newFrameworks: Fix<NewFrameworkRunOptions> = {
This migration is set to update your project to use the ${chalk.magenta(
'@storybook/react-vite'
)} framework, but Storybook provides a framework package specifically for Next.js projects: ${chalk.magenta(
'@storybook/nextjs'
)}.
'@storybook/nextjs'
)}.
This package provides a better, out of the box experience for Next.js users, however it is only compatible with the Webpack 5 builder, so we can't automigrate for you, as you are using the Vite builder. If you switch this project to use Webpack 5 and rerun this migration, we can update your project.
@ -379,8 +379,8 @@ export const newFrameworks: Fix<NewFrameworkRunOptions> = {
This migration is set to update your project to use the ${chalk.magenta(
'@storybook/svelte-webpack5'
)} framework, but Storybook provides a framework package specifically for SvelteKit projects: ${chalk.magenta(
'@storybook/sveltekit'
)}.
'@storybook/sveltekit'
)}.
This package provides a better experience for SvelteKit users, however it is only compatible with the Vite builder, so we can't automigrate for you, as you are using the Webpack builder.

View File

@ -32,8 +32,8 @@ export const nodeJsRequirement: Fix<NodeJsRequirementOptions> = {
We've detected that you're using Node ${chalk.bold(
nodeVersion
)} but Storybook 7 only supports Node ${chalk.bold(
'v16.0.0'
)} and higher. You will either need to upgrade your Node version or keep using an older version of Storybook.
'v16.0.0'
)} and higher. You will either need to upgrade your Node version or keep using an older version of Storybook.
Please see the migration guide for more information:
${chalk.yellow(

View File

@ -20,49 +20,52 @@ const logger = console;
* which could actually be a custom script even though the name matches the legacy binary name
*/
export const getStorybookScripts = (allScripts: NonNullable<PackageJson['scripts']>) => {
return Object.keys(allScripts).reduce((acc, key) => {
const currentScript = allScripts[key];
if (currentScript == null) {
return Object.keys(allScripts).reduce(
(acc, key) => {
const currentScript = allScripts[key];
if (currentScript == null) {
return acc;
}
let isStorybookScript = false;
const allWordsFromScript = currentScript.split(' ');
const newScript = allWordsFromScript
.map((currentWord, index) => {
const previousWord = allWordsFromScript[index - 1];
// full word check, rather than regex which could be faulty
const isSbBinary =
currentWord === 'build-storybook' ||
currentWord === 'start-storybook' ||
currentWord === 'sb';
// in case people have scripts like `yarn start-storybook`
const isPrependedByPkgManager =
previousWord &&
['npx', 'run', 'yarn', 'pnpx', 'pnpm dlx'].some((cmd) => previousWord.includes(cmd));
if (isSbBinary && !isPrependedByPkgManager) {
isStorybookScript = true;
return currentWord
.replace('sb', 'storybook')
.replace('start-storybook', 'storybook dev')
.replace('build-storybook', 'storybook build');
}
return currentWord;
})
.join(' ');
if (isStorybookScript) {
acc[key] = {
before: currentScript,
after: newScript,
};
}
return acc;
}
let isStorybookScript = false;
const allWordsFromScript = currentScript.split(' ');
const newScript = allWordsFromScript
.map((currentWord, index) => {
const previousWord = allWordsFromScript[index - 1];
// full word check, rather than regex which could be faulty
const isSbBinary =
currentWord === 'build-storybook' ||
currentWord === 'start-storybook' ||
currentWord === 'sb';
// in case people have scripts like `yarn start-storybook`
const isPrependedByPkgManager =
previousWord &&
['npx', 'run', 'yarn', 'pnpx', 'pnpm dlx'].some((cmd) => previousWord.includes(cmd));
if (isSbBinary && !isPrependedByPkgManager) {
isStorybookScript = true;
return currentWord
.replace('sb', 'storybook')
.replace('start-storybook', 'storybook dev')
.replace('build-storybook', 'storybook build');
}
return currentWord;
})
.join(' ');
if (isStorybookScript) {
acc[key] = {
before: currentScript,
after: newScript,
};
}
return acc;
}, {} as Record<string, { before: string; after: string }>);
},
{} as Record<string, { before: string; after: string }>
);
};
/**
@ -111,10 +114,10 @@ export const sbScripts: Fix<SbScriptsRunOptions> = {
return dedent`
We've detected you are using ${sbFormatted} with scripts from previous versions of Storybook.
Starting in Storybook 7, the ${chalk.yellow('start-storybook')} and ${chalk.yellow(
'build-storybook'
)} binaries have changed to ${chalk.magenta('storybook dev')} and ${chalk.magenta(
'storybook build'
)} respectively.
'build-storybook'
)} binaries have changed to ${chalk.magenta('storybook dev')} and ${chalk.magenta(
'storybook build'
)} respectively.
In order to work with ${sbFormatted}, your storybook scripts have to be adjusted to use the binary. We can adjust them for you:
${newScriptsMessage.join('\n\n')}
@ -129,10 +132,13 @@ export const sbScripts: Fix<SbScriptsRunOptions> = {
logger.info(`✅ Updating scripts in package.json`);
logger.log();
if (!dryRun) {
const newScripts = Object.keys(storybookScripts).reduce((acc, scriptKey) => {
acc[scriptKey] = storybookScripts[scriptKey].after;
return acc;
}, {} as Record<string, string>);
const newScripts = Object.keys(storybookScripts).reduce(
(acc, scriptKey) => {
acc[scriptKey] = storybookScripts[scriptKey].after;
return acc;
},
{} as Record<string, string>
);
logger.log();

View File

@ -47,8 +47,8 @@ export function getRequireWrapperName(config: ConfigFile) {
doesVariableOrFunctionDeclarationExist(node, 'wrapForPnp')
? ['wrapForPnp']
: doesVariableOrFunctionDeclarationExist(node, defaultRequireWrapperName)
? [defaultRequireWrapperName]
: []
? [defaultRequireWrapperName]
: []
);
if (declarationName.length) {

View File

@ -20,9 +20,11 @@ export const checkWebpack5Builder = async ({
To upgrade to the latest stable release, run this from your project directory:
${chalk.cyan('npx storybook upgrade')}
${chalk.cyan('npx storybook@latest upgrade')}
Add the ${chalk.cyan('--prerelease')} flag to get the latest prerelease.
To upgrade to the latest pre-release, run this from your project directory:
${chalk.cyan('npx storybook@next upgrade')}
`.trim()
);
return null;

View File

@ -92,8 +92,8 @@ export function getMigrationSummary({
const title = hasNoFixes
? 'No migrations were applicable to your project'
: hasFailures
? 'Migration check ran with failures'
: 'Migration check ran successfully';
? 'Migration check ran with failures'
: 'Migration check ran successfully';
return boxen(messages.filter(Boolean).join(segmentDivider), {
borderStyle: 'round',

View File

@ -107,12 +107,10 @@ export const detectBuilderInfo = async ({
// if builder is still not detected, rely on package dependencies
if (!builderOrFrameworkName) {
const storybookBuilderViteVersion = await packageManager.getPackageVersion(
'@storybook/builder-vite'
);
const storybookBuilderVite2Version = await packageManager.getPackageVersion(
'storybook-builder-vite'
);
const storybookBuilderViteVersion =
await packageManager.getPackageVersion('@storybook/builder-vite');
const storybookBuilderVite2Version =
await packageManager.getPackageVersion('storybook-builder-vite');
const storybookBuilderWebpack5Version = await packageManager.getPackageVersion(
'@storybook/builder-webpack5'
);

View File

@ -185,9 +185,8 @@ export async function detectLanguage(packageManager: JsPackageManager) {
'@typescript-eslint/parser'
);
const eslintPluginStorybookVersion = await packageManager.getPackageVersion(
'eslint-plugin-storybook'
);
const eslintPluginStorybookVersion =
await packageManager.getPackageVersion('eslint-plugin-storybook');
if (isTypescriptDirectDependency && typescriptVersion) {
if (

View File

@ -52,7 +52,7 @@ export const getIncompatibleAddons = async (
({
name: addon,
version: await packageManager.getPackageVersion(addon),
} as { name: keyof typeof incompatibleList; version: string })
}) as { name: keyof typeof incompatibleList; version: string }
)
);

View File

@ -76,15 +76,13 @@ command('remove <addon>')
.action((addonName: string, options: any) => remove(addonName, options));
command('upgrade')
.description('Upgrade your Storybook packages to the latest')
.description(`Upgrade your Storybook packages to v${versions.storybook}`)
.option(
'--package-manager <npm|pnpm|yarn1|yarn2>',
'Force package manager for installing dependencies'
)
.option('-y --yes', 'Skip prompting the user')
.option('-n --dry-run', 'Only check for upgrades, do not install')
.option('-t --tag <tag>', 'Upgrade to a certain npm dist-tag (e.g. next, prerelease)')
.option('-p --prerelease', 'Upgrade to the pre-release packages')
.option('-s --skip-check', 'Skip postinstall version and automigration checks')
.option('-c, --config-dir <dir-name>', 'Directory where to load Storybook configurations from')
.action(async (options: UpgradeOptions) => upgrade(options).catch(() => process.exit(1)));
@ -143,10 +141,9 @@ command('sandbox [filterValue]')
.alias('repro') // for backwards compatibility
.description('Create a sandbox from a set of possible templates')
.option('-o --output <outDir>', 'Define an output directory')
.option('-b --branch <branch>', 'Define the branch to download from', 'next')
.option('--no-init', 'Whether to download a template without an initialized Storybook', false)
.action((filterValue, options) =>
sandbox({ filterValue, ...options }).catch((e) => {
sandbox({ filterValue, ...options }, pkg).catch((e) => {
logger.error(e);
process.exit(1);
})

View File

@ -300,9 +300,8 @@ export async function baseGenerator(
try {
if (process.env.CI !== 'true') {
const { hasEslint, isStorybookPluginInstalled, eslintConfigFile } = await extractEslintInfo(
packageManager
);
const { hasEslint, isStorybookPluginInstalled, eslintConfigFile } =
await extractEslintInfo(packageManager);
if (hasEslint && !isStorybookPluginInstalled) {
versionedPackages.push('eslint-plugin-storybook');

View File

@ -105,8 +105,8 @@ export async function configureMain({
try {
const prettier = (await import('prettier')).default;
mainJsContents = prettier.format(dedent(mainJsContents), {
...prettier.resolveConfig.sync(process.cwd()),
mainJsContents = await prettier.format(dedent(mainJsContents), {
...(await prettier.resolveConfig(mainPath)),
filepath: mainPath,
});
} catch {
@ -168,8 +168,8 @@ export async function configurePreview(options: ConfigurePreviewOptions) {
try {
const prettier = (await import('prettier')).default;
preview = prettier.format(preview, {
...prettier.resolveConfig.sync(process.cwd()),
preview = await prettier.format(preview, {
...(await prettier.resolveConfig(previewPath)),
filepath: previewPath,
});
} catch {

View File

@ -224,7 +224,7 @@ const projectTypeInquirer = async (
process.exit(0);
};
async function doInitiate(
export async function doInitiate(
options: CommandOptions,
pkg: PackageJson
): Promise<

View File

@ -134,6 +134,8 @@ export abstract class JsPackageManager {
done = commandLog('Installing dependencies');
logger.log();
try {
await this.runInstall();
done();

View File

@ -192,7 +192,7 @@ export class Yarn1Proxy extends JsPackageManager {
const existingVersions: Record<string, string[]> = {};
const duplicatedDependencies: Record<string, string[]> = {};
const recurse = (tree: typeof trees[0]) => {
const recurse = (tree: (typeof trees)[0]) => {
const { children } = tree;
const { name, value } = parsePackageData(tree.name);
if (!name || !name.includes('storybook')) return;

View File

@ -7,8 +7,13 @@ import { downloadTemplate } from 'giget';
import { existsSync, readdir } from 'fs-extra';
import invariant from 'tiny-invariant';
import { lt, prerelease } from 'semver';
import type { Template, TemplateKey } from './sandbox-templates';
import { allTemplates as TEMPLATES } from './sandbox-templates';
import type { PackageJson, PackageManagerName } from './js-package-manager';
import { JsPackageManagerFactory } from './js-package-manager';
import versions from './versions';
import { doInitiate } from './initiate';
const logger = console;
@ -17,20 +22,60 @@ interface SandboxOptions {
output?: string;
branch?: string;
init?: boolean;
packageManager: PackageManagerName;
}
type Choice = keyof typeof TEMPLATES;
const toChoices = (c: Choice): prompts.Choice => ({ title: TEMPLATES[c].name, value: c });
export const sandbox = async ({
output: outputDirectory,
filterValue,
branch,
init,
}: SandboxOptions) => {
export const sandbox = async (
{ output: outputDirectory, filterValue, init, ...options }: SandboxOptions,
pkg: PackageJson
) => {
// Either get a direct match when users pass a template id, or filter through all templates
let selectedConfig: Template | undefined = TEMPLATES[filterValue as TemplateKey];
let selectedTemplate: Choice | null = selectedConfig ? (filterValue as TemplateKey) : null;
let templateId: Choice | null = selectedConfig ? (filterValue as TemplateKey) : null;
const { packageManager: pkgMgr } = options;
const packageManager = JsPackageManagerFactory.getPackageManager({
force: pkgMgr,
});
const latestVersion = await packageManager.latestVersion('@storybook/cli');
const nextVersion = await packageManager.latestVersion('@storybook/cli@next');
const currentVersion = versions['@storybook/cli'];
const isPrerelease = prerelease(currentVersion);
const isOutdated = lt(currentVersion, isPrerelease ? nextVersion : latestVersion);
const borderColor = isOutdated ? '#FC521F' : '#F1618C';
const downloadType = !isOutdated && init ? 'after-storybook' : 'before-storybook';
const branch = isPrerelease ? 'next' : 'main';
const messages = {
welcome: `Creating a Storybook ${chalk.bold(currentVersion)} sandbox..`,
notLatest: chalk.red(dedent`
This version is behind the latest release, which is: ${chalk.bold(latestVersion)}!
You likely ran the init command through npx, which can use a locally cached version, to get the latest please run:
${chalk.bold('npx storybook@latest sandbox')}
You may want to CTRL+C to stop, and run with the latest version instead.
`),
longInitTime: chalk.yellow(
'The creation of the sandbox will take longer, because we will need to run init.'
),
prerelease: chalk.yellow('This is a pre-release version.'),
};
logger.log(
boxen(
[messages.welcome]
.concat(isOutdated && !isPrerelease ? [messages.notLatest] : [])
.concat(init && (isOutdated || isPrerelease) ? [messages.longInitTime] : [])
.concat(isPrerelease ? [messages.prerelease] : [])
.join('\n'),
{ borderStyle: 'round', padding: 1, borderColor }
)
);
if (!selectedConfig) {
const filterRegex = new RegExp(`^${filterValue || ''}`, 'i');
@ -79,7 +124,7 @@ export const sandbox = async ({
}
if (choices.length === 1) {
[selectedTemplate] = choices;
[templateId] = choices;
} else {
logger.info(
boxen(
@ -97,16 +142,16 @@ export const sandbox = async ({
)
);
selectedTemplate = await promptSelectedTemplate(choices);
templateId = await promptSelectedTemplate(choices);
}
const hasSelectedTemplate = !!(selectedTemplate ?? null);
const hasSelectedTemplate = !!(templateId ?? null);
if (!hasSelectedTemplate) {
logger.error('Somehow we got no templates. Please rerun this command!');
return;
}
selectedConfig = selectedTemplate ? TEMPLATES[selectedTemplate] : undefined;
selectedConfig = templateId ? TEMPLATES[templateId] : undefined;
if (!selectedConfig) {
throw new Error('🚨 Sandbox: please specify a valid template type');
@ -114,7 +159,7 @@ export const sandbox = async ({
}
let selectedDirectory = outputDirectory;
const outputDirectoryName = outputDirectory || selectedTemplate;
const outputDirectoryName = outputDirectory || templateId;
if (selectedDirectory && existsSync(`${selectedDirectory}`)) {
logger.info(`⚠️ ${selectedDirectory} already exists! Overwriting...`);
}
@ -149,22 +194,35 @@ export const sandbox = async ({
logger.info(`🏃 Adding ${selectedConfig.name} into ${templateDestination}`);
logger.log('📦 Downloading sandbox template...');
logger.log(`📦 Downloading sandbox template (${chalk.bold(downloadType)})...`);
try {
const templateType = init ? 'after-storybook' : 'before-storybook';
// Download the sandbox based on subfolder "after-storybook" and selected branch
const gitPath = `github:storybookjs/sandboxes/${selectedTemplate}/${templateType}#${branch}`;
const gitPath = `github:storybookjs/sandboxes/${templateId}/${downloadType}#${branch}`;
await downloadTemplate(gitPath, {
force: true,
dir: templateDestination,
});
// throw an error if templateDestination is an empty directory using fs-extra
if ((await readdir(templateDestination)).length === 0) {
throw new Error(
dedent`Template downloaded from ${chalk.blue(gitPath)} is empty.
Are you use it exists? Or did you want to set ${chalk.yellow(
selectedTemplate
)} to inDevelopment first?`
const selected = chalk.yellow(templateId);
throw new Error(dedent`
Template downloaded from ${chalk.blue(gitPath)} is empty.
Are you use it exists? Or did you want to set ${selected} to inDevelopment first?
`);
}
// when user wanted an sandbox that has been initiated, but force-downloaded the before-storybook directory
// then we need to initiate the sandbox
// this is to ensure we DO get the latest version of the template (output of the generator), but we initialize using the version of storybook that the CLI is.
// we warned the user about the fact they are running an old version of storybook
// we warned the user the sandbox step would take longer
if ((isOutdated || isPrerelease) && init) {
// we run doInitiate, instead of initiate, to avoid sending this init event to telemetry, because it's not a real world project
await doInitiate(
{
...options,
},
pkg
);
}
} catch (err) {
@ -173,7 +231,10 @@ export const sandbox = async ({
}
const initMessage = init
? chalk.yellow(`yarn install\nyarn storybook`)
? chalk.yellow(dedent`
yarn install
yarn storybook
`)
: `Recreate your setup, then ${chalk.yellow(`npx storybook@latest init`)}`;
logger.info(

View File

@ -1,5 +1,22 @@
import { describe, it, expect } from 'vitest';
import { addExtraFlags, addNxPackagesToReject, getStorybookVersion } from './upgrade';
import { describe, it, expect, vi } from 'vitest';
import { getStorybookCoreVersion } from '@storybook/telemetry';
import {
UpgradeStorybookToLowerVersionError,
UpgradeStorybookToSameVersionError,
} from '@storybook/core-events/server-errors';
import { doUpgrade, getStorybookVersion } from './upgrade';
import type versions from './versions';
vi.mock('@storybook/telemetry');
vi.mock('./versions', async (importOriginal) => {
const originalVersions = ((await importOriginal()) as { default: typeof versions }).default;
return {
default: Object.keys(originalVersions).reduce((acc, key) => {
acc[key] = '8.0.0';
return acc;
}, {} as Record<string, string>),
};
});
describe.each([
['│ │ │ ├── @babel/code-frame@7.10.3 deduped', null],
@ -22,68 +39,15 @@ describe.each([
});
});
describe('extra flags', () => {
const extraFlags = {
'react-scripts@<5': ['--foo'],
};
const devDependencies = {};
it('package matches constraints', () => {
expect(
addExtraFlags(extraFlags, [], { dependencies: { 'react-scripts': '4' }, devDependencies })
).toEqual(['--foo']);
});
it('package prerelease matches constraints', () => {
expect(
addExtraFlags(extraFlags, [], {
dependencies: { 'react-scripts': '4.0.0-alpha.0' },
devDependencies,
})
).toEqual(['--foo']);
});
it('package not matches constraints', () => {
expect(
addExtraFlags(extraFlags, [], {
dependencies: { 'react-scripts': '5.0.0-alpha.0' },
devDependencies,
})
).toEqual([]);
});
it('no package not matches constraints', () => {
expect(
addExtraFlags(extraFlags, [], {
dependencies: {},
devDependencies,
})
).toEqual([]);
});
});
describe('Upgrade errors', () => {
it('should throw an error when upgrading to a lower version number', async () => {
vi.mocked(getStorybookCoreVersion).mockResolvedValue('8.1.0');
describe('addNxPackagesToReject', () => {
it('reject exists and is in regex pattern', () => {
const flags = ['--reject', '/preset-create-react-app/', '--some-flag', 'hello'];
expect(addNxPackagesToReject(flags)).toMatchObject([
'--reject',
'"/(preset-create-react-app|@nrwl/storybook|@nx/storybook)/"',
'--some-flag',
'hello',
]);
await expect(doUpgrade({} as any)).rejects.toThrowError(UpgradeStorybookToLowerVersionError);
});
it('reject exists and is in unknown pattern', () => {
const flags = ['--some-flag', 'hello', '--reject', '@storybook/preset-create-react-app'];
expect(addNxPackagesToReject(flags)).toMatchObject([
'--some-flag',
'hello',
'--reject',
'@storybook/preset-create-react-app,@nrwl/storybook,@nx/storybook',
]);
});
it('reject does not exist', () => {
const flags = ['--some-flag', 'hello'];
expect(addNxPackagesToReject(flags)).toMatchObject([
'--some-flag',
'hello',
'--reject',
'@nrwl/storybook,@nx/storybook',
]);
it('should throw an error when upgrading to the same version number', async () => {
vi.mocked(getStorybookCoreVersion).mockResolvedValue('8.0.0');
await expect(doUpgrade({} as any)).rejects.toThrowError(UpgradeStorybookToSameVersionError);
});
});

View File

@ -1,18 +1,22 @@
import { sync as spawnSync } from 'cross-spawn';
import { telemetry, getStorybookCoreVersion } from '@storybook/telemetry';
import semver from 'semver';
import semver, { eq, lt, prerelease } from 'semver';
import { logger } from '@storybook/node-logger';
import { withTelemetry } from '@storybook/core-server';
import {
ConflictingVersionTagsError,
UpgradeStorybookPackagesError,
UpgradeStorybookToLowerVersionError,
UpgradeStorybookToSameVersionError,
} from '@storybook/core-events/server-errors';
import type { PackageJsonWithMaybeDeps, PackageManagerName } from './js-package-manager';
import { getPackageDetails, JsPackageManagerFactory } from './js-package-manager';
import chalk from 'chalk';
import dedent from 'ts-dedent';
import boxen from 'boxen';
import type { PackageManagerName } from './js-package-manager';
import { JsPackageManagerFactory } from './js-package-manager';
import { coerceSemver, commandLog } from './helpers';
import { automigrate } from './automigrate';
import { isCorePackage } from './utils';
import versions from './versions';
type Package = {
package: string;
@ -87,57 +91,7 @@ export const checkVersionConsistency = () => {
});
};
type ExtraFlags = Record<string, string[]>;
const EXTRA_FLAGS: ExtraFlags = {
'react-scripts@<5': ['--reject', '/preset-create-react-app/'],
};
export const addExtraFlags = (
extraFlags: ExtraFlags,
flags: string[],
{ dependencies, devDependencies }: PackageJsonWithMaybeDeps
) => {
return Object.entries(extraFlags).reduce(
(acc, entry) => {
const [pattern, extra] = entry;
const [pkg, specifier] = getPackageDetails(pattern);
const pkgVersion = dependencies?.[pkg] || devDependencies?.[pkg];
if (pkgVersion && specifier && semver.satisfies(coerceSemver(pkgVersion), specifier)) {
return [...acc, ...extra];
}
return acc;
},
[...flags]
);
};
export const addNxPackagesToReject = (flags: string[]) => {
const newFlags = [...flags];
const index = flags.indexOf('--reject');
if (index > -1) {
// Try to understand if it's in the format of a regex pattern
if (newFlags[index + 1].endsWith('/') && newFlags[index + 1].startsWith('/')) {
// Remove last and first slash so that I can add the parentheses
newFlags[index + 1] = newFlags[index + 1].substring(1, newFlags[index + 1].length - 1);
newFlags[index + 1] = `"/(${newFlags[index + 1]}|@nrwl/storybook|@nx/storybook)/"`;
} else {
// Adding the two packages as comma-separated values
// If the existing rejects are in regex format, they will be ignored.
// Maybe we need to find a more robust way to treat rejects?
newFlags[index + 1] = `${newFlags[index + 1]},@nrwl/storybook,@nx/storybook`;
}
} else {
newFlags.push('--reject');
newFlags.push('@nrwl/storybook,@nx/storybook');
}
return newFlags;
};
export interface UpgradeOptions {
tag: string;
prerelease: boolean;
skipCheck: boolean;
packageManager: PackageManagerName;
dryRun: boolean;
@ -147,8 +101,6 @@ export interface UpgradeOptions {
}
export const doUpgrade = async ({
tag,
prerelease,
skipCheck,
packageManager: pkgMgr,
dryRun,
@ -158,66 +110,88 @@ export const doUpgrade = async ({
}: UpgradeOptions) => {
const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr });
const currentVersion = versions['@storybook/cli'];
const beforeVersion = await getStorybookCoreVersion();
commandLog(`Checking for latest versions of '@storybook/*' packages\n`);
if (tag && prerelease) {
throw new ConflictingVersionTagsError();
if (lt(currentVersion, beforeVersion)) {
throw new UpgradeStorybookToLowerVersionError({ beforeVersion, currentVersion });
}
if (eq(currentVersion, beforeVersion)) {
throw new UpgradeStorybookToSameVersionError({ beforeVersion });
}
let target = 'latest';
if (prerelease) {
// '@next' is storybook's convention for the latest prerelease tag.
// This used to be 'greatest', but that was not reliable and could pick canaries, etc.
// and random releases of other packages with storybook in their name.
target = '@next';
} else if (tag) {
target = `@${tag}`;
}
const latestVersion = await packageManager.latestVersion('@storybook/cli');
const isOutdated = lt(currentVersion, latestVersion);
const isPrerelease = prerelease(currentVersion) !== null;
let flags = [];
if (!dryRun) flags.push('--upgrade');
flags.push('--target');
flags.push(target);
flags = addExtraFlags(EXTRA_FLAGS, flags, await packageManager.retrievePackageJson());
flags = addNxPackagesToReject(flags);
const borderColor = isOutdated ? '#FC521F' : '#F1618C';
const command = 'npx';
const checkArgs = ['npm-check-updates@latest', '/storybook/', ...flags];
const check = spawnSync(command, checkArgs, {
stdio: 'pipe',
shell: true,
});
const messages = {
welcome: `Upgrading Storybook from version ${chalk.bold(beforeVersion)} to version ${chalk.bold(
currentVersion
)}..`,
notLatest: chalk.red(dedent`
This version is behind the latest release, which is: ${chalk.bold(latestVersion)}!
You likely ran the upgrade command through npx, which can use a locally cached version, to upgrade to the latest version please run:
${chalk.bold('npx storybook@latest upgrade')}
You may want to CTRL+C to stop, and run with the latest version instead.
`),
prelease: chalk.yellow('This is a pre-release version.'),
};
if (check.stderr && check.stderr.toString().includes('npm ERR')) {
throw new UpgradeStorybookPackagesError({
command,
args: checkArgs,
errorMessage: check.stderr.toString(),
});
}
logger.plain(
boxen(
[messages.welcome]
.concat(isOutdated && !isPrerelease ? [messages.notLatest] : [])
.concat(isPrerelease ? [messages.prelease] : [])
.join('\n'),
{ borderStyle: 'round', padding: 1, borderColor }
)
);
logger.info(check.stdout.toString());
const packageJson = await packageManager.retrievePackageJson();
const checkSbArgs = ['npm-check-updates@latest', 'sb', ...flags];
const checkSb = spawnSync(command, checkSbArgs, {
stdio: 'pipe',
shell: true,
});
logger.info(checkSb.stdout.toString());
logger.info(checkSb.stderr.toString());
const toUpgradedDependencies = (deps: Record<string, any>) => {
const monorepoDependencies = Object.keys(deps || {}).filter((dependency) => {
// don't upgrade @storybook/preset-create-react-app if react-scripts is < v5
if (dependency === '@storybook/preset-create-react-app') {
const reactScriptsVersion =
packageJson.dependencies['react-scripts'] ?? packageJson.devDependencies['react-scripts'];
if (reactScriptsVersion && lt(coerceSemver(reactScriptsVersion), '5.0.0')) {
return false;
}
}
if (checkSb.stderr && checkSb.stderr.toString().includes('npm ERR')) {
throw new UpgradeStorybookPackagesError({
command,
args: checkSbArgs,
errorMessage: checkSb.stderr.toString(),
});
}
// only upgrade packages that are in the monorepo
return dependency in versions;
}) as Array<keyof typeof versions>;
return monorepoDependencies.map(
(dependency) =>
// add ^ modifier to the version if this is the latest and stable version
// example output: @storybook/react@^8.0.0
`${dependency}@${!isOutdated || isPrerelease ? '^' : ''}${versions[dependency]}`
);
};
const upgradedDependencies = toUpgradedDependencies(packageJson.dependencies);
const upgradedDevDependencies = toUpgradedDependencies(packageJson.devDependencies);
if (!dryRun) {
commandLog(`Installing upgrades`);
commandLog(`Updating dependencies in ${chalk.cyan('package.json')}..`);
logger.plain('');
if (upgradedDependencies.length > 0) {
await packageManager.addDependencies(
{ installAsDevDependencies: false, skipInstall: true, packageJson },
upgradedDependencies
);
}
if (upgradedDevDependencies.length > 0) {
await packageManager.addDependencies(
{ installAsDevDependencies: true, skipInstall: true, packageJson },
upgradedDevDependencies
);
}
await packageManager.installDependencies();
}
@ -234,8 +208,6 @@ export const doUpgrade = async ({
automigrationPreCheckFailure: preCheckFailure || null,
};
telemetry('upgrade', {
prerelease,
tag,
beforeVersion,
afterVersion,
...automigrationTelemetry,

View File

@ -64,7 +64,7 @@
"globby": "^11.0.2",
"jscodeshift": "^0.15.1",
"lodash": "^4.17.21",
"prettier": "^2.8.0",
"prettier": "^3.1.1",
"recast": "^0.23.1",
"tiny-invariant": "^1.3.1"
},

View File

@ -9,21 +9,25 @@ expect.addSnapshotSerializer({
test: () => true,
});
const jsTransform = (source: string) =>
_transform({ source, path: 'Component.stories.js' }, {} as API, {}).trim();
const tsTransform = (source: string) =>
_transform({ source, path: 'Component.stories.ts' }, {} as API, { parser: 'tsx' }).trim();
const jsTransform = async (source: string) =>
(await _transform({ source, path: 'Component.stories.jsx' }, {} as API, {})).trim();
const tsTransform = async (source: string) =>
(
await _transform({ source, path: 'Component.stories.tsx' }, {} as API, {
parser: 'tsx',
})
).trim();
describe('csf-2-to-3', () => {
describe('javascript', () => {
it('should replace non-simple function exports with objects', () => {
expect(
it('should replace non-simple function exports with objects', async () => {
await expect(
jsTransform(dedent`
export default { title: 'Cat' };
export const A = () => <Cat />;
export const B = (args) => <Button {...args} />;
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'Cat' };
export const A = () => <Cat />;
export const B = {
@ -32,8 +36,8 @@ describe('csf-2-to-3', () => {
`);
});
it('should move annotations into story objects', () => {
expect(
it('should move annotations into story objects', async () => {
await expect(
jsTransform(dedent`
export default { title: 'Cat' };
@ -42,7 +46,7 @@ describe('csf-2-to-3', () => {
A.parameters = { bar: 2 };
A.play = () => {};
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'Cat' };
export const A = {
@ -54,8 +58,8 @@ describe('csf-2-to-3', () => {
`);
});
it('should ignore non-story exports, statements', () => {
expect(
it('should ignore non-story exports, statements', async () => {
await expect(
jsTransform(dedent`
export default { title: 'components/Fruit', includeStories: ['A'] };
@ -65,7 +69,7 @@ describe('csf-2-to-3', () => {
const C = (args) => <Cherry {...args} />;
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'components/Fruit', includeStories: ['A'] };
export const A = {
@ -78,28 +82,28 @@ describe('csf-2-to-3', () => {
`);
});
it('should do nothing when there is no meta', () => {
expect(
it('should do nothing when there is no meta', async () => {
await expect(
jsTransform(dedent`
export const A = () => <Apple />;
export const B = (args) => <Banana {...args} />;
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export const A = () => <Apple />;
export const B = (args) => <Banana {...args} />;
`);
});
it('should remove implicit global render function (react)', () => {
expect(
it('should remove implicit global render function (react)', async () => {
await expect(
jsTransform(dedent`
export default { title: 'Cat', component: Cat };
export const A = (args) => <Cat {...args} />;
export const B = (args) => <Banana {...args} />;
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'Cat', component: Cat };
export const A = {};
export const B = {
@ -108,8 +112,8 @@ describe('csf-2-to-3', () => {
`);
});
it('should ignore object exports', () => {
expect(
it('should ignore object exports', async () => {
await expect(
jsTransform(dedent`
export default { title: 'Cat', component: Cat };
@ -117,7 +121,7 @@ describe('csf-2-to-3', () => {
render: (args) => <Cat {...args} />
};
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'Cat', component: Cat };
export const A = {
@ -126,15 +130,15 @@ describe('csf-2-to-3', () => {
`);
});
it('should hoist template.bind (if there is only one)', () => {
expect(
it('should hoist template.bind (if there is only one)', async () => {
await expect(
jsTransform(dedent`
export default { title: 'Cat' };
const Template = (args) => <Cat {...args} />;
export const A = Template.bind({});
A.args = { isPrimary: false };
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'Cat' };
const Template = (args) => <Cat {...args} />;
@ -145,8 +149,8 @@ describe('csf-2-to-3', () => {
`);
});
it('should reuse the template when there are multiple Template.bind references but no component defined', () => {
expect(
it('should reuse the template when there are multiple Template.bind references but no component defined', async () => {
await expect(
jsTransform(dedent`
export default { title: 'Cat' };
const Template = (args) => <Cat {...args} />;
@ -164,7 +168,7 @@ describe('csf-2-to-3', () => {
export const D = Template.bind({});
D.args = { bla: false };
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'Cat' };
const Template = (args) => <Cat {...args} />;
@ -190,8 +194,8 @@ describe('csf-2-to-3', () => {
`);
});
it('should remove implicit global render for template.bind', () => {
expect(
it('should remove implicit global render for template.bind', async () => {
await expect(
jsTransform(dedent`
export default { title: 'Cat', component: Cat };
@ -205,7 +209,7 @@ describe('csf-2-to-3', () => {
export const B = Template2.bind({});
B.args = { isPrimary: true };
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'Cat', component: Cat };
export const A = {
@ -221,8 +225,8 @@ describe('csf-2-to-3', () => {
`);
});
it('should ignore no-arg stories without annotations', () => {
expect(
it('should ignore no-arg stories without annotations', async () => {
await expect(
jsTransform(dedent`
export default { title: 'Cat', component: Cat };
@ -231,7 +235,7 @@ describe('csf-2-to-3', () => {
export const C = () => <Cat name="fluffy" />;
C.parameters = { foo: 2 };
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'Cat', component: Cat };
export const A = {};
@ -244,8 +248,8 @@ describe('csf-2-to-3', () => {
`);
});
it('should work for v1-style annotations', () => {
expect(
it('should work for v1-style annotations', async () => {
await expect(
jsTransform(dedent`
export default { title: 'Cat' };
export const A = (args) => <Cat {...args} />;
@ -253,7 +257,7 @@ describe('csf-2-to-3', () => {
parameters: { foo: 2 }
};
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
export default { title: 'Cat' };
export const A = {
@ -265,15 +269,15 @@ describe('csf-2-to-3', () => {
});
describe('typescript', () => {
it('should error with namespace imports', () => {
expect.addSnapshotSerializer({
it('should error with namespace imports', async () => {
await expect.addSnapshotSerializer({
serialize: (value) => {
const stringVal = typeof value === 'string' ? value : value.toString();
return stringVal.replace(ansiRegex(), '');
},
test: () => true,
});
expect(() =>
await expect(() =>
tsTransform(dedent`
import * as SB from '@storybook/react';
import { CatProps } from './Cat';
@ -283,13 +287,13 @@ describe('csf-2-to-3', () => {
export const A: SB.StoryFn<CatProps> = () => <Cat />;
`)
).toThrowErrorMatchingInlineSnapshot(dedent`
).rejects.toThrowErrorMatchingInlineSnapshot(dedent`
Error: This codemod does not support namespace imports for a @storybook/react package.
Replace the namespace import with named imports and try again.
`);
});
it('should keep local names', () => {
expect(
it('should keep local names', async () => {
await expect(
tsTransform(dedent`
import { Meta, StoryObj as CSF3, StoryFn as CSF2 } from '@storybook/react';
import { CatProps } from './Cat';
@ -308,7 +312,7 @@ describe('csf-2-to-3', () => {
name: "Fluffy"
};
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
import { Meta, StoryObj as CSF3, StoryFn as CSF2 } from '@storybook/react';
import { CatProps } from './Cat';
@ -329,8 +333,8 @@ describe('csf-2-to-3', () => {
`);
});
it('should replace function exports with objects and update type', () => {
expect(
it('should replace function exports with objects and update type', async () => {
await expect(
tsTransform(dedent`
import { Story, StoryFn, ComponentStory, ComponentStoryObj } from '@storybook/react';
@ -367,7 +371,7 @@ describe('csf-2-to-3', () => {
},
};
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
import { StoryObj, StoryFn } from '@storybook/react';
// some extra whitespace to test
@ -411,8 +415,8 @@ describe('csf-2-to-3', () => {
`);
});
it('migrate Story type to StoryFn when used in an not exported Template function', () => {
expect(
it('migrate Story type to StoryFn when used in an not exported Template function', async () => {
await expect(
tsTransform(dedent`
import { Story, Meta } from '@storybook/react'
@ -424,7 +428,7 @@ describe('csf-2-to-3', () => {
export const Default = Template.bind({})
`)
).toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
import { StoryFn, Meta } from '@storybook/react';
export default {

View File

@ -15,24 +15,23 @@ beforeEach(() => {
fs.existsSync.mockImplementation(() => false);
});
it('update import even when no stories can be extracted', () => {
it('update import even when no stories can be extracted', async () => {
const input = dedent`
import { Heading } from '@storybook/addon-docs';
<Heading />
`;
const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
expect(mdx).toMatchInlineSnapshot(`
import { Heading } from '@storybook/blocks';
<Heading />
`);
});
it('drop invalid story nodes', () => {
it('drop invalid story nodes', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
@ -43,7 +42,7 @@ it('drop invalid story nodes', () => {
<Story name="Primary">Story</Story>
`;
const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
expect(mdx).toMatchInlineSnapshot(`
import { Meta, Story } from '@storybook/blocks';
@ -54,11 +53,10 @@ it('drop invalid story nodes', () => {
<Story of={FoobarStories.Primary} />
`);
});
it('convert story re-definition', () => {
it('convert story re-definition', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
import { Primary } from './Foobar.stories';
@ -70,7 +68,7 @@ it('convert story re-definition', () => {
fs.existsSync.mockImplementation((path) => path === 'Foobar.stories.js');
const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
expect(mdx).toMatchInlineSnapshot(`
import { Meta, Story } from '@storybook/blocks';
@ -80,7 +78,6 @@ it('convert story re-definition', () => {
<Meta of={FoobarStories} />
<Story of={FoobarStories.Primary} />
`);
const [csfFileName, csf] = fs.writeFileSync.mock.calls[0];
expect(csfFileName).toMatchInlineSnapshot(`Foobar_.stories.js`);
@ -92,11 +89,10 @@ it('convert story re-definition', () => {
};
export { Primary };
`);
});
it('Comment out story nodes with id', () => {
it('Comment out story nodes with id', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
@ -105,7 +101,7 @@ it('Comment out story nodes with id', () => {
<Story id="button--primary" />
`;
const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
expect(mdx).toMatchInlineSnapshot(`
import { Meta, Story } from '@storybook/blocks';
@ -116,11 +112,10 @@ it('Comment out story nodes with id', () => {
{/* <Story id="button--primary" /> is deprecated, please migrate it to <Story of={referenceToStory} /> see: https://storybook.js.org/migration-guides/7.0 */}
<Story id="button--primary" />
`);
});
it('convert correct story nodes', () => {
it('convert correct story nodes', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
@ -129,7 +124,7 @@ it('convert correct story nodes', () => {
<Story name="Primary">Story</Story>
`;
const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
expect(mdx).toMatchInlineSnapshot(`
import { Meta, Story } from '@storybook/blocks';
@ -138,7 +133,6 @@ it('convert correct story nodes', () => {
<Meta of={FoobarStories} />
<Story of={FoobarStories.Primary} />
`);
const [, csf] = fs.writeFileSync.mock.calls[0];
@ -151,11 +145,10 @@ it('convert correct story nodes', () => {
render: () => 'Story',
name: 'Primary',
};
`);
});
it('convert addon-docs imports', () => {
it('convert addon-docs imports', async () => {
const input = dedent`
import { Meta } from '@storybook/addon-docs';
import { Story } from '@storybook/addon-docs/blocks';
@ -165,7 +158,7 @@ it('convert addon-docs imports', () => {
<Story name="Primary">Story</Story>
`;
const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
expect(mdx).toMatchInlineSnapshot(`
import { Meta } from '@storybook/blocks';
@ -175,11 +168,10 @@ it('convert addon-docs imports', () => {
<Meta of={FoobarStories} />
<Story of={FoobarStories.Primary} />
`);
});
it('convert story nodes with spaces', () => {
it('convert story nodes with spaces', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
@ -188,7 +180,7 @@ it('convert story nodes with spaces', () => {
<Story name="Primary Space">Story</Story>
`;
const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
expect(mdx).toMatchInlineSnapshot(`
import { Meta, Story } from '@storybook/blocks';
@ -197,7 +189,6 @@ it('convert story nodes with spaces', () => {
<Meta of={FoobarStories} />
<Story of={FoobarStories.PrimarySpace} />
`);
const [, csf] = fs.writeFileSync.mock.calls[0];
@ -210,11 +201,10 @@ it('convert story nodes with spaces', () => {
render: () => 'Story',
name: 'Primary Space',
};
`);
});
it('extract esm into csf head code', () => {
it('extract esm into csf head code', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
import { Button } from './Button';
@ -240,7 +230,7 @@ it('extract esm into csf head code', () => {
</Story>
`;
const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
expect(mdx).toMatchInlineSnapshot(`
import { Meta, Story } from '@storybook/blocks';
@ -259,7 +249,6 @@ it('extract esm into csf head code', () => {
<Story of={FoobarStories.Foo} />
<Story of={FoobarStories.Unchecked} />
`);
const [csfFileName, csf] = fs.writeFileSync.mock.calls[0];
@ -288,11 +277,10 @@ it('extract esm into csf head code', () => {
label: 'Unchecked',
},
};
`);
});
it('extract all meta parameters', () => {
it('extract all meta parameters', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
@ -303,7 +291,7 @@ it('extract all meta parameters', () => {
<Story name="foo">bar</Story>
`;
jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const [, csf] = fs.writeFileSync.mock.calls[0];
@ -326,11 +314,10 @@ it('extract all meta parameters', () => {
render: () => 'bar',
name: 'foo',
};
`);
});
it('extract all story attributes', () => {
it('extract all story attributes', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
import { Button } from './Button';
@ -355,7 +342,7 @@ it('extract all story attributes', () => {
`;
jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const [, csf] = fs.writeFileSync.mock.calls[0];
@ -388,11 +375,10 @@ it('extract all story attributes', () => {
render: Template.bind({}),
name: 'Second',
};
`);
});
it('duplicate story name', () => {
it('duplicate story name', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
import { Button } from './Button';
@ -409,7 +395,7 @@ it('duplicate story name', () => {
`;
const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const [, csf] = fs.writeFileSync.mock.calls[0];
expect(mdx).toMatchInlineSnapshot(`
@ -444,11 +430,10 @@ it('duplicate story name', () => {
render: Default.bind({}),
name: 'Second',
};
`);
});
it('kebab case file name', () => {
it('kebab case file name', async () => {
const input = dedent`
import { Meta, Story } from '@storybook/addon-docs';
import { Kebab } from './my-component/some-kebab-case';
@ -465,7 +450,7 @@ it('kebab case file name', () => {
`;
const mdx = jscodeshift({ source: input, path: 'some-kebab-case.stories.mdx' });
const mdx = await jscodeshift({ source: input, path: 'some-kebab-case.stories.mdx' });
expect(mdx).toMatchInlineSnapshot(`
import { Meta, Story } from '@storybook/blocks';
@ -479,7 +464,6 @@ it('kebab case file name', () => {
<Story of={SomeKebabCaseStories.MuchKebab} />
<Story of={SomeKebabCaseStories.ReallyMuchKebab} />
`);
const [, csf] = fs.writeFileSync.mock.calls[0];
@ -502,11 +486,10 @@ it('kebab case file name', () => {
render: Template.bind({}),
name: 'Really-Much-Kebab',
};
`);
});
it('story child is jsx', () => {
it('story child is jsx', async () => {
const input = dedent`
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Button } from './button';
@ -518,28 +501,27 @@ it('story child is jsx', () => {
</Story>
`;
jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const [, csf] = fs.writeFileSync.mock.calls[0];
expect(csf).toMatchInlineSnapshot(`
import { Button } from './button';
export default {};
import { Button } from './button';
export default {};
export const Primary = {
render: () => (
<Button>
<div>Hello!</div>
</Button>
),
export const Primary = {
render: () => (
<Button>
<div>Hello!</div>
</Button>
),
name: 'Primary',
};
`);
name: 'Primary',
};
`);
});
it('story child is CSF3', () => {
it('story child is CSF3', async () => {
const input = dedent`
import { Story } from '@storybook/addon-docs';
import { Button } from './button';
@ -547,7 +529,7 @@ it('story child is CSF3', () => {
<Story name="Primary" render={(args) => <Button {...args}></Button> } args={{label: 'Hello' }} />
`;
jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const [, csf] = fs.writeFileSync.mock.calls[0];
@ -563,11 +545,10 @@ it('story child is CSF3', () => {
label: 'Hello',
},
};
`);
});
it('story child is arrow function', () => {
it('story child is arrow function', async () => {
const input = dedent`
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Button } from './button';
@ -577,23 +558,22 @@ it('story child is arrow function', () => {
</Story>
`;
jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const [, csf] = fs.writeFileSync.mock.calls[0];
expect(csf).toMatchInlineSnapshot(`
import { Button } from './button';
export default {};
import { Button } from './button';
export default {};
export const Primary = {
render: (args) => <Button />,
name: 'Primary',
};
`);
export const Primary = {
render: (args) => <Button />,
name: 'Primary',
};
`);
});
it('story child is identifier', () => {
it('story child is identifier', async () => {
const input = dedent`
import { Canvas, Meta, Story } from '@storybook/addon-docs';
import { Button } from './button';
@ -603,20 +583,19 @@ it('story child is identifier', () => {
</Story>
`;
jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
await jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
const [, csf] = fs.writeFileSync.mock.calls[0];
expect(csf).toMatchInlineSnapshot(`
import { Button } from './button';
export default {};
import { Button } from './button';
export default {};
export const Primary = {
render: Button,
name: 'Primary',
};
`);
export const Primary = {
render: Button,
name: 'Primary',
};
`);
});
it('nameToValidExport', () => {

View File

@ -9,13 +9,17 @@ expect.addSnapshotSerializer({
test: () => true,
});
const tsTransform = (source: string) =>
_transform({ source, path: 'Component.stories.ts' }, {} as API, { parser: 'tsx' }).trim();
const tsTransform = async (source: string) =>
(
await _transform({ source, path: 'Component.stories.tsx' }, {} as API, {
parser: 'tsx',
})
).trim();
describe('upgrade-deprecated-types', () => {
describe('typescript', () => {
it('upgrade regular imports', () => {
expect(
it('upgrade regular imports', async () => {
await expect(
tsTransform(dedent`
import { Story, ComponentMeta, Meta, ComponentStory, ComponentStoryObj, ComponentStoryFn } from '@storybook/react';
import { Cat, CatProps } from './Cat';
@ -34,11 +38,11 @@ describe('upgrade-deprecated-types', () => {
};
export const E: Story<CatProps> = (args) => <Cat {...args} />;
`)
).toMatchSnapshot();
).resolves.toMatchSnapshot();
});
it('upgrade imports with local names', () => {
expect(
it('upgrade imports with local names', async () => {
await expect(
tsTransform(dedent`
import { Story as Story_, ComponentMeta as ComponentMeta_, ComponentStory as Story__, ComponentStoryObj as ComponentStoryObj_, ComponentStoryFn as StoryFn_ } from '@storybook/react';
import { Cat } from './Cat';
@ -57,11 +61,11 @@ describe('upgrade-deprecated-types', () => {
};
export const E: Story_<CatProps> = (args) => <Cat {...args} />;
`)
).toMatchSnapshot();
).resolves.toMatchSnapshot();
});
it('upgrade imports with conflicting local names', () => {
expect.addSnapshotSerializer({
it('upgrade imports with conflicting local names', async () => {
await expect.addSnapshotSerializer({
serialize: (value) => {
const stringVal = typeof value === 'string' ? value : value.toString();
return stringVal.replace(ansiRegex(), '');
@ -69,7 +73,7 @@ describe('upgrade-deprecated-types', () => {
test: () => true,
});
expect(() =>
await expect(() =>
tsTransform(dedent`
import { ComponentMeta as Meta, ComponentStory as StoryFn } from '@storybook/react';
import { Cat } from './Cat';
@ -80,11 +84,11 @@ describe('upgrade-deprecated-types', () => {
export const A: StoryFn<typeof Cat> = (args) => <Cat {...args} />;
`)
).toThrowErrorMatchingSnapshot();
).rejects.toThrowErrorMatchingSnapshot();
});
it('upgrade namespaces', () => {
expect(
it('upgrade namespaces', async () => {
await expect(
tsTransform(dedent`
import * as SB from '@storybook/react';
import { Cat, CatProps } from './Cat';
@ -104,7 +108,7 @@ describe('upgrade-deprecated-types', () => {
export const E: SB.Story<CatProps> = (args) => <Cat {...args} />;
`)
).toMatchSnapshot();
).resolves.toMatchSnapshot();
});
});
});

View File

@ -110,7 +110,7 @@ function removeUnusedTemplates(csf: CsfFile) {
});
}
export default function transform(info: FileInfo, api: API, options: { parser?: string }) {
export default async function transform(info: FileInfo, api: API, options: { parser?: string }) {
const makeTitle = (userTitle?: string) => {
return userTitle || 'FIXME';
};
@ -204,7 +204,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
let output = printCsf(csf).code;
try {
const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
const prettierConfig = (await prettier.resolveConfig(info.path)) ?? {
printWidth: 100,
tabWidth: 2,
bracketSpacing: true,
@ -212,9 +212,8 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
singleQuote: true,
};
output = prettier.format(output, {
output = await prettier.format(output, {
...prettierConfig,
// This will infer the parser from the filename.
filepath: info.path,
});
} catch (e) {

View File

@ -24,7 +24,7 @@ import type { MdxFlowExpression } from 'mdast-util-mdx-expression';
const mdxProcessor = remark().use(remarkMdx) as ReturnType<typeof remark>;
export default function jscodeshift(info: FileInfo) {
export default async function jscodeshift(info: FileInfo) {
const parsed = path.parse(info.path);
let baseName = path.join(
@ -37,7 +37,7 @@ export default function jscodeshift(info: FileInfo) {
baseName += '_';
}
const result = transform(info.source, path.basename(baseName));
const result = await transform(info, path.basename(baseName));
const [mdx, csf] = result;
@ -48,8 +48,8 @@ export default function jscodeshift(info: FileInfo) {
return mdx;
}
export function transform(source: string, baseName: string): [string, string] {
const root = mdxProcessor.parse(source);
export async function transform(info: FileInfo, baseName: string): Promise<[string, string]> {
const root = mdxProcessor.parse(info.source);
const storyNamespaceName = nameToValidExport(`${baseName}Stories`);
const metaAttributes: Array<MdxJsxAttribute | MdxJsxExpressionAttribute> = [];
@ -291,7 +291,7 @@ export function transform(source: string, baseName: string): [string, string] {
const newMdx = mdxProcessor.stringify(root);
let output = recast.print(file.path.node).code;
const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
const prettierConfig = (await prettier.resolveConfig(`${info.path}.jsx`)) || {
printWidth: 100,
tabWidth: 2,
bracketSpacing: true,
@ -299,7 +299,10 @@ export function transform(source: string, baseName: string): [string, string] {
singleQuote: true,
};
output = prettier.format(output, { ...prettierConfig, filepath: `file.jsx` });
output = await prettier.format(output.trim(), {
...prettierConfig,
filepath: `${info.path}.jsx`,
});
return [newMdx, output];
}

View File

@ -24,7 +24,7 @@ import { sanitizeName, jscodeshiftToPrettierParser } from '../lib/utils';
*
* NOTES: only support chained storiesOf() calls
*/
export default function transformer(file, api, options) {
export default async function transformer(file, api, options) {
const LITERAL = ['ts', 'tsx'].includes(options.parser) ? 'StringLiteral' : 'Literal';
const j = api.jscodeshift;
@ -262,16 +262,24 @@ export default function transformer(file, api, options) {
return source;
}
const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
printWidth: 100,
tabWidth: 2,
bracketSpacing: true,
trailingComma: 'es5',
singleQuote: true,
};
let output = source;
return prettier.format(source, {
...prettierConfig,
parser: jscodeshiftToPrettierParser(options.parser),
});
try {
const prettierConfig = (await prettier.resolveConfig(file.path)) || {
printWidth: 100,
tabWidth: 2,
bracketSpacing: true,
trailingComma: 'es5',
singleQuote: true,
};
output = prettier.format(source, {
...prettierConfig,
parser: jscodeshiftToPrettierParser(options.parser),
});
} catch (e) {
logger.warn(`Failed to format ${file.path} with prettier`);
}
return output;
}

View File

@ -21,7 +21,7 @@ function migrateType(oldType: string) {
return oldType.replace('Component', '');
}
export default function transform(info: FileInfo, api: API, options: { parser?: string }) {
export default async function transform(info: FileInfo, api: API, options: { parser?: string }) {
// TODO what do I need to with the title?
const csf = loadCsf(info.source, { makeTitle: (title) => title });
const fileNode = csf._ast;
@ -36,7 +36,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
let output = printCsf(csf).code;
try {
const prettierConfig = prettier.resolveConfig.sync('.', { editorconfig: true }) || {
const prettierConfig = (await prettier.resolveConfig(info.path)) || {
printWidth: 100,
tabWidth: 2,
bracketSpacing: true,
@ -44,7 +44,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?:
singleQuote: true,
};
output = prettier.format(output, { ...prettierConfig, filepath: info.path });
output = await prettier.format(output, { ...prettierConfig, filepath: info.path });
} catch (e) {
logger.log(`Failed applying prettier to ${info.path}.`);
}

View File

@ -409,42 +409,63 @@ export class GenerateNewProjectOnInitError extends StorybookError {
template() {
return dedent`
There was an error while using ${this.data.packageManager} to create a new ${
this.data.projectType
} project.
this.data.projectType
} project.
${this.data.error instanceof Error ? this.data.error.message : ''}
`;
}
}
export class ConflictingVersionTagsError extends StorybookError {
export class UpgradeStorybookToLowerVersionError extends StorybookError {
readonly category = Category.CLI_UPGRADE;
readonly code = 1;
readonly code = 3;
template() {
return 'Cannot set both --tag and --prerelease. Use --tag=next to get the latest prerelease.';
}
}
export class UpgradeStorybookPackagesError extends StorybookError {
readonly category = Category.CLI_UPGRADE;
readonly code = 2;
constructor(public data: { command: string; args: string[]; errorMessage: string }) {
constructor(public data: { beforeVersion: string; currentVersion: string }) {
super();
}
template() {
return dedent`
There was an error while trying to upgrade your Storybook dependencies.
You are trying to upgrade Storybook to a lower version than the version currently installed. This is not supported.
Command:
${this.data.command} ${this.data.args.join(' ')}
Storybook version ${this.data.beforeVersion} was detected in your project, but you are trying to "upgrade" to version ${this.data.currentVersion}.
This usually happens when running the upgrade command without a version specifier, e.g. "npx storybook upgrade".
This will cause npm to run the globally cached storybook binary, which might be an older version.
Error:
${this.data.errorMessage}
Instead you should always run the Storybook CLI with a version specifier to force npm to download the latest version:
"npx storybook@latest upgrade"
`;
}
}
export class UpgradeStorybookToSameVersionError extends StorybookError {
readonly category = Category.CLI_UPGRADE;
readonly code = 4;
constructor(public data: { beforeVersion: string }) {
super();
}
template() {
return dedent`
You are trying to upgrade Storybook to the same version that is currently installed in the project, version ${this.data.beforeVersion}. This is not supported.
This usually happens when running the upgrade command without a version specifier, e.g. "npx storybook upgrade".
This will cause npm to run the globally cached storybook binary, which might be the same version that you already have.
This also happens if you're running the Storybook CLI that is locally installed in your project.
If you intended to upgrade to the latest version, you should always run the Storybook CLI with a version specifier to force npm to download the latest version:
"npx storybook@latest upgrade"
If you intended to re-run automigrations, you should run the "automigrate" command directly instead:
"npx storybook@${this.data.beforeVersion} automigrate"
`;
}
}

View File

@ -66,6 +66,7 @@
"@storybook/docs-mdx": "3.0.0",
"@storybook/global": "^5.0.0",
"@storybook/manager": "workspace:*",
"@storybook/manager-api": "workspace:*",
"@storybook/node-logger": "workspace:*",
"@storybook/preview-api": "workspace:*",
"@storybook/telemetry": "workspace:*",
@ -115,6 +116,7 @@
"entries": [
"./src/index.ts",
"./src/presets/common-preset.ts",
"./src/presets/common-manager.ts",
"./src/presets/common-override-preset.ts"
],
"platform": "node"

View File

@ -0,0 +1,21 @@
import { addons } from '@storybook/manager-api';
import { global } from '@storybook/global';
const STATIC_FILTER = 'static-filter';
addons.register(STATIC_FILTER, (api) => {
// FIXME: this ensures the filter is applied after the first render
// to avoid a strange race condition in Webkit only.
const excludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce((acc, entry) => {
const [tag, option] = entry;
if ((option as any).excludeFromSidebar) {
acc[tag] = true;
}
return acc;
}, {} as Record<string, boolean>);
api.experimental_setFilter(STATIC_FILTER, (item) => {
const tags = item.tags || [];
return tags.filter((tag) => excludeTags[tag]).length === 0;
});
});

View File

@ -359,3 +359,19 @@ export const resolvedReact = async (existing: any) => {
return existing;
}
};
/**
* Set up `dev-only`, `docs-only`, `test-only` tags out of the box
*/
export const tags = async (existing: any) => {
return {
...existing,
'dev-only': { excludeFromDocsStories: true },
'docs-only': { excludeFromSidebar: true },
'test-only': { excludeFromSidebar: true, excludeFromDocsStories: true },
};
};
export const managerEntries = async (existing: any, options: Options) => {
return [require.resolve('./common-manager'), ...(existing || [])];
};

View File

@ -7,3 +7,4 @@ declare module '@discoveryjs/json-ext';
declare module 'watchpack';
declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features'];
declare var TAGS_OPTIONS: import('@storybook/types').TagsOptions;

View File

@ -318,7 +318,7 @@ export class StoryIndexGenerator {
const name = this.options.docs.defaultName ?? 'Docs';
const { metaId } = indexInputs[0];
const { title } = entries[0];
const tags = indexInputs[0].tags || [];
const metaTags = indexInputs[0].metaTags || [];
const id = toId(metaId ?? title, name);
entries.unshift({
id,
@ -326,7 +326,7 @@ export class StoryIndexGenerator {
name,
importPath,
type: 'docs',
tags: [...tags, 'docs', ...(!hasAutodocsTag && !isStoriesMdx ? [AUTODOCS_TAG] : [])],
tags: [...metaTags, 'docs', ...(!hasAutodocsTag && !isStoriesMdx ? [AUTODOCS_TAG] : [])],
storiesImports: [],
});
}
@ -432,6 +432,7 @@ export class StoryIndexGenerator {
importPath,
storiesImports: sortedDependencies.map((dep) => dep.entries[0].importPath),
type: 'docs',
// FIXME: update this to use the index entry's metaTags once we update this to run on `IndexInputs`
tags: [...(result.tags || []), csfEntry ? 'attached-mdx' : 'unattached-mdx', 'docs'],
};
return docsEntry;
@ -528,10 +529,13 @@ export class StoryIndexGenerator {
const fileNameOrder = this.storyFileNames();
sortStoriesV7(sortableStories, storySortParameter, fileNameOrder);
return sortableStories.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {} as StoryIndex['entries']);
return sortableStories.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as StoryIndex['entries']
);
}
async getIndex() {

View File

@ -405,7 +405,6 @@ describe('docs entries from story extraction', () => {
"name": "docs",
"storiesImports": [],
"tags": [
"story-tag-from-indexer",
"docs",
"autodocs",
],
@ -466,8 +465,6 @@ describe('docs entries from story extraction', () => {
"name": "docs",
"storiesImports": [],
"tags": [
"autodocs",
"story-tag-from-indexer",
"docs",
],
"title": "A",
@ -577,8 +574,6 @@ describe('docs entries from story extraction', () => {
"name": "docs",
"storiesImports": [],
"tags": [
"stories-mdx",
"story-tag-from-indexer",
"docs",
],
"title": "A",

View File

@ -6,8 +6,8 @@ export async function buildOrThrow<T>(callback: () => Promise<T>): Promise<T> {
} catch (err: any) {
const builderErrors = err.errors as { text: string }[];
if (builderErrors) {
const inconsistentVersionsError = builderErrors.find((er) =>
er.text?.includes('No matching export')
const inconsistentVersionsError = builderErrors.find(
(er) => er.text?.includes('No matching export')
);
if (inconsistentVersionsError) {

View File

@ -38,8 +38,7 @@ export function createUpdateMessage(updateInfo: VersionCheck, version: string):
try {
const isPrerelease = semver.prerelease(updateInfo.data.latest.version);
const suffix = isPrerelease ? '@next upgrade --prerelease' : '@latest upgrade';
const upgradeCommand = `npx storybook${suffix}`;
const upgradeCommand = `npx storybook@${isPrerelease ? 'next' : 'latest'} upgrade`;
updateMessage =
updateInfo.success && semver.lt(version, updateInfo.data.latest.version)
? dedent`

View File

@ -79,7 +79,7 @@ describe('withTelemetry', () => {
it('does not send full error message when crash reports are disabled', async () => {
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({ enableCrashReports: false } as any),
apply: async () => ({ enableCrashReports: false }) as any,
});
await expect(async () =>
withTelemetry(
@ -99,7 +99,7 @@ describe('withTelemetry', () => {
it('does send error message when crash reports are enabled', async () => {
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({ enableCrashReports: true } as any),
apply: async () => ({ enableCrashReports: true }) as any,
});
await expect(async () =>
@ -120,7 +120,7 @@ describe('withTelemetry', () => {
it('does not send any error message when telemetry is disabled', async () => {
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({ disableTelemetry: true } as any),
apply: async () => ({ disableTelemetry: true }) as any,
});
await expect(async () =>
@ -141,7 +141,7 @@ describe('withTelemetry', () => {
it('does send error messages when telemetry is disabled, but crash reports are enabled', async () => {
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({ disableTelemetry: true, enableCrashReports: true } as any),
apply: async () => ({ disableTelemetry: true, enableCrashReports: true }) as any,
});
await expect(async () =>
@ -162,7 +162,7 @@ describe('withTelemetry', () => {
it('does not send full error messages when disabled crash reports are cached', async () => {
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({} as any),
apply: async () => ({}) as any,
});
vi.mocked(cache.get).mockResolvedValueOnce(false);
@ -184,7 +184,7 @@ describe('withTelemetry', () => {
it('does send error messages when enabled crash reports are cached', async () => {
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({} as any),
apply: async () => ({}) as any,
});
vi.mocked(cache.get).mockResolvedValueOnce(true);
@ -206,7 +206,7 @@ describe('withTelemetry', () => {
it('does not send full error messages when disabled crash reports are prompted', async () => {
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({} as any),
apply: async () => ({}) as any,
});
vi.mocked(cache.get).mockResolvedValueOnce(undefined);
vi.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: false });
@ -229,7 +229,7 @@ describe('withTelemetry', () => {
it('does send error messages when enabled crash reports are prompted', async () => {
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({} as any),
apply: async () => ({}) as any,
});
vi.mocked(cache.get).mockResolvedValueOnce(undefined);
vi.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: true });
@ -386,7 +386,7 @@ describe('getErrorLevel', () => {
};
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({ enableCrashReports: true } as any),
apply: async () => ({ enableCrashReports: true }) as any,
});
vi.mocked(cache.get).mockResolvedValueOnce(false);
@ -405,7 +405,7 @@ describe('getErrorLevel', () => {
};
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({ enableCrashReports: false } as any),
apply: async () => ({ enableCrashReports: false }) as any,
});
vi.mocked(cache.get).mockResolvedValueOnce(false);
@ -424,7 +424,7 @@ describe('getErrorLevel', () => {
};
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({ disableTelemetry: true } as any),
apply: async () => ({ disableTelemetry: true }) as any,
});
vi.mocked(cache.get).mockResolvedValueOnce(false);
@ -444,7 +444,7 @@ describe('getErrorLevel', () => {
vi.mocked(cache.get).mockResolvedValueOnce(true);
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({} as any),
apply: async () => ({}) as any,
});
const errorLevel = await getErrorLevel(options);
@ -462,7 +462,7 @@ describe('getErrorLevel', () => {
};
vi.mocked(loadAllPresets).mockResolvedValueOnce({
apply: async () => ({} as any),
apply: async () => ({}) as any,
});
vi.mocked(cache.get).mockResolvedValueOnce(undefined);

View File

@ -4,6 +4,20 @@ import { globToRegexp } from '@storybook/core-common';
import { importPipeline } from './importPipeline';
function adjustRegexToExcludeNodeModules(originalRegex: RegExp) {
const originalRegexString = originalRegex.source;
const startsWithCaret = originalRegexString.startsWith('^');
const excludeNodeModulesPattern = startsWithCaret ? '(?!.*node_modules)' : '^(?!.*node_modules)';
// Combine the new exclusion pattern with the original regex
const adjustedRegexString = startsWithCaret
? `^${excludeNodeModulesPattern}${originalRegexString.substring(1)}`
: excludeNodeModulesPattern + originalRegexString;
// Create and return the new regex
return new RegExp(adjustedRegexString);
}
export function webpackIncludeRegexp(specifier: NormalizedStoriesSpecifier) {
const { directory, files } = specifier;
@ -17,7 +31,9 @@ export function webpackIncludeRegexp(specifier: NormalizedStoriesSpecifier) {
const webpackIncludeGlob = ['.', '..'].includes(directory)
? files
: `${directoryWithoutLeadingDots}/${files}`;
const webpackIncludeRegexpWithCaret = globToRegexp(webpackIncludeGlob);
const webpackIncludeRegexpWithCaret = webpackIncludeGlob.includes('node_modules')
? globToRegexp(webpackIncludeGlob)
: adjustRegexToExcludeNodeModules(globToRegexp(webpackIncludeGlob));
// picomatch is creating an exact match, but we are only matching the end of the filename
return new RegExp(webpackIncludeRegexpWithCaret.source.replace(/^\^/, ''));
}

View File

@ -5,7 +5,8 @@ import type { EnrichCsfOptions } from '@storybook/csf-tools';
export type CsfPluginOptions = EnrichCsfOptions;
const STORIES_REGEX = /\.(story|stories)\.[tj]sx?$/;
// Ignore node_modules
const STORIES_REGEX = /(?<!node_modules.*)\.(story|stories)\.[tj]sx?$/;
const logger = console;

View File

@ -1098,6 +1098,8 @@ describe('CsfFile', () => {
- component-tag
- story-tag
- play-fn
metaTags: &ref_0
- component-tag
__id: component-id--a
- type: story
importPath: foo/bar.stories.js
@ -1109,6 +1111,7 @@ describe('CsfFile', () => {
- component-tag
- story-tag
- play-fn
metaTags: *ref_0
__id: component-id--b
`);
});
@ -1138,6 +1141,8 @@ describe('CsfFile', () => {
metaId: component-id
tags:
- component-tag
metaTags:
- component-tag
__id: custom-story-id
`);
});
@ -1169,6 +1174,11 @@ describe('CsfFile', () => {
- inherit-tag-dup
- story-tag
- story-tag-dup
metaTags:
- component-tag
- component-tag-dup
- component-tag-dup
- inherit-tag-dup
__id: custom-foo-title--a
`);
});

View File

@ -100,11 +100,14 @@ const parseExportsOrder = (init: t.Expression) => {
};
const sortExports = (exportByName: Record<string, any>, order: string[]) => {
return order.reduce((acc, name) => {
const namedExport = exportByName[name];
if (namedExport) acc[name] = namedExport;
return acc;
}, {} as Record<string, any>);
return order.reduce(
(acc, name) => {
const namedExport = exportByName[name];
if (namedExport) acc[name] = namedExport;
return acc;
},
{} as Record<string, any>
);
};
export interface CsfOptions {
@ -490,35 +493,38 @@ export class CsfFile {
if (self._metaAnnotations.play) {
self._meta.tags = [...(self._meta.tags || []), 'play-fn'];
}
self._stories = entries.reduce((acc, [key, story]) => {
if (!isExportStory(key, self._meta as StaticMeta)) {
return acc;
}
const id =
story.parameters?.__id ??
toId((self._meta?.id || self._meta?.title) as string, storyNameFromExport(key));
const parameters: Record<string, any> = { ...story.parameters, __id: id };
self._stories = entries.reduce(
(acc, [key, story]) => {
if (!isExportStory(key, self._meta as StaticMeta)) {
return acc;
}
const id =
story.parameters?.__id ??
toId((self._meta?.id || self._meta?.title) as string, storyNameFromExport(key));
const parameters: Record<string, any> = { ...story.parameters, __id: id };
const { includeStories } = self._meta || {};
if (
key === '__page' &&
(entries.length === 1 || (Array.isArray(includeStories) && includeStories.length === 1))
) {
parameters.docsOnly = true;
}
acc[key] = { ...story, id, parameters };
const { tags, play } = self._storyAnnotations[key];
if (tags) {
const node = t.isIdentifier(tags)
? findVarInitialization(tags.name, this._ast.program)
: tags;
acc[key].tags = parseTags(node);
}
if (play) {
acc[key].tags = [...(acc[key].tags || []), 'play-fn'];
}
return acc;
}, {} as Record<string, StaticStory>);
const { includeStories } = self._meta || {};
if (
key === '__page' &&
(entries.length === 1 || (Array.isArray(includeStories) && includeStories.length === 1))
) {
parameters.docsOnly = true;
}
acc[key] = { ...story, id, parameters };
const { tags, play } = self._storyAnnotations[key];
if (tags) {
const node = t.isIdentifier(tags)
? findVarInitialization(tags.name, this._ast.program)
: tags;
acc[key].tags = parseTags(node);
}
if (play) {
acc[key].tags = [...(acc[key].tags || []), 'play-fn'];
}
return acc;
},
{} as Record<string, StaticStory>
);
Object.keys(self._storyExports).forEach((key) => {
if (!isExportStory(key, self._meta as StaticMeta)) {
@ -571,6 +577,7 @@ export class CsfFile {
title: this.meta?.title,
metaId: this.meta?.id,
tags,
metaTags: this.meta?.tags,
__id: story.id,
};
});

View File

@ -53,6 +53,7 @@
"lodash": "^4.17.21"
},
"devDependencies": {
"@babel/preset-react": "^7.23.3",
"babel-plugin-react-docgen": "4.2.1",
"require-from-string": "^2.0.2",
"typescript": "^5.3.2"

View File

@ -248,12 +248,15 @@ export class Instrumenter {
cleanup() {
// Reset stories with retained state to their initial state, and drop the rest.
this.state = Object.entries(this.state).reduce((acc, [storyId, state]) => {
const retainedState = getRetainedState(state);
if (!retainedState) return acc;
acc[storyId] = Object.assign(getInitialState(), retainedState);
return acc;
}, {} as Record<StoryId, State>);
this.state = Object.entries(this.state).reduce(
(acc, [storyId, state]) => {
const retainedState = getRetainedState(state);
if (!retainedState) return acc;
acc[storyId] = Object.assign(getInitialState(), retainedState);
return acc;
},
{} as Record<StoryId, State>
);
const payload: SyncPayload = { controlStates: controlsDisabled, logItems: [] };
this.channel.emit(EVENTS.SYNC, payload);
// @ts-expect-error (TS doesn't know about this global variable)

View File

@ -69,7 +69,7 @@ export class AddonStore {
| Addon_Types
| Addon_TypesEnum.experimental_PAGE
| Addon_TypesEnum.experimental_SIDEBAR_BOTTOM
| Addon_TypesEnum.experimental_SIDEBAR_TOP
| Addon_TypesEnum.experimental_SIDEBAR_TOP,
>(type: T): Addon_Collection<Addon_TypesMapping[T]> | any {
if (!this.elements[type]) {
this.elements[type] = {};

View File

@ -92,16 +92,19 @@ export const transformSetStoriesStoryDataToPreparedStoryIndex = (
export const transformStoryIndexV2toV3 = (index: StoryIndexV2): StoryIndexV3 => {
return {
v: 3,
stories: Object.values(index.stories).reduce((acc, entry) => {
acc[entry.id] = {
...entry,
title: entry.kind,
name: entry.name || entry.story,
importPath: entry.parameters.fileName || '',
};
stories: Object.values(index.stories).reduce(
(acc, entry) => {
acc[entry.id] = {
...entry,
title: entry.kind,
name: entry.name || entry.story,
importPath: entry.parameters.fileName || '',
};
return acc;
}, {} as StoryIndexV3['stories']),
return acc;
},
{} as StoryIndexV3['stories']
),
};
};
@ -109,27 +112,30 @@ export const transformStoryIndexV3toV4 = (index: StoryIndexV3): API_PreparedStor
const countByTitle = countBy(Object.values(index.stories), 'title');
return {
v: 4,
entries: Object.values(index.stories).reduce((acc, entry: any) => {
let type: IndexEntry['type'] = 'story';
if (
entry.parameters?.docsOnly ||
(entry.name === 'Page' && countByTitle[entry.title] === 1)
) {
type = 'docs';
}
acc[entry.id] = {
type,
...(type === 'docs' && { tags: ['stories-mdx'], storiesImports: [] }),
...entry,
};
entries: Object.values(index.stories).reduce(
(acc, entry: any) => {
let type: IndexEntry['type'] = 'story';
if (
entry.parameters?.docsOnly ||
(entry.name === 'Page' && countByTitle[entry.title] === 1)
) {
type = 'docs';
}
acc[entry.id] = {
type,
...(type === 'docs' && { tags: ['stories-mdx'], storiesImports: [] }),
...entry,
};
// @ts-expect-error (we're removing something that should not be there)
delete acc[entry.id].story;
// @ts-expect-error (we're removing something that should not be there)
delete acc[entry.id].kind;
// @ts-expect-error (we're removing something that should not be there)
delete acc[entry.id].story;
// @ts-expect-error (we're removing something that should not be there)
delete acc[entry.id].kind;
return acc;
}, {} as API_PreparedStoryIndex['entries']),
return acc;
},
{} as API_PreparedStoryIndex['entries']
),
};
};
@ -215,11 +221,6 @@ export const transformStoryIndexToStoriesHash = (
startCollapsed: collapsedRoots.includes(id),
// Note that this will later get appended to the previous list of children (see below)
children: [childId],
// deprecated fields
isRoot: true,
isComponent: false,
isLeaf: false,
});
// Usually the last path/name pair will be displayed as a component,
// *unless* there are other stories that are more deeply nested under it
@ -240,10 +241,6 @@ export const transformStoryIndexToStoriesHash = (
...(childId && {
children: [childId],
}),
// deprecated fields
isRoot: false,
isComponent: true,
isLeaf: false,
});
} else {
acc[id] = merge<API_GroupEntry>((acc[id] || {}) as API_GroupEntry, {
@ -256,10 +253,6 @@ export const transformStoryIndexToStoriesHash = (
...(childId && {
children: [childId],
}),
// deprecated fields
isRoot: false,
isComponent: false,
isLeaf: false,
});
}
});
@ -272,12 +265,6 @@ export const transformStoryIndexToStoriesHash = (
parent: paths[paths.length - 1],
renderLabel,
prepared: !!item.parameters,
// deprecated fields
kind: item.title,
isRoot: false,
isComponent: false,
isLeaf: true,
} as API_DocsEntry | API_StoryEntry;
return acc;

View File

@ -27,7 +27,7 @@ export interface SubAPI {
| Addon_Types
| Addon_TypesEnum.experimental_PAGE
| Addon_TypesEnum.experimental_SIDEBAR_BOTTOM
| Addon_TypesEnum.experimental_SIDEBAR_TOP = Addon_Types
| Addon_TypesEnum.experimental_SIDEBAR_TOP = Addon_Types,
>(
type: T
) => Addon_Collection<Addon_TypesMapping[T]>;

View File

@ -57,10 +57,11 @@ export const init: ModuleFn = ({ fullAPI, store, provider }) => {
function getLatestWhatsNewPost(): Promise<WhatsNewData> {
provider.channel?.emit(REQUEST_WHATS_NEW_DATA);
return new Promise((resolve) =>
provider.channel?.once(RESULT_WHATS_NEW_DATA, ({ data }: { data: WhatsNewData }) =>
resolve(data)
)
return new Promise(
(resolve) =>
provider.channel?.once(RESULT_WHATS_NEW_DATA, ({ data }: { data: WhatsNewData }) =>
resolve(data)
)
);
}

View File

@ -1448,9 +1448,6 @@ describe('stories API', () => {
],
"depth": 0,
"id": "a",
"isComponent": true,
"isLeaf": false,
"isRoot": false,
"name": "a",
"parent": undefined,
"renderLabel": undefined,
@ -1460,10 +1457,6 @@ describe('stories API', () => {
"depth": 1,
"id": "a--1",
"importPath": "./a.ts",
"isComponent": false,
"isLeaf": true,
"isRoot": false,
"kind": "a",
"name": "1",
"parent": "a",
"prepared": false,
@ -1475,10 +1468,6 @@ describe('stories API', () => {
"depth": 1,
"id": "a--2",
"importPath": "./a.ts",
"isComponent": false,
"isLeaf": true,
"isRoot": false,
"kind": "a",
"name": "2",
"parent": "a",
"prepared": false,
@ -1524,9 +1513,6 @@ describe('stories API', () => {
],
"depth": 0,
"id": "a",
"isComponent": true,
"isLeaf": false,
"isRoot": false,
"name": "a",
"parent": undefined,
"renderLabel": undefined,
@ -1536,10 +1522,6 @@ describe('stories API', () => {
"depth": 1,
"id": "a--1",
"importPath": "./a.ts",
"isComponent": false,
"isLeaf": true,
"isRoot": false,
"kind": "a",
"name": "1",
"parent": "a",
"prepared": false,
@ -1572,9 +1554,6 @@ describe('stories API', () => {
],
"depth": 0,
"id": "a",
"isComponent": true,
"isLeaf": false,
"isRoot": false,
"name": "a",
"parent": undefined,
"renderLabel": undefined,
@ -1584,10 +1563,6 @@ describe('stories API', () => {
"depth": 1,
"id": "a--1",
"importPath": "./a.ts",
"isComponent": false,
"isLeaf": true,
"isRoot": false,
"kind": "a",
"name": "1",
"parent": "a",
"prepared": false,
@ -1599,10 +1574,6 @@ describe('stories API', () => {
"depth": 1,
"id": "a--2",
"importPath": "./a.ts",
"isComponent": false,
"isLeaf": true,
"isRoot": false,
"kind": "a",
"name": "2",
"parent": "a",
"prepared": false,

View File

@ -157,7 +157,6 @@ describe('initModule', () => {
type: 'story',
args: { a: 1, b: 2 },
initialArgs: { a: 1, b: 1 },
isLeaf: true,
}),
}),
});
@ -194,7 +193,7 @@ describe('initModule', () => {
state: { location: {} },
navigate,
fullAPI: Object.assign(fullAPI, {
getCurrentStoryData: () => ({ type: 'story', args: { a: 1 }, isLeaf: true }),
getCurrentStoryData: () => ({ type: 'story', args: { a: 1 } }),
}),
});

View File

@ -3,6 +3,7 @@ declare var __STORYBOOK_ADDONS_MANAGER: any;
declare var CONFIG_TYPE: string;
declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features'];
declare var TAGS_OPTIONS: import('@storybook/types').StorybookConfigRaw['tags'];
declare var REFS: any;
declare var VERSIONCHECK: any;
declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined;

View File

@ -221,14 +221,14 @@ const invalidHooksError = () =>
function getHooksContextOrNull<
TRenderer extends Renderer,
TArgs extends Args = Args
TArgs extends Args = Args,
>(): HooksContext<TRenderer, TArgs> | null {
return global.STORYBOOK_HOOKS_CONTEXT || null;
}
function getHooksContextOrThrow<
TRenderer extends Renderer,
TArgs extends Args = Args
TArgs extends Args = Args,
>(): HooksContext<TRenderer, TArgs> {
const hooks = getHooksContextOrNull<TRenderer, TArgs>();
if (hooks == null) {
@ -530,7 +530,7 @@ export function useChannel(eventMap: EventMap, deps: any[] = []) {
*/
export function useStoryContext<
TRenderer extends Renderer,
TArgs extends Args = Args
TArgs extends Args = Args,
>(): StoryContext<TRenderer> {
const { currentContext } = getHooksContextOrThrow<TRenderer, TArgs>();
if (currentContext == null) {
@ -576,7 +576,7 @@ export function useParameter<S>(parameterKey: string, defaultValue?: S): S | und
export function useArgs<TArgs extends Args = Args>(): [
TArgs,
(newArgs: Partial<TArgs>) => void,
(argNames?: (keyof TArgs)[]) => void
(argNames?: (keyof TArgs)[]) => void,
] {
const channel = addons.getChannel();
const { id: storyId, args } = useStoryContext<Renderer, TArgs>();

View File

@ -57,7 +57,7 @@ export const importFn: Mocked<ModuleImportFn> = vi.fn(
'./src/ComponentTwo.stories.js': componentTwoExports,
'./src/Introduction.mdx': unattachedDocsExports,
'./src/ExtraComponentOne.stories.js': extraComponentOneExports,
}[path] || {})
})[path] || {}
);
export const docsRenderer = {

View File

@ -38,7 +38,7 @@ const ansiConverter = new AnsiToHtml({
});
export class WebView implements View<HTMLElement> {
private currentLayoutClass?: typeof layoutClassMap[keyof typeof layoutClassMap] | null;
private currentLayoutClass?: (typeof layoutClassMap)[keyof typeof layoutClassMap] | null;
private testing = false;

View File

@ -12,10 +12,13 @@ import { MissingStoryAfterHmrError } from '@storybook/core-events/preview-errors
export type StorySpecifier = StoryId | { name: StoryName; title: ComponentTitle } | '*';
const getImportPathMap = memoize(1)((entries: StoryIndex['entries']) =>
Object.values(entries).reduce((acc, entry) => {
acc[entry.importPath] = acc[entry.importPath] || entry;
return acc;
}, {} as Record<Path, IndexEntry>)
Object.values(entries).reduce(
(acc, entry) => {
acc[entry.importPath] = acc[entry.importPath] || entry;
return acc;
},
{} as Record<Path, IndexEntry>
)
);
export class StoryIndexStore {

View File

@ -175,10 +175,13 @@ export class StoryStore<TRenderer extends Renderer> {
};
const list = await loadInBatches(importPaths);
return list.reduce((acc, { importPath, csfFile }) => {
acc[importPath] = csfFile;
return acc;
}, {} as Record<Path, CSFFile<TRenderer>>);
return list.reduce(
(acc, { importPath, csfFile }) => {
acc[importPath] = csfFile;
return acc;
},
{} as Record<Path, CSFFile<TRenderer>>
);
}
async cacheAllCSFFiles(): Promise<void> {

View File

@ -228,7 +228,7 @@ function preparePartialAnnotations<TRenderer extends Renderer>(
// eg. reactive proxies set by frameworks like SolidJS or Vue
export function prepareContext<
TRenderer extends Renderer,
TContext extends Pick<StoryContextForLoaders<TRenderer>, 'args' | 'argTypes' | 'globals'>
TContext extends Pick<StoryContextForLoaders<TRenderer>, 'args' | 'argTypes' | 'globals'>,
>(
context: TContext
): TContext & Pick<StoryContextForLoaders<TRenderer>, 'allArgs' | 'argsByTarget' | 'unmappedArgs'> {

View File

@ -11,6 +11,7 @@ declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features'];
declare var STORIES: any;
declare var DOCS_OPTIONS: any;
declare var TAGS_OPTIONS: import('@storybook/types').StorybookConfigRaw['tags'];
// To enable user code to detect if it is running in Storybook
declare var IS_STORYBOOK: boolean;

View File

@ -5,7 +5,7 @@ import { expect } from '@storybook/jest';
export default {
component: globalThis.Components.Pre,
tags: ['component-one', 'component-two'],
tags: ['component-one', 'component-two', 'autodocs'],
decorators: [
(storyFn: PartialStoryFn, context: StoryContext) => {
return storyFn({
@ -13,6 +13,7 @@ export default {
});
},
],
parameters: { chromatic: { disable: true } },
};
export const Inheritance = {
@ -23,4 +24,17 @@ export const Inheritance = {
tags: ['story-one', 'story-two', 'story'],
});
},
parameters: { chromatic: { disable: false } },
};
export const DocsOnly = {
tags: ['docs-only'],
};
export const TestOnly = {
tags: ['test-only'],
};
export const DevOnly = {
tags: ['dev-only'],
};

View File

@ -0,0 +1,11 @@
import { global as globalThis } from '@storybook/global';
export default {
component: globalThis.Components.Button,
tags: ['autodocs', 'test-only'],
parameters: { chromatic: { disable: true } },
};
export const Default = {
args: { label: 'Button' },
};

View File

@ -14,27 +14,27 @@ export interface StoryData {
const splitPathRegex = /\/([^/]+)\/(?:(.*)_)?([^/]+)?/;
export const parsePath: (path: string | undefined) => StoryData = memoize(1000)(
(path: string | undefined | null) => {
const result: StoryData = {
viewMode: undefined,
storyId: undefined,
refId: undefined,
};
export const parsePath: (path: string | undefined) => StoryData = memoize(1000)((
path: string | undefined | null
) => {
const result: StoryData = {
viewMode: undefined,
storyId: undefined,
refId: undefined,
};
if (path) {
const [, viewMode, refId, storyId] = path.toLowerCase().match(splitPathRegex) || [];
if (viewMode) {
Object.assign(result, {
viewMode,
storyId,
refId,
});
}
if (path) {
const [, viewMode, refId, storyId] = path.toLowerCase().match(splitPathRegex) || [];
if (viewMode) {
Object.assign(result, {
viewMode,
storyId,
refId,
});
}
return result;
}
);
return result;
});
interface Args {
[key: string]: any;
@ -157,27 +157,29 @@ export const stringifyQuery = (query: Query) =>
type Match = { path: string };
export const getMatch = memoize(1000)(
(current: string, target: string | RegExp, startsWith = true): Match | null => {
if (startsWith) {
if (typeof target !== 'string') {
throw new Error('startsWith only works with string targets');
}
const startsWithTarget = current && current.startsWith(target);
if (startsWithTarget) {
return { path: current };
}
return null;
export const getMatch = memoize(1000)((
current: string,
target: string | RegExp,
startsWith = true
): Match | null => {
if (startsWith) {
if (typeof target !== 'string') {
throw new Error('startsWith only works with string targets');
}
const currentIsTarget = typeof target === 'string' && current === target;
const matchTarget = current && target && current.match(target);
if (currentIsTarget || matchTarget) {
const startsWithTarget = current && current.startsWith(target);
if (startsWithTarget) {
return { path: current };
}
return null;
}
);
const currentIsTarget = typeof target === 'string' && current === target;
const matchTarget = current && target && current.match(target);
if (currentIsTarget || matchTarget) {
return { path: current };
}
return null;
});

View File

@ -49,7 +49,7 @@
"@storybook/types": "workspace:*",
"estraverse": "^5.2.0",
"lodash": "^4.17.21",
"prettier": "^2.8.0"
"prettier": "^3.1.1"
},
"devDependencies": {
"typescript": "^5.3.2"

View File

@ -1,4 +1,4 @@
import parseFlow from 'prettier/parser-flow';
import parseFlow from 'prettier/plugins/flow';
function parse(source) {
return parseFlow.parsers.flow.parse(source);

View File

@ -1,4 +1,4 @@
import parseJs from 'prettier/parser-babel';
import parseJs from 'prettier/plugins/babel';
function parse(source) {
try {

View File

@ -1,4 +1,4 @@
import parseTs from 'prettier/parser-typescript';
import parseTs from 'prettier/plugins/typescript';
function parse(source) {
try {

View File

@ -50,9 +50,8 @@
"@testing-library/dom": "^9.3.1",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/user-event": "14.3.0",
"@types/chai": "^4",
"@vitest/expect": "^0.34.2",
"@vitest/spy": "^0.34.1",
"@vitest/expect": "1.1.3",
"@vitest/spy": "^1.1.3",
"chai": "^4.3.7",
"util": "^0.12.4"
},

View File

@ -2,7 +2,7 @@ import {
spyOn,
isMockFunction,
fn,
spies as mocks,
mocks,
type MaybeMocked,
type MaybeMockedDeep,
type MaybePartiallyMocked,

View File

@ -84,30 +84,28 @@ export const createReset = memoize(1)(
})
);
export const createGlobal = memoize(1)(
({
color,
background,
typography,
}: {
color: Color;
background: Background;
typography: Typography;
}): Return => {
const resetStyles = createReset({ typography });
return {
...resetStyles,
body: {
...resetStyles.body,
color: color.defaultText,
background: background.app,
overflow: 'hidden',
},
export const createGlobal = memoize(1)(({
color,
background,
typography,
}: {
color: Color;
background: Background;
typography: Typography;
}): Return => {
const resetStyles = createReset({ typography });
return {
...resetStyles,
body: {
...resetStyles.body,
color: color.defaultText,
background: background.app,
overflow: 'hidden',
},
hr: {
...resetStyles.hr,
borderTop: `1px solid ${color.border}`,
},
};
}
);
hr: {
...resetStyles.hr,
borderTop: `1px solid ${color.border}`,
},
};
});

Some files were not shown because too many files have changed in this diff Show More