diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..3317e750e0c --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +34e364a0ca1d93555d36a7367d78e8e229493de8 +c0896915fb7fb9a8dd416b9aebca17abd909d1c1 +a41c227037e7e7249b8b376f838f4f8bcc3e3e59 +13c46e6c0b7f3dd8cf4ba42d1cfd6714f4777d54 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a2458954ccd..5180c2c1d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)! diff --git a/MIGRATION.md b/MIGRATION.md index 08a2907dac6..0a13cc59da8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -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). diff --git a/code/.eslintrc.js b/code/.eslintrc.js index 06b9f06ee75..6acd1c798be 100644 --- a/code/.eslintrc.js +++ b/code/.eslintrc.js @@ -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', diff --git a/code/.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch b/code/.yarn/patches/@vitest-expect-npm-1.1.3-2062bf533f.patch similarity index 67% rename from code/.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch rename to code/.yarn/patches/@vitest-expect-npm-1.1.3-2062bf533f.patch index 175c8fbcc34..ea5e834a06d 100644 --- a/code/.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch +++ b/code/.yarn/patches/@vitest-expect-npm-1.1.3-2062bf533f.patch @@ -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) || {}; diff --git a/code/addons/a11y/src/components/Report/HighlightToggle.tsx b/code/addons/a11y/src/components/Report/HighlightToggle.tsx index cff5ec7ceb9..90a81b26c06 100644 --- a/code/addons/a11y/src/components/Report/HighlightToggle.tsx +++ b/code/addons/a11y/src/components/Report/HighlightToggle.tsx @@ -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 = ({ toggleId, elementsToHighlight = [] }) => { diff --git a/code/addons/actions/package.json b/code/addons/actions/package.json index 2a2c9d4df7e..1ebc8b64e43 100644 --- a/code/addons/actions/package.json +++ b/code/addons/actions/package.json @@ -40,6 +40,7 @@ }, "main": "dist/index.js", "module": "dist/index.mjs", + "react-native": "dist/index.mjs", "types": "dist/index.d.ts", "typesVersions": { "*": { diff --git a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx index 87bf6e84f13..c8d4bcb9019 100644 --- a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx +++ b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx @@ -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, diff --git a/code/addons/docs/src/preview.ts b/code/addons/docs/src/preview.ts index 0d1183bd0cf..d983f454ccd 100644 --- a/code/addons/docs/src/preview.ts +++ b/code/addons/docs/src/preview.ts @@ -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); + 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 + ); + }, + }, }, }; diff --git a/code/addons/docs/src/typings.d.ts b/code/addons/docs/src/typings.d.ts index cfa3c4639f8..a3efeb653c8 100644 --- a/code/addons/docs/src/typings.d.ts +++ b/code/addons/docs/src/typings.d.ts @@ -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; diff --git a/code/addons/viewport/src/Tool.tsx b/code/addons/viewport/src/Tool.tsx index f365fb65d04..53e7bce1c45 100644 --- a/code/addons/viewport/src/Tool.tsx +++ b/code/addons/viewport/src/Tool.tsx @@ -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; diff --git a/code/builders/builder-manager/src/index.ts b/code/builders/builder-manager/src/index.ts index b7923a64a2b..d55a8bb2d89 100644 --- a/code/builders/builder-manager/src/index.ts +++ b/code/builders/builder-manager/src/index.ts @@ -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 ); diff --git a/code/builders/builder-manager/src/utils/data.ts b/code/builders/builder-manager/src/utils/data.ts index eb6754e7635..2b7fc15e137 100644 --- a/code/builders/builder-manager/src/utils/data.ts +++ b/code/builders/builder-manager/src/utils/data.ts @@ -14,6 +14,7 @@ export const getData = async (options: Options) => { const logLevel = options.presets.apply('logLevel'); const title = options.presets.apply('title'); const docsOptions = options.presets.apply('docs', {}); + const tagsOptions = options.presets.apply('tags', {}); const template = readTemplate('template.ejs'); const customHead = options.presets.apply('managerHead'); @@ -35,5 +36,6 @@ export const getData = async (options: Options) => { config, logLevel, favicon, + tagsOptions, }; }; diff --git a/code/builders/builder-manager/src/utils/template.ts b/code/builders/builder-manager/src/utils/template.ts index 0d7b67a1dff..4ccb2d50864 100644 --- a/code/builders/builder-manager/src/utils/template.ts +++ b/code/builders/builder-manager/src/utils/template.ts @@ -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>, logLevel: Promise, docsOptions: Promise, + tagsOptions: Promise, { 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, diff --git a/code/builders/builder-vite/input/iframe.html b/code/builders/builder-vite/input/iframe.html index 867a16a4a22..dd976d6c4ab 100644 --- a/code/builders/builder-vite/input/iframe.html +++ b/code/builders/builder-vite/input/iframe.html @@ -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'); diff --git a/code/builders/builder-vite/src/transform-iframe-html.ts b/code/builders/builder-vite/src/transform-iframe-html.ts index 8c054612516..a4a482b2f11 100644 --- a/code/builders/builder-vite/src/transform-iframe-html.ts +++ b/code/builders/builder-vite/src/transform-iframe-html.ts @@ -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('previewBody'); const logLevel = await presets.apply('logLevel', undefined); const docsOptions = await presets.apply('docs'); + const tagsOptions = await presets.apply('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('', headHtmlSnippet || '') .replace('', bodyHtmlSnippet || ''); } diff --git a/code/builders/builder-vite/src/vite-config.test.ts b/code/builders/builder-vite/src/vite-config.test.ts index 0dfd0534ee3..8f34e65277e 100644 --- a/code/builders/builder-vite/src/vite-config.test.ts +++ b/code/builders/builder-vite/src/vite-config.test.ts @@ -25,7 +25,7 @@ const dummyOptions: Options = { builder: {}, }, options: {}, - }[key]), + })[key], } as Presets, presetsList: [], }; diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index 97cde4815b6..ef510ef2378 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -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: [ { diff --git a/code/e2e-tests/tags.spec.ts b/code/e2e-tests/tags.spec.ts new file mode 100644 index 00000000000..37fb76fb814 --- /dev/null +++ b/code/e2e-tests/tags.spec.ts @@ -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(); + }); +}); diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts index 38b0e78cb4c..ce4cd6b09af 100644 --- a/code/e2e-tests/util.ts +++ b/code/e2e-tests/util.ts @@ -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(); } /** diff --git a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts index a4462ea1a3d..e75354a4bf9 100644 --- a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts +++ b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts @@ -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'], diff --git a/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts b/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts index e6db7384488..f7bb907831c 100644 --- a/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts +++ b/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts @@ -41,7 +41,10 @@ export class PropertyExtractor implements NgModuleMetadata { applicationProviders?: Array>; /* eslint-enable @typescript-eslint/lines-between-class-members */ - constructor(private metadata: NgModuleMetadata, private component?: any) { + constructor( + private metadata: NgModuleMetadata, + private component?: any + ) { this.init(); } diff --git a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/attribute-selector.component.ts b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/attribute-selector.component.ts index 58618870358..af082743914 100644 --- a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/attribute-selector.component.ts +++ b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/attribute-selector.component.ts @@ -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; diff --git a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/class-selector.component.ts b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/class-selector.component.ts index 3e81b5c5074..74dd70d7251 100644 --- a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/class-selector.component.ts +++ b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/class-selector.component.ts @@ -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; diff --git a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/multiple-selector.component.ts b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/multiple-selector.component.ts index 00a4b98c3bf..88d7020da50 100644 --- a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/multiple-selector.component.ts +++ b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/multiple-selector.component.ts @@ -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; diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index cb00d48ff9d..ab6a8fc9cad 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -99,7 +99,7 @@ npx storybook@latest init This framework is designed to work with Storybook 7. If you’re not already using v7, upgrade with this command: ```bash -npx storybook@latest upgrade --prerelease +npx storybook@latest upgrade ``` #### Automatic migration diff --git a/code/frameworks/nextjs/src/babel/plugins/jsx-pragma.ts b/code/frameworks/nextjs/src/babel/plugins/jsx-pragma.ts index 419b178d576..76e1eec9c66 100644 --- a/code/frameworks/nextjs/src/babel/plugins/jsx-pragma.ts +++ b/code/frameworks/nextjs/src/babel/plugins/jsx-pragma.ts @@ -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') ); diff --git a/code/frameworks/nextjs/src/babel/plugins/next-ssg-transform.ts b/code/frameworks/nextjs/src/babel/plugins/next-ssg-transform.ts index 5fa80c7e125..4c97b90e470 100644 --- a/code/frameworks/nextjs/src/babel/plugins/next-ssg-transform.ts +++ b/code/frameworks/nextjs/src/babel/plugins/next-ssg-transform.ts @@ -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; 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; if (refs.has(local) && !isIdentifierReferenced(local)) { diff --git a/code/frameworks/preact-vite/README.md b/code/frameworks/preact-vite/README.md index e418166a3b5..1e7d742e167 100644 --- a/code/frameworks/preact-vite/README.md +++ b/code/frameworks/preact-vite/README.md @@ -22,7 +22,7 @@ npx storybook@latest init This framework is designed to work with Storybook 7. If you’re not already using v7, upgrade with this command: ```bash -npx storybook@latest upgrade --prerelease +npx storybook@latest upgrade ``` #### Manual migration diff --git a/code/frameworks/sveltekit/README.md b/code/frameworks/sveltekit/README.md index c3c01c77189..0243fde57b2 100644 --- a/code/frameworks/sveltekit/README.md +++ b/code/frameworks/sveltekit/README.md @@ -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 you’re not already using v7, upgrade with this command: ```bash -npx storybook@latest upgrade --prerelease +npx storybook@latest upgrade ``` #### Automatic migration diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index 525eea05884..216181e7d4b 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -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", diff --git a/code/lib/cli/src/automigrate/fixes/angular-builders-multiproject.test.ts b/code/lib/cli/src/automigrate/fixes/angular-builders-multiproject.test.ts index 5d5b36f0c52..50844068d13 100644 --- a/code/lib/cli/src/automigrate/fixes/angular-builders-multiproject.test.ts +++ b/code/lib/cli/src/automigrate/fixes/angular-builders-multiproject.test.ts @@ -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 ); }); diff --git a/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts b/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts index 7c9e63bd4a5..c527b6434b3 100644 --- a/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts +++ b/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts @@ -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 ); }); diff --git a/code/lib/cli/src/automigrate/fixes/builder-vite.ts b/code/lib/cli/src/automigrate/fixes/builder-vite.ts index b1d31444e91..6a46f16f15b 100644 --- a/code/lib/cli/src/automigrate/fixes/builder-vite.ts +++ b/code/lib/cli/src/automigrate/fixes/builder-vite.ts @@ -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 = { 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}`, ]); } diff --git a/code/lib/cli/src/automigrate/fixes/new-frameworks.ts b/code/lib/cli/src/automigrate/fixes/new-frameworks.ts index 657372d70ea..f335d654050 100644 --- a/code/lib/cli/src/automigrate/fixes/new-frameworks.ts +++ b/code/lib/cli/src/automigrate/fixes/new-frameworks.ts @@ -218,10 +218,10 @@ export const newFrameworks: Fix = { ❌ 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 = { 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 = { 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. diff --git a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts b/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts index 4c3006b7a36..cf82bceb9ba 100644 --- a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts +++ b/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts @@ -32,8 +32,8 @@ export const nodeJsRequirement: Fix = { 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( diff --git a/code/lib/cli/src/automigrate/fixes/sb-scripts.ts b/code/lib/cli/src/automigrate/fixes/sb-scripts.ts index 24ac7854ef4..5192ccd4e18 100644 --- a/code/lib/cli/src/automigrate/fixes/sb-scripts.ts +++ b/code/lib/cli/src/automigrate/fixes/sb-scripts.ts @@ -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) => { - 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); + }, + {} as Record + ); }; /** @@ -111,10 +114,10 @@ export const sbScripts: Fix = { 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 = { 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); + const newScripts = Object.keys(storybookScripts).reduce( + (acc, scriptKey) => { + acc[scriptKey] = storybookScripts[scriptKey].after; + return acc; + }, + {} as Record + ); logger.log(); diff --git a/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts b/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts index 99bf4b8d550..ef06f654a65 100644 --- a/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts +++ b/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts @@ -47,8 +47,8 @@ export function getRequireWrapperName(config: ConfigFile) { doesVariableOrFunctionDeclarationExist(node, 'wrapForPnp') ? ['wrapForPnp'] : doesVariableOrFunctionDeclarationExist(node, defaultRequireWrapperName) - ? [defaultRequireWrapperName] - : [] + ? [defaultRequireWrapperName] + : [] ); if (declarationName.length) { diff --git a/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts b/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts index 860b68fb071..a350b23fd82 100644 --- a/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts +++ b/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts @@ -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; diff --git a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts index e23c29dcea4..4c51a53224e 100644 --- a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts +++ b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts @@ -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', diff --git a/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts b/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts index 7f62392c683..2232993d9aa 100644 --- a/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts +++ b/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts @@ -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' ); diff --git a/code/lib/cli/src/detect.ts b/code/lib/cli/src/detect.ts index 0e6878c43c8..1138b84e016 100644 --- a/code/lib/cli/src/detect.ts +++ b/code/lib/cli/src/detect.ts @@ -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 ( diff --git a/code/lib/cli/src/doctor/getIncompatibleAddons.ts b/code/lib/cli/src/doctor/getIncompatibleAddons.ts index 465ad661a89..0a0fc5dc2d1 100644 --- a/code/lib/cli/src/doctor/getIncompatibleAddons.ts +++ b/code/lib/cli/src/doctor/getIncompatibleAddons.ts @@ -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 } ) ); diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index fa9acb1f7f0..c514dfb35f2 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -76,15 +76,13 @@ command('remove ') .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 ', '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 ', '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 ', '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 ', 'Define an output directory') - .option('-b --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); }) diff --git a/code/lib/cli/src/generators/baseGenerator.ts b/code/lib/cli/src/generators/baseGenerator.ts index a0bedbf5597..51b5397008f 100644 --- a/code/lib/cli/src/generators/baseGenerator.ts +++ b/code/lib/cli/src/generators/baseGenerator.ts @@ -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'); diff --git a/code/lib/cli/src/generators/configure.ts b/code/lib/cli/src/generators/configure.ts index 38cd265df83..2d0a3e1f5fc 100644 --- a/code/lib/cli/src/generators/configure.ts +++ b/code/lib/cli/src/generators/configure.ts @@ -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 { diff --git a/code/lib/cli/src/initiate.ts b/code/lib/cli/src/initiate.ts index f32b824ab97..175bbcb7e18 100644 --- a/code/lib/cli/src/initiate.ts +++ b/code/lib/cli/src/initiate.ts @@ -224,7 +224,7 @@ const projectTypeInquirer = async ( process.exit(0); }; -async function doInitiate( +export async function doInitiate( options: CommandOptions, pkg: PackageJson ): Promise< diff --git a/code/lib/cli/src/js-package-manager/JsPackageManager.ts b/code/lib/cli/src/js-package-manager/JsPackageManager.ts index 17ec76ad34a..9555d08a90b 100644 --- a/code/lib/cli/src/js-package-manager/JsPackageManager.ts +++ b/code/lib/cli/src/js-package-manager/JsPackageManager.ts @@ -134,6 +134,8 @@ export abstract class JsPackageManager { done = commandLog('Installing dependencies'); + logger.log(); + try { await this.runInstall(); done(); diff --git a/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts b/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts index 2dd3251ead6..c57de55e1d2 100644 --- a/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts +++ b/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts @@ -192,7 +192,7 @@ export class Yarn1Proxy extends JsPackageManager { const existingVersions: Record = {}; const duplicatedDependencies: Record = {}; - 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; diff --git a/code/lib/cli/src/sandbox.ts b/code/lib/cli/src/sandbox.ts index 397fda11830..8bc306d3904 100644 --- a/code/lib/cli/src/sandbox.ts +++ b/code/lib/cli/src/sandbox.ts @@ -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( diff --git a/code/lib/cli/src/upgrade.test.ts b/code/lib/cli/src/upgrade.test.ts index 6fcea5f8459..69b85cbc1d2 100644 --- a/code/lib/cli/src/upgrade.test.ts +++ b/code/lib/cli/src/upgrade.test.ts @@ -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), + }; +}); 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); }); }); diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index f42cfd13f5c..8738f1bfea6 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -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; -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) => { + 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; + 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, diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 172a185e6a2..6c4ac7e72bb 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -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" }, diff --git a/code/lib/codemod/src/transforms/__tests__/csf-2-to-3.test.ts b/code/lib/codemod/src/transforms/__tests__/csf-2-to-3.test.ts index 65f77bb4ec2..a91946f345b 100644 --- a/code/lib/codemod/src/transforms/__tests__/csf-2-to-3.test.ts +++ b/code/lib/codemod/src/transforms/__tests__/csf-2-to-3.test.ts @@ -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 = () => ; export const B = (args) => - ), + export const Primary = { + render: () => ( + + ), - 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', () => { } 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', () => { `; - 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) =>