diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index fa9c9e593a0f..846e510aec6d 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,13 @@ +## 10.1.0-beta.4 + +- Angular: Migrate from RxJS to async/await in command builders and run Compodoc utility as spinner - [#33156](https://github.com/storybookjs/storybook/pull/33156), thanks @valentinpalkovic! +- CLI: Fix 'beforeVersion' evaluation for Storybook package - [#33141](https://github.com/storybookjs/storybook/pull/33141), thanks @valentinpalkovic! +- CLI: Update clack - [#33151](https://github.com/storybookjs/storybook/pull/33151), thanks @valentinpalkovic! +- Checklist: Data improvements - [#33129](https://github.com/storybookjs/storybook/pull/33129), thanks @ghengeveld! +- Guide: Collapse checklist items by default - [#33160](https://github.com/storybookjs/storybook/pull/33160), thanks @ghengeveld! +- React: Add isPackage flag to component imports for better package identification - [#33090](https://github.com/storybookjs/storybook/pull/33090), thanks @kasperpeulen! +- UI: Improve status handling in sidebar nodes - [#32965](https://github.com/storybookjs/storybook/pull/32965), thanks @yannbf! + ## 10.1.0-beta.3 - A11y: Make search clear button keyboard accessible - [#32590](https://github.com/storybookjs/storybook/pull/32590), thanks @ritoban23! diff --git a/code/addons/vitest/src/manager.tsx b/code/addons/vitest/src/manager.tsx index 5b1fd9471851..f12c41fe95d7 100644 --- a/code/addons/vitest/src/manager.tsx +++ b/code/addons/vitest/src/manager.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { Addon_TypesEnum } from 'storybook/internal/types'; +import { Addon_TypesEnum, SupportedBuilder } from 'storybook/internal/types'; import { a11yStatusStore, @@ -23,8 +23,7 @@ import { import { useTestProvider } from './use-test-provider-state'; addons.register(ADDON_ID, (api) => { - const storybookBuilder = (globalThis as any).STORYBOOK_BUILDER || ''; - if (storybookBuilder.includes('vite')) { + if (globalThis.STORYBOOK_BUILDER === SupportedBuilder.VITE) { const openPanel = (panelId: string) => { api.setSelectedPanel(panelId); api.togglePanel(true); diff --git a/code/addons/vitest/src/typings.d.ts b/code/addons/vitest/src/typings.d.ts index 199a788ad2dc..235b6170bdd9 100644 --- a/code/addons/vitest/src/typings.d.ts +++ b/code/addons/vitest/src/typings.d.ts @@ -1,5 +1,5 @@ declare const BROWSER_CONFIG: object; -declare var STORYBOOK_BUILDER: string | undefined; +declare var STORYBOOK_BUILDER: import('storybook/internal/types').SupportedBuilder | undefined; interface ImportMetaEnv { __STORYBOOK_URL__?: string; diff --git a/code/core/package.json b/code/core/package.json index afa2487e2079..b688c7e7700e 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -220,7 +220,7 @@ "@babel/parser": "^7.26.9", "@babel/traverse": "^7.26.9", "@babel/types": "^7.26.8", - "@clack/prompts": "1.0.0-alpha.6", + "@clack/prompts": "1.0.0-alpha.7", "@devtools-ds/object-inspector": "^1.1.2", "@discoveryjs/json-ext": "^0.5.3", "@emotion/cache": "^11.14.0", diff --git a/code/core/src/builder-manager/utils/framework.test.ts b/code/core/src/builder-manager/utils/framework.test.ts deleted file mode 100644 index cea3453294e2..000000000000 --- a/code/core/src/builder-manager/utils/framework.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { join } from 'node:path'; - -import { describe, expect, it } from 'vitest'; - -import { - pluckNameFromConfigProperty, - pluckStorybookPackageFromPath, - pluckThirdPartyPackageFromPath, -} from './framework'; - -describe('UTILITIES: Framework information', () => { - describe('UTILITY: pluckNameFromConfigProperty', () => { - it('should return undefined if the property is undefined', () => { - expect(pluckNameFromConfigProperty(undefined)).toBe(undefined); - }); - - it('should return the name if the property is a string', () => { - expect(pluckNameFromConfigProperty('foo')).toBe('foo'); - }); - - it('should return the name if the property is an object', () => { - expect(pluckNameFromConfigProperty({ name: 'foo' })).toBe('foo'); - }); - }); - - describe('UTILITY: pluckStorybookPackageFromPath', () => { - it('should return the package name if the path is a storybook package', () => { - const packagePath = join(process.cwd(), 'node_modules', '@storybook', 'foo'); - expect(pluckStorybookPackageFromPath(packagePath)).toBe('@storybook/foo'); - }); - - it('should return undefined if the path is not a storybook package', () => { - const packagePath = join(process.cwd(), 'foo'); - expect(pluckStorybookPackageFromPath(packagePath)).toBe(undefined); - }); - }); - - describe('UTILITY: pluckThirdPartyPackageFromPath', () => { - it('should return the package name if the path is a third party package', () => { - const packagePath = join(process.cwd(), 'node_modules', 'bar'); - expect(pluckThirdPartyPackageFromPath(packagePath)).toBe('bar'); - }); - - it('should return the given path if the path is not a third party package', () => { - const packagePath = join(process.cwd(), 'foo', 'bar', 'baz'); - expect(pluckThirdPartyPackageFromPath(packagePath)).toBe(packagePath); - }); - }); -}); diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts index 7a06fded0ee3..cff2475a123b 100644 --- a/code/core/src/builder-manager/utils/framework.ts +++ b/code/core/src/builder-manager/utils/framework.ts @@ -1,59 +1,27 @@ -import { sep } from 'node:path'; - -import { extractRenderer, getFrameworkName } from 'storybook/internal/common'; -import type { Options } from 'storybook/internal/types'; - -interface PropertyObject { - name: string; - options?: Record; -} - -type Property = string | PropertyObject | undefined; - -export const pluckNameFromConfigProperty = (property: Property) => { - if (!property) { - return undefined; - } - - return typeof property === 'string' ? property : property.name; -}; - -// For replacing Windows backslashes with forward slashes -const normalizePath = (packagePath: string) => packagePath.replaceAll(sep, '/'); - -export const pluckStorybookPackageFromPath = (packagePath: string) => - normalizePath(packagePath).match(/(@storybook\/.*)$/)?.[1]; - -export const pluckThirdPartyPackageFromPath = (packagePath: string) => - normalizePath(packagePath).split('node_modules/')[1] ?? packagePath; +import { + extractFrameworkPackageName, + frameworkPackages, + frameworkToRenderer, + getFrameworkName, +} from 'storybook/internal/common'; +import { type Options, SupportedBuilder } from 'storybook/internal/types'; export const buildFrameworkGlobalsFromOptions = async (options: Options) => { - const globals: Record = {}; + const globals: Record = {}; - const { builder } = await options.presets.apply('core'); + const builderConfig = (await options.presets.apply('core')).builder; + const builderName = typeof builderConfig === 'string' ? builderConfig : builderConfig?.name; + const builder = Object.values(SupportedBuilder).find((builder) => builderName?.includes(builder)); const frameworkName = await getFrameworkName(options); - const rendererName = await extractRenderer(frameworkName); - - if (rendererName) { - globals.STORYBOOK_RENDERER = rendererName ?? undefined; - } - - const resolvedPreviewBuilder = pluckNameFromConfigProperty(builder); - if (resolvedPreviewBuilder) { - globals.STORYBOOK_BUILDER = - pluckStorybookPackageFromPath(resolvedPreviewBuilder) ?? - pluckThirdPartyPackageFromPath(resolvedPreviewBuilder); - } - - const framework = pluckNameFromConfigProperty(await options.presets.apply('framework')); - if (framework) { - globals.STORYBOOK_FRAMEWORK = framework; - } - - if (options.networkAddress) { - globals.STORYBOOK_NETWORK_ADDRESS = options.networkAddress; - } + const frameworkPackageName = extractFrameworkPackageName(frameworkName); + const framework = frameworkPackages[frameworkPackageName]; + const renderer = frameworkToRenderer[framework]; + + globals.STORYBOOK_BUILDER = builder; + globals.STORYBOOK_FRAMEWORK = framework; + globals.STORYBOOK_RENDERER = renderer; + globals.STORYBOOK_NETWORK_ADDRESS = options.networkAddress; return globals; }; diff --git a/code/core/src/cli/AddonVitestService.constants.ts b/code/core/src/cli/AddonVitestService.constants.ts new file mode 100644 index 000000000000..238890792c99 --- /dev/null +++ b/code/core/src/cli/AddonVitestService.constants.ts @@ -0,0 +1,13 @@ +import { SupportedFramework } from '../types'; + +export const SUPPORTED_FRAMEWORKS: readonly SupportedFramework[] = [ + SupportedFramework.HTML_VITE, + SupportedFramework.NEXTJS_VITE, + SupportedFramework.PREACT_VITE, + SupportedFramework.REACT_NATIVE_WEB_VITE, + SupportedFramework.REACT_VITE, + SupportedFramework.SVELTE_VITE, + SupportedFramework.SVELTEKIT, + SupportedFramework.VUE3_VITE, + SupportedFramework.WEB_COMPONENTS_VITE, +]; diff --git a/code/core/src/cli/AddonVitestService.ts b/code/core/src/cli/AddonVitestService.ts index 8b15daeaa2a8..8a07313d4e5d 100644 --- a/code/core/src/cli/AddonVitestService.ts +++ b/code/core/src/cli/AddonVitestService.ts @@ -12,7 +12,8 @@ import * as find from 'empathic/find'; import { coerce, minVersion, satisfies, validRange } from 'semver'; import { dedent } from 'ts-dedent'; -import { SupportedBuilder, SupportedFramework } from '../types'; +import { SupportedBuilder, type SupportedFramework } from '../types'; +import { SUPPORTED_FRAMEWORKS } from './AddonVitestService.constants'; type Result = { compatible: boolean; @@ -38,17 +39,6 @@ export interface AddonVitestCompatibilityOptions { export class AddonVitestService { constructor(private readonly packageManager: JsPackageManager) {} - readonly supportedFrameworks: SupportedFramework[] = [ - SupportedFramework.HTML_VITE, - SupportedFramework.NEXTJS_VITE, - SupportedFramework.PREACT_VITE, - SupportedFramework.REACT_NATIVE_WEB_VITE, - SupportedFramework.REACT_VITE, - SupportedFramework.SVELTE_VITE, - SupportedFramework.SVELTEKIT, - SupportedFramework.VUE3_VITE, - SupportedFramework.WEB_COMPONENTS_VITE, - ]; /** * Collect all dependencies needed for @storybook/addon-vitest * @@ -196,7 +186,7 @@ export class AddonVitestService { } // Check renderer/framework support - const isFrameworkSupported = this.supportedFrameworks.some( + const isFrameworkSupported = SUPPORTED_FRAMEWORKS.some( (framework) => options.framework === framework ); diff --git a/code/core/src/common/js-package-manager/BUNProxy.ts b/code/core/src/common/js-package-manager/BUNProxy.ts index 3884c175db0b..a2030402413a 100644 --- a/code/core/src/common/js-package-manager/BUNProxy.ts +++ b/code/core/src/common/js-package-manager/BUNProxy.ts @@ -89,7 +89,10 @@ export class BUNProxy extends JsPackageManager { public async getModulePackageJSON(packageName: string): Promise { const wantedPath = join('node_modules', packageName, 'package.json'); - const packageJsonPath = find.up(wantedPath, { cwd: this.cwd, last: getProjectRoot() }); + const packageJsonPath = find.up(wantedPath, { + cwd: this.primaryPackageJson.operationDir, + last: getProjectRoot(), + }); if (!packageJsonPath) { return null; diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts index f749c3283e4a..5afb7b7b77f7 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.ts @@ -105,7 +105,7 @@ export abstract class JsPackageManager { abstract getRunCommand(command: string): string; /** Get the package.json file for a given module. */ - abstract getModulePackageJSON(packageName: string): Promise; + abstract getModulePackageJSON(packageName: string, cwd?: string): Promise; isStorybookInMonorepo() { const turboJsonPath = find.up(`turbo.json`, { last: getProjectRoot() }); diff --git a/code/core/src/common/js-package-manager/NPMProxy.ts b/code/core/src/common/js-package-manager/NPMProxy.ts index 7444b64f7aea..71ed0790002e 100644 --- a/code/core/src/common/js-package-manager/NPMProxy.ts +++ b/code/core/src/common/js-package-manager/NPMProxy.ts @@ -78,7 +78,10 @@ export class NPMProxy extends JsPackageManager { async getModulePackageJSON(packageName: string): Promise { const wantedPath = join('node_modules', packageName, 'package.json'); - const packageJsonPath = find.up(wantedPath, { cwd: this.cwd, last: getProjectRoot() }); + const packageJsonPath = find.up(wantedPath, { + cwd: this.primaryPackageJson.operationDir, + last: getProjectRoot(), + }); if (!packageJsonPath) { return null; diff --git a/code/core/src/common/js-package-manager/PNPMProxy.ts b/code/core/src/common/js-package-manager/PNPMProxy.ts index 4eb2122c1f83..c781d835daa3 100644 --- a/code/core/src/common/js-package-manager/PNPMProxy.ts +++ b/code/core/src/common/js-package-manager/PNPMProxy.ts @@ -158,7 +158,10 @@ export class PNPMProxy extends JsPackageManager { } const wantedPath = join('node_modules', packageName, 'package.json'); - const packageJsonPath = find.up(wantedPath, { cwd: this.cwd, last: getProjectRoot() }); + const packageJsonPath = find.up(wantedPath, { + cwd: this.primaryPackageJson.operationDir, + last: getProjectRoot(), + }); if (!packageJsonPath) { return null; diff --git a/code/core/src/common/js-package-manager/Yarn1Proxy.ts b/code/core/src/common/js-package-manager/Yarn1Proxy.ts index a13d7a13a9ed..69164b5d493f 100644 --- a/code/core/src/common/js-package-manager/Yarn1Proxy.ts +++ b/code/core/src/common/js-package-manager/Yarn1Proxy.ts @@ -78,7 +78,10 @@ export class Yarn1Proxy extends JsPackageManager { public async getModulePackageJSON(packageName: string): Promise { const wantedPath = join('node_modules', packageName, 'package.json'); - const packageJsonPath = find.up(wantedPath, { cwd: this.cwd, last: getProjectRoot() }); + const packageJsonPath = find.up(wantedPath, { + cwd: this.primaryPackageJson.operationDir, + last: getProjectRoot(), + }); if (!packageJsonPath) { return null; diff --git a/code/core/src/common/js-package-manager/Yarn2Proxy.ts b/code/core/src/common/js-package-manager/Yarn2Proxy.ts index a8d5975cec0a..bba32562b962 100644 --- a/code/core/src/common/js-package-manager/Yarn2Proxy.ts +++ b/code/core/src/common/js-package-manager/Yarn2Proxy.ts @@ -148,7 +148,7 @@ export class Yarn2Proxy extends JsPackageManager { // TODO: Remove pnp compatibility code in SB11 async getModulePackageJSON(packageName: string): Promise { const pnpapiPath = find.any(['.pnp.js', '.pnp.cjs'], { - cwd: this.cwd, + cwd: this.primaryPackageJson.operationDir, last: getProjectRoot(), }); diff --git a/code/core/src/common/utils/cli.ts b/code/core/src/common/utils/cli.ts index 70f5cb76b8bb..9b216fbee848 100644 --- a/code/core/src/common/utils/cli.ts +++ b/code/core/src/common/utils/cli.ts @@ -10,7 +10,6 @@ import uniqueString from 'unique-string'; import type { JsPackageManager } from '../js-package-manager'; import satelliteAddons from '../satellite-addons'; import storybookPackagesVersions from '../versions'; -import { rendererPackages } from './get-storybook-info'; const tempDir = () => realpath(os.tmpdir()); @@ -67,25 +66,6 @@ export function parseList(str: string): string[] { .filter((item) => item.length > 0); } -/** - * Given a package manager, returns the coerced version of Storybook. It tries to find renderer - * packages in the project and returns the coerced version of the first one found. Example: If - * - * @storybook/react version 8.0.0-alpha.14 is installed, it returns the coerced version 8.0.0 - */ -export async function getCoercedStorybookVersion(packageManager: JsPackageManager) { - const packages = ( - await Promise.all( - Object.keys(rendererPackages).map(async (pkg) => ({ - name: pkg, - version: (await packageManager.getModulePackageJSON(pkg))?.version ?? null, - })) - ) - ).filter(({ version }) => !!version); - - return packages[0]?.version || storybookPackagesVersions.storybook; -} - export function getEnvConfig(program: Record, configEnv: Record): void { Object.keys(configEnv).forEach((fieldName) => { const envVarName = configEnv[fieldName]; diff --git a/code/core/src/common/utils/get-storybook-info.ts b/code/core/src/common/utils/get-storybook-info.ts index ad83e36cf00f..88e77f038ba0 100644 --- a/code/core/src/common/utils/get-storybook-info.ts +++ b/code/core/src/common/utils/get-storybook-info.ts @@ -150,12 +150,12 @@ export const getStorybookInfo = async ( const frameworkValue = mainConfig.framework; const frameworkField = typeof frameworkValue === 'string' ? frameworkValue : frameworkValue?.name; const addons = getAddonNames(mainConfig); - const version = getStorybookVersionSpecifier(configDir); + const versionSpecifier = getStorybookVersionSpecifier(configDir); if (!frameworkField) { return { ...configInfo, - version, + versionSpecifier, addons, mainConfig, mainConfigPath: configInfo.mainConfigPath ?? undefined, @@ -183,7 +183,7 @@ export const getStorybookInfo = async ( addons, mainConfig, framework, - version, + versionSpecifier, renderer: renderer ?? undefined, builder: builder ?? undefined, frameworkPackage, diff --git a/code/core/src/manager-api/typings.d.ts b/code/core/src/manager-api/typings.d.ts index 8f029b75af12..7bf67c97fc33 100644 --- a/code/core/src/manager-api/typings.d.ts +++ b/code/core/src/manager-api/typings.d.ts @@ -7,6 +7,6 @@ declare var REFS: any; declare var VERSIONCHECK: any; declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined; declare var STORYBOOK_ADDON_STATE: Record; -declare var STORYBOOK_RENDERER: string | undefined; -declare var STORYBOOK_BUILDER: string | undefined; -declare var STORYBOOK_FRAMEWORK: string | undefined; +declare var STORYBOOK_FRAMEWORK: import('storybook/internal/types').SupportedFramework | undefined; +declare var STORYBOOK_RENDERER: import('storybook/internal/types').SupportedRenderer | undefined; +declare var STORYBOOK_BUILDER: import('storybook/internal/types').SupportedBuilder | undefined; diff --git a/code/core/src/manager/components/sidebar/ChecklistWidget.stories.tsx b/code/core/src/manager/components/sidebar/ChecklistWidget.stories.tsx index 39a93f282722..4cb1d1d25234 100644 --- a/code/core/src/manager/components/sidebar/ChecklistWidget.stories.tsx +++ b/code/core/src/manager/components/sidebar/ChecklistWidget.stories.tsx @@ -38,6 +38,7 @@ const meta = preview.meta({ ...initialState.items, controls: { status: 'accepted' }, renderComponent: { status: 'done' }, + installVitest: { status: 'done' }, moreComponents: { status: 'skipped' }, moreStories: { status: 'skipped' }, }, @@ -57,6 +58,7 @@ const play: PlayFunction = async ({ step }) => { ...initialState.items, controls: { status: 'accepted' }, renderComponent: { status: 'done' }, + installVitest: { status: 'done' }, viewports: { status: 'done' }, moreComponents: { status: 'skipped' }, moreStories: { status: 'skipped' }, @@ -73,10 +75,11 @@ const play: PlayFunction = async ({ step }) => { ...initialState.items, controls: { status: 'accepted' }, renderComponent: { status: 'done' }, + installVitest: { status: 'done' }, viewports: { status: 'done' }, moreComponents: { status: 'skipped' }, moreStories: { status: 'skipped' }, - installVitest: { status: 'skipped' }, + writeInteractions: { status: 'skipped' }, }, }); }); diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx index e015820d492f..d5466c184773 100644 --- a/code/core/src/manager/components/sidebar/ContextMenu.tsx +++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx @@ -1,5 +1,5 @@ import type { ComponentProps, FC, SyntheticEvent } from 'react'; -import React, { useMemo, useState } from 'react'; +import React, { useContext, useMemo, useState } from 'react'; import { PopoverProvider, TooltipLinkList } from 'storybook/internal/components'; import { @@ -7,6 +7,7 @@ import { type Addon_Collection, type Addon_TestProviderType, Addon_TypesEnum, + type StatusValue, } from 'storybook/internal/types'; import { CopyIcon, EditorIcon, EllipsisIcon } from '@storybook/icons'; @@ -18,7 +19,10 @@ import { styled } from 'storybook/theming'; import type { Link } from '../../../components/components/tooltip/TooltipLinkList'; import { Shortcut } from '../../container/Menu'; +import { getMostCriticalStatusValue } from '../../utils/status'; +import { UseSymbol } from './IconSymbols'; import { StatusButton } from './StatusButton'; +import { StatusContext } from './StatusContext'; import type { ExcludesNull } from './Tree'; const empty = { @@ -41,6 +45,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) const [hoverCount, setHoverCount] = useState(0); const [isOpen, setIsOpen] = useState(false); const [copyText, setCopyText] = React.useState('Copy story name'); + const { allStatuses, groupStatus } = useContext(StatusContext); const shortcutKeys = api.getShortcutKeys(); const enableShortcuts = !!shortcutKeys; @@ -85,7 +90,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) } return defaultLinks; - }, [context, copyText, enableShortcuts, shortcutKeys]); + }, [api, context, copyText, enableShortcuts, shortcutKeys]); const handlers = useMemo(() => { return { @@ -118,6 +123,69 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) const shouldRender = !context.refId && (providerLinks.length > 0 || links.length > 0 || topLinks.length > 0); + const isLeafNode = context.type === 'story' || context.type === 'docs'; + + const itemStatus = useMemo(() => { + let status: StatusValue = 'status-value:unknown'; + if (!context) { + return status; + } + + if (isLeafNode) { + const values = Object.values(allStatuses?.[context.id] || {}).map((s) => s.value); + status = getMostCriticalStatusValue(values); + } + + if (!isLeafNode) { + // On component/groups we only show non-ellipsis on hover on non-success status colors + const groupValue = groupStatus && groupStatus[context.id]; + status = + groupValue === 'status-value:success' || groupValue === undefined + ? 'status-value:unknown' + : groupValue; + } + + return status; + }, [allStatuses, groupStatus, context, isLeafNode]); + + const MenuIcon = useMemo(() => { + // On component/groups we only show non-ellipsis on hover on non-success statuses + if (context.type !== 'story' && context.type !== 'docs') { + if (itemStatus !== 'status-value:success' && itemStatus !== 'status-value:unknown') { + return ( + + + + ); + } + + return ; + } + + if (itemStatus === 'status-value:error') { + return ( + + + + ); + } + if (itemStatus === 'status-value:warning') { + return ( + + + + ); + } + if (itemStatus === 'status-value:success') { + return ( + + + + ); + } + return ; + }, [itemStatus, context.type]); + return useMemo(() => { // Never show the SidebarContextMenu in production if (globalThis.CONFIG_TYPE !== 'DEVELOPMENT') { @@ -141,15 +209,15 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) data-testid="context-menu" ariaLabel="Open context menu" type="button" - status="status-value:pending" + status={itemStatus} onClick={handlers.onOpen} > - + {MenuIcon} ) : null, }; - }, [context, handlers, isOpen, shouldRender, links, topLinks]); + }, [context, handlers, isOpen, shouldRender, links, topLinks, itemStatus, MenuIcon]); }; /** @@ -200,5 +268,5 @@ export function generateTestProviderLinks( content, }; }) - .filter(Boolean as any as ExcludesNull); + .filter(Boolean as unknown as ExcludesNull); } diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index 0d0862c72943..e240259acbff 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -216,7 +216,6 @@ const Node = React.memo(function Node(props) { } = props; const theme = useTheme(); const { isDesktop, isMobile, setMobileMenuOpen } = useLayout(); - const { counts, statusesByValue } = useStatusSummary(item); if (!isDisplayed) { return null; @@ -240,42 +239,8 @@ const Node = React.memo(function Node(props) { })); } - // TODO should this be updated for stories with tests? - if (item.type === 'component' || item.type === 'group') { - const links: Link[] = []; - const errorCount = counts['status-value:error']; - const warningCount = counts['status-value:warning']; - if (errorCount) { - links.push({ - id: 'errors', - icon: StatusIconMap['status-value:error'], - title: `${errorCount} ${errorCount === 1 ? 'story' : 'stories'} with errors`, - onClick: () => { - const [firstStoryId] = Object.entries(statusesByValue['status-value:error'])[0]; - onSelectStoryId(firstStoryId); - const errorStatuses = Object.values(statusesByValue['status-value:error']).flat(); - fullStatusStore.selectStatuses(errorStatuses); - }, - }); - } - if (warningCount) { - links.push({ - id: 'warnings', - icon: StatusIconMap['status-value:warning'], - title: `${warningCount} ${warningCount === 1 ? 'story' : 'stories'} with warnings`, - onClick: () => { - const [firstStoryId] = Object.entries(statusesByValue['status-value:warning'])[0]; - onSelectStoryId(firstStoryId); - const warningStatuses = Object.values(statusesByValue['status-value:warning']).flat(); - fullStatusStore.selectStatuses(warningStatuses); - }, - }); - } - return links; - } - return []; - }, [counts, item.id, item.type, onSelectStoryId, statuses, statusesByValue]); + }, [item.id, item.type, onSelectStoryId, statuses]); const id = createId(item.id, refId); const contextMenu = diff --git a/code/core/src/manager/settings/Checklist/Checklist.tsx b/code/core/src/manager/settings/Checklist/Checklist.tsx index 2fea864a1b49..32225c67747f 100644 --- a/code/core/src/manager/settings/Checklist/Checklist.tsx +++ b/code/core/src/manager/settings/Checklist/Checklist.tsx @@ -1,4 +1,4 @@ -import React, { createRef, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { Button, Collapsible, Listbox } from 'storybook/internal/components'; @@ -266,19 +266,10 @@ export const Checklist = ({ [itemsById, sectionsById] ); - const next = useMemo( - () => - Object.values(sections).findIndex(({ items }) => - items.some((item) => item.isOpen && item.isAvailable) - ), - [sections] - ); - return ( - {sections.map(({ id, title, items, progress }, index) => { - const hasTarget = items.some((item) => item.id === locationHash); - const collapsed = !hasTarget && (progress === 0 || progress === 100) && next !== index; + {sections.map(({ id, title, items, progress }) => { + const collapsed = progress === 100 && items.every((item) => item.id !== locationHash); return (
  • @@ -322,7 +313,7 @@ export const Checklist = ({ ...item }) => { const isChecked = isAccepted || isDone; - const isCollapsed = isChecked && item.id !== locationHash; + const isCollapsed = item.id !== locationHash; const isLocked = !!isLockedBy; const itemContent = content?.({ api }); diff --git a/code/core/src/node-logger/prompts/prompt-functions.ts b/code/core/src/node-logger/prompts/prompt-functions.ts index 30182d843900..731f557a11bf 100644 --- a/code/core/src/node-logger/prompts/prompt-functions.ts +++ b/code/core/src/node-logger/prompts/prompt-functions.ts @@ -1,5 +1,4 @@ -import { logger } from '../../client-logger'; -import { shouldLog } from '../logger'; +import { error, log, shouldLog } from '../logger'; import { wrapTextForClack, wrapTextForClackHint } from '../wrap-utils'; import { getPromptProvider } from './prompt-config'; import type { @@ -129,6 +128,20 @@ export const spinner = (options: SpinnerOptions): SpinnerInstance => { spinnerInstance.stop(message); } }, + cancel: (message?: string) => { + activeSpinner = null; + restoreConsoleLog(); + if (shouldLog('info')) { + spinnerInstance.cancel(message); + } + }, + error: (message?: string) => { + activeSpinner = null; + restoreConsoleLog(); + if (shouldLog('error')) { + spinnerInstance.error(message); + } + }, message: (text: string) => { if (shouldLog('info')) { spinnerInstance.message(text); @@ -138,7 +151,7 @@ export const spinner = (options: SpinnerOptions): SpinnerInstance => { return wrappedSpinner; } else { - const maybeLog = shouldLog('info') ? logger.log : (_: string) => {}; + const maybeLog = shouldLog('info') ? log : (_: string) => {}; return { start: (message) => { @@ -151,6 +164,18 @@ export const spinner = (options: SpinnerOptions): SpinnerInstance => { maybeLog(message); } }, + cancel: (message) => { + if (message) { + maybeLog(message); + } + }, + error: (message) => { + if (message) { + if (shouldLog('error')) { + error(message); + } + } + }, message: (message) => { maybeLog(message); }, @@ -199,7 +224,7 @@ export const taskLog = (options: TaskLogOptions): TaskLogInstance => { return wrappedTaskLog; } else { - const maybeLog = shouldLog('info') ? logger.log : (_: string) => {}; + const maybeLog = shouldLog('info') ? log : (_: string) => {}; return { message: (message: string) => { diff --git a/code/core/src/node-logger/prompts/prompt-provider-base.ts b/code/core/src/node-logger/prompts/prompt-provider-base.ts index b9b2bd9b54c9..54a1615e0890 100644 --- a/code/core/src/node-logger/prompts/prompt-provider-base.ts +++ b/code/core/src/node-logger/prompts/prompt-provider-base.ts @@ -46,6 +46,8 @@ export interface PromptOptions { export interface SpinnerInstance { start: (message?: string) => void; stop: (message?: string) => void; + cancel: (message?: string) => void; + error: (message?: string) => void; message: (text: string) => void; } diff --git a/code/core/src/node-logger/prompts/prompt-provider-clack.ts b/code/core/src/node-logger/prompts/prompt-provider-clack.ts index 38880c47dfca..a103c3db1ff4 100644 --- a/code/core/src/node-logger/prompts/prompt-provider-clack.ts +++ b/code/core/src/node-logger/prompts/prompt-provider-clack.ts @@ -105,6 +105,14 @@ export class ClackPromptProvider extends PromptProvider { logTracker.addLog('info', `${spinnerId}-stop: ${message}`); task.stop(message); }, + cancel: (message) => { + logTracker.addLog('info', `${spinnerId}-cancel: ${message}`); + task.cancel(message); + }, + error: (message) => { + logTracker.addLog('error', `${spinnerId}-error: ${message}`); + task.error(message); + }, }; } diff --git a/code/core/src/node-logger/tasks.ts b/code/core/src/node-logger/tasks.ts index cbd5b67569e5..de9ed3192521 100644 --- a/code/core/src/node-logger/tasks.ts +++ b/code/core/src/node-logger/tasks.ts @@ -158,12 +158,12 @@ export const executeTaskWithSpinner = async ( if (isAborted) { logTracker.addLog('info', `${intro} aborted`); - task.stop(CLI_COLORS.warning(`${intro} aborted`)); + task.cancel(CLI_COLORS.warning(`${intro} aborted`)); return; } const errorMessage = err instanceof Error ? (err.stack ?? err.message) : String(err); logTracker.addLog('error', error, { error: errorMessage }); - task.stop(CLI_COLORS.error(error)); + task.error(CLI_COLORS.error(error)); throw err; } finally { cleanup?.(); diff --git a/code/core/src/shared/checklist-store/checklistData.tsx b/code/core/src/shared/checklist-store/checklistData.tsx index dd202a3807e8..bc0103ae2321 100644 --- a/code/core/src/shared/checklist-store/checklistData.tsx +++ b/code/core/src/shared/checklist-store/checklistData.tsx @@ -9,10 +9,10 @@ import { STORY_INDEX_INVALIDATED, UPDATE_GLOBALS, } from 'storybook/internal/core-events'; -import type { - API_IndexHash, - API_PreparedIndexEntry, - API_StoryEntry, +import { + type API_IndexHash, + type API_PreparedIndexEntry, + type API_StoryEntry, } from 'storybook/internal/types'; import { type API, addons, internal_universalTestProviderStore } from 'storybook/manager-api'; @@ -27,6 +27,7 @@ import { ADDON_ID as ADDON_TEST_ID, STORYBOOK_ADDON_TEST_CHANNEL, } from '../../../../addons/vitest/src/constants'; +import { SUPPORTED_FRAMEWORKS } from '../../cli/AddonVitestService.constants'; import { ADDON_ID as ADDON_DOCS_ID } from '../../docs-tools/shared'; import { TourGuide } from '../../manager/components/TourGuide/TourGuide'; import type { initialState } from './checklistData.state'; @@ -119,12 +120,21 @@ export interface ChecklistData { }[]; } +const isExample = (id: string) => + id.startsWith('example-') || id.startsWith('configure-your-project--'); + const subscribeToIndex: ( condition: (entries: Record) => boolean ) => ChecklistData['sections'][number]['items'][number]['subscribe'] = (condition) => ({ api, done }) => { - const check = () => condition(api.getIndex()?.entries || {}); + const check = () => + condition( + Object.entries(api.getIndex()?.entries || {}).reduce( + (acc, [id, entry]) => (isExample(entry.id) ? acc : Object.assign(acc, { [id]: entry })), + {} as Record + ) + ); if (check()) { done(); } else { @@ -187,7 +197,10 @@ export const checklistData = { label: 'Render a component', criteria: 'A story finished rendering successfully', subscribe: ({ api, done }) => - api.on(STORY_FINISHED, ({ status }) => status === 'success' && done()), + api.on( + STORY_FINISHED, + ({ storyId, status }) => status === 'success' && !isExample(storyId) && done() + ), content: ({ api }) => ( <>

    @@ -301,8 +314,7 @@ export const Primary: Story = { criteria: 'At least 5 components exist in the index', subscribe: subscribeToIndex((entries) => { const stories = Object.values(entries).filter( - (entry): entry is API_StoryEntry => - entry.type === 'story' && !entry.id.startsWith('example-') + (entry): entry is API_StoryEntry => entry.type === 'story' ); const components = new Set(stories.map(({ title }) => title)); return components.size >= 5; @@ -342,8 +354,7 @@ export const Primary: Story = { criteria: 'At least 20 stories exist in the index', subscribe: subscribeToIndex((entries) => { const stories = Object.values(entries).filter( - (entry): entry is API_StoryEntry => - entry.type === 'story' && !entry.id.startsWith('example-') + (entry): entry is API_StoryEntry => entry.type === 'story' ); return stories.length >= 20; }), @@ -522,7 +533,9 @@ export default { id: 'installVitest', label: 'Install Vitest addon', afterCompletion: 'unavailable', - available: () => true, // TODO check for compatibility with the project + available: () => + !!globalThis.STORYBOOK_FRAMEWORK && + SUPPORTED_FRAMEWORKS.includes(globalThis.STORYBOOK_FRAMEWORK), criteria: '@storybook/addon-vitest registered in .storybook/main.js|ts', subscribe: ({ done }) => { if (addons.experimental_getRegisteredAddons().includes(ADDON_TEST_ID)) { @@ -665,9 +678,7 @@ export default { criteria: 'At least one story with a play or test function', subscribe: subscribeToIndex((entries) => Object.values(entries).some( - ({ id, tags }) => - !id.startsWith('example-') && - (tags?.includes('play-fn') || tags?.includes('test-fn')) + (entry) => entry.tags?.includes('play-fn') || entry.tags?.includes('test-fn') ) ), content: ({ api }) => ( @@ -1079,9 +1090,7 @@ export const Disabled: Story = { label: 'Automatically document your components', criteria: 'At least one component with the autodocs tag applied', subscribe: subscribeToIndex((entries) => - Object.values(entries).some( - ({ id, tags }) => !id.startsWith('example-') && tags?.includes('autodocs') - ) + Object.values(entries).some((entry) => entry.tags?.includes('autodocs')) ), content: ({ api }) => ( <> @@ -1091,12 +1100,14 @@ export const Disabled: Story = { and a description.

    - {`// Button.stories.ts + {`// Button.stories.js -export default { +const meta = { component: Button, tags: ['autodocs'], // πŸ‘ˆ Add this tag -}`} +} + +export default meta;`}

    That tag can also be applied in .storybook/preview.ts, to generate @@ -1138,9 +1149,7 @@ export default { label: 'Custom content with MDX', criteria: 'At least one MDX page', subscribe: subscribeToIndex((entries) => - Object.values(entries).some( - ({ id, type }) => type === 'docs' && !id.startsWith('example-') - ) + Object.values(entries).some((entry) => entry.type === 'docs') ), content: ({ api }) => ( <> diff --git a/code/core/src/telemetry/storybook-metadata.ts b/code/core/src/telemetry/storybook-metadata.ts index 56d4f7a357cb..5d420f2cabfd 100644 --- a/code/core/src/telemetry/storybook-metadata.ts +++ b/code/core/src/telemetry/storybook-metadata.ts @@ -241,7 +241,7 @@ export const computeStorybookMetadata = async ({ portableStoriesFileCount, applicationFileCount, storybookVersion: version, - storybookVersionSpecifier: storybookInfo.version ?? '', + storybookVersionSpecifier: storybookInfo.versionSpecifier ?? '', language, storybookPackages, addons, diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 1f4406111ada..3523f60f7f0c 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -684,7 +684,7 @@ export type CoreCommon_AddonInfo = { name: string; inEssentials: boolean }; export interface CoreCommon_StorybookInfo { addons: string[]; - version?: string; + versionSpecifier?: string; framework?: SupportedFramework; renderer?: SupportedRenderer; builder?: SupportedBuilder; diff --git a/code/core/src/typings.d.ts b/code/core/src/typings.d.ts index 9ae586492116..7426e797d488 100644 --- a/code/core/src/typings.d.ts +++ b/code/core/src/typings.d.ts @@ -7,10 +7,12 @@ declare var REFS: any; declare var VERSIONCHECK: any; declare var STORYBOOK_ADDON_STATE: Record; -declare var STORYBOOK_BUILDER: string | undefined; -declare var STORYBOOK_FRAMEWORK: string | undefined; +declare var STORYBOOK_BUILDER: import('./types/modules/builders').SupportedBuilder | undefined; +declare var STORYBOOK_FRAMEWORK: + | import('./types/modules/frameworks').SupportedFramework + | undefined; +declare var STORYBOOK_RENDERER: import('./types/modules/renderers').SupportedRenderer | undefined; declare var STORYBOOK_HOOKS_CONTEXT: any; -declare var STORYBOOK_RENDERER: string | undefined; declare var STORYBOOK_CURRENT_TASK_LOG: undefined | null | Array; declare var __STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER__: any; diff --git a/code/frameworks/angular/src/builders/build-storybook/index.ts b/code/frameworks/angular/src/builders/build-storybook/index.ts index dfc9deb1b438..191f93a16e31 100644 --- a/code/frameworks/angular/src/builders/build-storybook/index.ts +++ b/code/frameworks/angular/src/builders/build-storybook/index.ts @@ -10,7 +10,6 @@ import type { BuilderContext, BuilderHandlerFn, BuilderOutput, - BuilderOutputLike, Target, Builder as DevkitBuilder, } from '@angular-devkit/architect'; @@ -27,8 +26,6 @@ import type { import type { JsonObject } from '@angular-devkit/core'; import * as find from 'empathic/find'; import * as pkg from 'empathic/package'; -import { from, of, throwError } from 'rxjs'; -import { catchError, map, mapTo, switchMap } from 'rxjs/operators'; import { errorSummary, printErrorDetails } from '../utils/error-handler'; import { runCompodoc } from '../utils/run-compodoc'; @@ -70,94 +67,88 @@ export type StorybookBuilderOutput = JsonObject & BuilderOutput & { [key: string type StandaloneBuildOptions = StandaloneOptions & { outputDir: string }; -const commandBuilder: BuilderHandlerFn = ( +const commandBuilder: BuilderHandlerFn = async ( options, context -): BuilderOutputLike => { - const builder = from(setup(options, context)).pipe( - switchMap(({ tsConfig }) => { - const docTSConfig = find.up('tsconfig.doc.json', { - cwd: options.configDir, - last: getProjectRoot(), - }); - const runCompodoc$ = options.compodoc - ? runCompodoc( - { compodocArgs: options.compodocArgs, tsconfig: docTSConfig ?? tsConfig }, - context - ).pipe(mapTo({ tsConfig })) - : of({}); - - return runCompodoc$.pipe(mapTo({ tsConfig })); - }), - map(({ tsConfig }) => { - getEnvConfig(options, { - staticDir: 'SBCONFIG_STATIC_DIR', - outputDir: 'SBCONFIG_OUTPUT_DIR', - configDir: 'SBCONFIG_CONFIG_DIR', - }); - - const { - browserTarget, - stylePreprocessorOptions, - styles, - configDir, - docs, - loglevel, - test, - outputDir, - quiet, - enableProdMode = true, - webpackStatsJson, - statsJson, - debugWebpack, - disableTelemetry, - assets, - previewUrl, - sourceMap = false, - preserveSymlinks = false, - experimentalZoneless = !!(VERSION.major && Number(VERSION.major) >= 21), - } = options; - - const packageJsonPath = pkg.up({ cwd: __dirname }); - const packageJson = - packageJsonPath != null ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : null; - - const standaloneOptions: StandaloneBuildOptions = { - packageJson, - configDir, - ...(docs ? { docs } : {}), - loglevel, - outputDir, - test, - quiet, - enableProdMode, - disableTelemetry, - angularBrowserTarget: browserTarget, - angularBuilderContext: context, - angularBuilderOptions: { - ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), - ...(styles ? { styles } : {}), - ...(assets ? { assets } : {}), - sourceMap, - preserveSymlinks, - experimentalZoneless, - }, - tsConfig, - webpackStatsJson, - statsJson, - debugWebpack, - previewUrl, - }; - - return standaloneOptions; - }), - switchMap((standaloneOptions) => runInstance({ ...standaloneOptions, mode: 'static' })), - map(() => { - return { success: true }; - }) - ); - - return builder as any as BuilderOutput; +): Promise => { + logger.intro('Building Storybook'); + + const { tsConfig } = await setup(options, context); + + const docTSConfig = find.up('tsconfig.doc.json', { + cwd: options.configDir, + last: getProjectRoot(), + }); + + if (options.compodoc) { + await runCompodoc( + { compodocArgs: options.compodocArgs, tsconfig: docTSConfig ?? tsConfig }, + context + ); + } + + getEnvConfig(options, { + staticDir: 'SBCONFIG_STATIC_DIR', + outputDir: 'SBCONFIG_OUTPUT_DIR', + configDir: 'SBCONFIG_CONFIG_DIR', + }); + + const { + browserTarget, + stylePreprocessorOptions, + styles, + configDir, + docs, + loglevel, + test, + outputDir, + quiet, + enableProdMode = true, + webpackStatsJson, + statsJson, + debugWebpack, + disableTelemetry, + assets, + previewUrl, + sourceMap = false, + preserveSymlinks = false, + experimentalZoneless = !!(VERSION.major && Number(VERSION.major) >= 21), + } = options; + + const packageJsonPath = pkg.up({ cwd: __dirname }); + const packageJson = + packageJsonPath != null ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : null; + + const standaloneOptions: StandaloneBuildOptions = { + packageJson, + configDir, + ...(docs ? { docs } : {}), + loglevel, + outputDir, + test, + quiet, + enableProdMode, + disableTelemetry, + angularBrowserTarget: browserTarget, + angularBuilderContext: context, + angularBuilderOptions: { + ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), + ...(styles ? { styles } : {}), + ...(assets ? { assets } : {}), + sourceMap, + preserveSymlinks, + experimentalZoneless, + }, + tsConfig, + webpackStatsJson, + statsJson, + debugWebpack, + previewUrl, + }; + + await runInstance({ ...standaloneOptions, mode: 'static' }); + logger.outro('Storybook build completed successfully'); + return { success: true } as BuilderOutput; }; export default createBuilder(commandBuilder) as DevkitBuilder; @@ -182,9 +173,9 @@ async function setup(options: StorybookBuilderOptions, context: BuilderContext) }; } -function runInstance(options: StandaloneBuildOptions) { - return from( - withTelemetry( +async function runInstance(options: StandaloneBuildOptions) { + try { + await withTelemetry( 'build', { cliOptions: options, @@ -192,11 +183,12 @@ function runInstance(options: StandaloneBuildOptions) { printError: printErrorDetails, }, async () => { - logger.intro('Building storybook'); const result = await buildStaticStandalone(options); - logger.outro('Storybook build completed successfully'); return result; } - ) - ).pipe(catchError((error: any) => throwError(errorSummary(error)))); + ); + } catch (error) { + const summary = errorSummary(error); + throw new Error(summary); + } } diff --git a/code/frameworks/angular/src/builders/start-storybook/index.ts b/code/frameworks/angular/src/builders/start-storybook/index.ts index 8695d87662b0..e09d6089fb3b 100644 --- a/code/frameworks/angular/src/builders/start-storybook/index.ts +++ b/code/frameworks/angular/src/builders/start-storybook/index.ts @@ -26,8 +26,6 @@ import type { import type { JsonObject } from '@angular-devkit/core'; import * as find from 'empathic/find'; import * as pkg from 'empathic/package'; -import { Observable, from, of } from 'rxjs'; -import { map, mapTo, switchMap } from 'rxjs/operators'; import { errorSummary, printErrorDetails } from '../utils/error-handler'; import { runCompodoc } from '../utils/run-compodoc'; @@ -74,115 +72,110 @@ export type StorybookBuilderOptions = JsonObject & { export type StorybookBuilderOutput = JsonObject & BuilderOutput & {}; -const commandBuilder: BuilderHandlerFn = (options, context) => { - const builder = from(setup(options, context)).pipe( - switchMap(({ tsConfig }) => { - const docTSConfig = find.up('tsconfig.doc.json', { - cwd: options.configDir, - last: getProjectRoot(), - }); - - const runCompodoc$ = options.compodoc - ? runCompodoc( - { - compodocArgs: [...options.compodocArgs, ...(options.quiet ? ['--silent'] : [])], - tsconfig: docTSConfig ?? tsConfig, - }, - context - ).pipe(mapTo({ tsConfig })) - : of({}); - - return runCompodoc$.pipe(mapTo({ tsConfig })); - }), - map(({ tsConfig }) => { - getEnvConfig(options, { - port: 'SBCONFIG_PORT', - host: 'SBCONFIG_HOSTNAME', - staticDir: 'SBCONFIG_STATIC_DIR', - configDir: 'SBCONFIG_CONFIG_DIR', - ci: 'CI', - }); - - options.port = parseInt(`${options.port}`, 10); - - const { - browserTarget, - stylePreprocessorOptions, - styles, - ci, - configDir, - docs, - host, - https, - port, - quiet, - enableProdMode = false, - smokeTest, - sslCa, - sslCert, - sslKey, - disableTelemetry, - assets, - initialPath, - open, - debugWebpack, - loglevel, - webpackStatsJson, - statsJson, - previewUrl, - sourceMap = false, - preserveSymlinks = false, - experimentalZoneless = !!(VERSION.major && Number(VERSION.major) >= 21), - } = options; - - const packageJsonPath = pkg.up({ cwd: __dirname }); - const packageJson = - packageJsonPath != null ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : null; - - const standaloneOptions: StandaloneOptions = { - packageJson, - ci, - configDir, - ...(docs ? { docs } : {}), - host, - https, - port, - quiet, - enableProdMode, - smokeTest, - sslCa, - sslCert, - sslKey, - disableTelemetry, - angularBrowserTarget: browserTarget, - angularBuilderContext: context, - angularBuilderOptions: { - ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), - ...(styles ? { styles } : {}), - ...(assets ? { assets } : {}), - preserveSymlinks, - sourceMap, - experimentalZoneless, - }, - tsConfig, - initialPath, - open, - debugWebpack, - webpackStatsJson, - statsJson, - loglevel, - previewUrl, - }; - - return standaloneOptions; - }), - switchMap((standaloneOptions) => runInstance(standaloneOptions)), - map((port: number) => { - return { success: true, info: { port } }; - }) - ); - - return builder as any as BuilderOutput; +const commandBuilder: BuilderHandlerFn = async ( + options, + context +): Promise => { + logger.intro('Starting Storybook'); + + const { tsConfig } = await setup(options, context); + + const docTSConfig = find.up('tsconfig.doc.json', { + cwd: options.configDir, + last: getProjectRoot(), + }); + + if (options.compodoc) { + await runCompodoc( + { + compodocArgs: [...options.compodocArgs, ...(options.quiet ? ['--silent'] : [])], + tsconfig: docTSConfig ?? tsConfig, + }, + context + ); + } + + getEnvConfig(options, { + port: 'SBCONFIG_PORT', + host: 'SBCONFIG_HOSTNAME', + staticDir: 'SBCONFIG_STATIC_DIR', + configDir: 'SBCONFIG_CONFIG_DIR', + ci: 'CI', + }); + + options.port = parseInt(`${options.port}`, 10); + + const { + browserTarget, + stylePreprocessorOptions, + styles, + ci, + configDir, + docs, + host, + https, + port, + quiet, + enableProdMode = false, + smokeTest, + sslCa, + sslCert, + sslKey, + disableTelemetry, + assets, + initialPath, + open, + debugWebpack, + loglevel, + webpackStatsJson, + statsJson, + previewUrl, + sourceMap = false, + preserveSymlinks = false, + experimentalZoneless = !!(VERSION.major && Number(VERSION.major) >= 21), + } = options; + + const packageJsonPath = pkg.up({ cwd: __dirname }); + const packageJson = + packageJsonPath != null ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : null; + + const standaloneOptions: StandaloneOptions = { + packageJson, + ci, + configDir, + ...(docs ? { docs } : {}), + host, + https, + port, + quiet, + enableProdMode, + smokeTest, + sslCa, + sslCert, + sslKey, + disableTelemetry, + angularBrowserTarget: browserTarget, + angularBuilderContext: context, + angularBuilderOptions: { + ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), + ...(styles ? { styles } : {}), + ...(assets ? { assets } : {}), + preserveSymlinks, + sourceMap, + experimentalZoneless, + }, + tsConfig, + initialPath, + open, + debugWebpack, + webpackStatsJson, + statsJson, + loglevel, + previewUrl, + }; + + const startedPort = await runInstance(standaloneOptions); + return { success: true, info: { port: startedPort } } as BuilderOutput; }; export default createBuilder(commandBuilder) as DevkitBuilder; @@ -206,10 +199,9 @@ async function setup(options: StorybookBuilderOptions, context: BuilderContext) browserOptions.tsConfig, }; } -function runInstance(options: StandaloneOptions) { - return new Observable((observer) => { - // This Observable intentionally never complete, leaving the process running ;) - withTelemetry( +async function runInstance(options: StandaloneOptions) { + try { + const { port } = await withTelemetry( 'dev', { cliOptions: options, @@ -217,13 +209,12 @@ function runInstance(options: StandaloneOptions) { printError: printErrorDetails, }, () => { - logger.intro('Starting storybook'); return buildDevStandalone(options); } - ) - .then(({ port }) => observer.next(port)) - .catch((error) => { - observer.error(errorSummary(error)); - }); - }); + ); + return port; + } catch (error) { + const summarized = errorSummary(error); + throw new Error(String(summarized)); + } } diff --git a/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts b/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts index ebcf27f4c8e3..7ca3de67f9d4 100644 --- a/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts +++ b/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts @@ -1,7 +1,6 @@ import type { BuilderContext } from '@angular-devkit/architect'; // @ts-expect-error (TODO) import type { LoggerApi } from '@angular-devkit/core/src/logger'; -import { take } from 'rxjs/operators'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { runCompodoc } from './run-compodoc'; @@ -15,6 +14,13 @@ vi.mock('storybook/internal/common', () => ({ }), }, })); +vi.mock('storybook/internal/node-logger', () => ({ + prompt: { + executeTaskWithSpinner: async (fn: any) => { + await fn(); + }, + }, +})); const builderContextLoggerMock: LoggerApi = { createChild: vi.fn(), @@ -37,15 +43,13 @@ describe('runCompodoc', () => { } as BuilderContext; it('should run compodoc with tsconfig from context', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: [], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-p', 'path/to/tsconfig.json', '-d', 'path/to/project'], @@ -54,15 +58,13 @@ describe('runCompodoc', () => { }); it('should run compodoc with tsconfig from compodocArgs', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: ['-p', 'path/to/tsconfig.stories.json'], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-d', 'path/to/project', '-p', 'path/to/tsconfig.stories.json'], @@ -71,15 +73,13 @@ describe('runCompodoc', () => { }); it('should run compodoc with default output folder.', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: [], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-p', 'path/to/tsconfig.json', '-d', 'path/to/project'], @@ -88,15 +88,13 @@ describe('runCompodoc', () => { }); it('should run with custom output folder specified with --output compodocArgs', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: ['--output', 'path/to/customFolder'], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-p', 'path/to/tsconfig.json', '--output', 'path/to/customFolder'], @@ -105,15 +103,13 @@ describe('runCompodoc', () => { }); it('should run with custom output folder specified with -d compodocArgs', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: ['-d', 'path/to/customFolder'], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-p', 'path/to/tsconfig.json', '-d', 'path/to/customFolder'], diff --git a/code/frameworks/angular/src/builders/utils/run-compodoc.ts b/code/frameworks/angular/src/builders/utils/run-compodoc.ts index fd0a6353306a..e93aa9a40711 100644 --- a/code/frameworks/angular/src/builders/utils/run-compodoc.ts +++ b/code/frameworks/angular/src/builders/utils/run-compodoc.ts @@ -3,7 +3,7 @@ import { isAbsolute, relative } from 'node:path'; import { JsPackageManagerFactory } from 'storybook/internal/common'; import type { BuilderContext } from '@angular-devkit/architect'; -import { Observable } from 'rxjs'; +import { prompt } from 'storybook/internal/node-logger'; const hasTsConfigArg = (args: string[]) => args.indexOf('-p') !== -1; const hasOutputArg = (args: string[]) => @@ -15,35 +15,31 @@ const toRelativePath = (pathToTsConfig: string) => { return isAbsolute(pathToTsConfig) ? relative('.', pathToTsConfig) : pathToTsConfig; }; -export const runCompodoc = ( +export const runCompodoc = async ( { compodocArgs, tsconfig }: { compodocArgs: string[]; tsconfig: string }, context: BuilderContext -): Observable => { - return new Observable((observer) => { - const tsConfigPath = toRelativePath(tsconfig); - const finalCompodocArgs = [ - 'compodoc', - ...(hasTsConfigArg(compodocArgs) ? [] : ['-p', tsConfigPath]), - ...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot || '.'}`]), - ...compodocArgs, - ]; +): Promise => { + const tsConfigPath = toRelativePath(tsconfig); + const finalCompodocArgs = [ + 'compodoc', + ...(hasTsConfigArg(compodocArgs) ? [] : ['-p', tsConfigPath]), + ...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot || '.'}`]), + ...compodocArgs, + ]; - const packageManager = JsPackageManagerFactory.getPackageManager(); + const packageManager = JsPackageManagerFactory.getPackageManager(); - try { - packageManager - .runPackageCommand({ - args: finalCompodocArgs, - cwd: context.workspaceRoot, - }) - .then((result) => { - context.logger.info(result.stdout); - observer.next(); - observer.complete(); - }); - } catch (e) { - context.logger.error(e); - observer.error(); + await prompt.executeTaskWithSpinner( + () => + packageManager.runPackageCommand({ + args: finalCompodocArgs, + cwd: context.workspaceRoot, + }), + { + id: 'compodoc', + intro: 'Generating documentation with Compodoc', + success: 'Compodoc finished successfully', + error: 'Compodoc failed', } - }); + ); }; diff --git a/code/lib/cli-storybook/src/add.test.ts b/code/lib/cli-storybook/src/add.test.ts index d9a00130ee64..c5554cac28ee 100644 --- a/code/lib/cli-storybook/src/add.test.ts +++ b/code/lib/cli-storybook/src/add.test.ts @@ -85,7 +85,6 @@ vi.mock('storybook/internal/common', () => { getAbsolutePathWrapperName: MockWrapGetAbsolutePathUtils.getAbsolutePathWrapperName, wrapValueWithGetAbsolutePathWrapper: MockWrapGetAbsolutePathUtils.wrapValueWithGetAbsolutePathWrapper, - getCoercedStorybookVersion: vi.fn(() => '8.0.0'), versions: { storybook: '8.0.0', '@storybook/addon-docs': '8.0.0', diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.ts index 77948f6d458b..9e1151065a42 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.ts @@ -131,18 +131,11 @@ export const removeEssentials: Fix = { dryRun, packageManager, configDir, - storybookVersion, storiesPaths, mainConfigPath, previewConfigPath, }) { - const { - hasEssentials, - hasDocsDisabled, - hasDocsAddon, - additionalAddonsToRemove, - essentialsOptions, - } = result; + const { hasEssentials, hasDocsDisabled, additionalAddonsToRemove, essentialsOptions } = result; if (!hasEssentials && additionalAddonsToRemove.length === 0) { return; diff --git a/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts b/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts index 94e6269fa8bd..647944437211 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts @@ -8,7 +8,7 @@ import { getStorybookInfo, } from 'storybook/internal/common'; import type { PackageManagerName } from 'storybook/internal/common'; -import { frameworkToRenderer, getCoercedStorybookVersion } from 'storybook/internal/common'; +import { frameworkToRenderer } from 'storybook/internal/common'; import type { ConfigFile } from 'storybook/internal/csf-tools'; import { readConfig, writeConfig as writeConfigFile } from 'storybook/internal/csf-tools'; import { logger } from 'storybook/internal/node-logger'; @@ -101,11 +101,9 @@ export const getFrameworkOptions = ( export const getStorybookData = async ({ configDir: userDefinedConfigDir, - cwd, packageManagerName, }: { configDir?: string; - cwd?: string; packageManagerName?: PackageManagerName; cache?: boolean; }) => { @@ -115,7 +113,11 @@ export const getStorybookData = async ({ mainConfigPath: mainConfigPath, configDir: configDirFromScript, previewConfigPath, - } = await getStorybookInfo(userDefinedConfigDir, cwd); + versionSpecifier, + } = await getStorybookInfo( + userDefinedConfigDir, + userDefinedConfigDir ? dirname(userDefinedConfigDir) : undefined + ); const configDir = userDefinedConfigDir || configDirFromScript || '.storybook'; @@ -123,7 +125,7 @@ export const getStorybookData = async ({ const workingDir = isAbsolute(configDir) ? dirname(configDir) - : dirname(join(cwd ?? process.cwd(), configDir)); + : dirname(join(process.cwd(), configDir)); logger.debug('Getting stories paths...'); const storiesPaths = await getStoriesPathsFromConfig({ @@ -140,12 +142,15 @@ export const getStorybookData = async ({ }); logger.debug('Getting Storybook version...'); - const storybookVersion = await getCoercedStorybookVersion(packageManager); + const versionInstalled = (await packageManager.getModulePackageJSON('storybook'))?.version; return { configDir, mainConfig, - storybookVersion, + /** The version specifier of Storybook from the user's package.json */ + versionSpecifier, + /** The version of Storybook installed in the user's project */ + versionInstalled, mainConfigPath, previewConfigPath, packageManager, diff --git a/code/lib/cli-storybook/src/automigrate/index.test.ts b/code/lib/cli-storybook/src/automigrate/index.test.ts index 88b46248c5ea..e7e8f7553f9a 100644 --- a/code/lib/cli-storybook/src/automigrate/index.test.ts +++ b/code/lib/cli-storybook/src/automigrate/index.test.ts @@ -48,7 +48,7 @@ vi.mock('storybook/internal/node-logger', () => ({ }, })); -const fixes: Fix[] = [ +const fixes: Fix[] = [ { id: 'fix-1', @@ -88,7 +88,7 @@ class PackageManager implements Partial { } } -const packageManager = new PackageManager() as any as JsPackageManager; +const packageManager = new PackageManager() as unknown as JsPackageManager; const dryRun = false; const yes = true; @@ -113,17 +113,10 @@ const common = { storiesPaths: [], }; -const runFixWrapper = async ({ - beforeVersion, - storybookVersion, -}: { - beforeVersion: string; - storybookVersion: string; -}) => { +const runFixWrapper = async ({ storybookVersion }: { storybookVersion: string }) => { return runFixes({ ...common, storybookVersion, - beforeVersion, }); }; @@ -134,15 +127,13 @@ const runAutomigrateWrapper = async ({ beforeVersion: string; storybookVersion: string; }) => { - getStorybookData.mockImplementation(() => { - return { - ...common, - beforeVersion, - storybookVersion, - isLatest: true, - }; + getStorybookData.mockResolvedValue({ + ...common, + beforeVersion, + versionInstalled: storybookVersion, + isLatest: true, }); - return doAutomigrate({ configDir }); + return doAutomigrate({ configDir, fixes }); }; describe('runFixes', () => { @@ -161,7 +152,7 @@ describe('runFixes', () => { }); it('should be necessary to run fix-1 from SB 6.5.15 to 7.0.0', async () => { - const { fixResults } = await runFixWrapper({ beforeVersion, storybookVersion: '7.0.0' }); + const { fixResults } = await runFixWrapper({ storybookVersion: '7.0.0' }); expect(fixResults).toEqual({ 'fix-1': 'succeeded', @@ -182,7 +173,7 @@ describe('runFixes', () => { it('should fail if an error is thrown by migration', async () => { check1.mockRejectedValue(new Error('check1 error')); - const { fixResults } = await runFixWrapper({ beforeVersion, storybookVersion: '7.0.0' }); + const { fixResults } = await runFixWrapper({ storybookVersion: '7.0.0' }); expect(fixResults).toEqual({ 'fix-1': 'check_failed', @@ -190,7 +181,7 @@ describe('runFixes', () => { expect(run1).not.toHaveBeenCalled(); }); - it('should throw error if an error is thrown my migration', async () => { + it('should throw error if an error is thrown by migration', async () => { check1.mockRejectedValue(new Error('check1 error')); const result = runAutomigrateWrapper({ beforeVersion, storybookVersion: '7.0.0' }); diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index 937c2402ad3e..b2f6e0a74e21 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -46,7 +46,7 @@ export const doAutomigrate = async (options: AutofixOptionsFromCLI) => { mainConfig, mainConfigPath, previewConfigPath, - storybookVersion, + versionInstalled, configDir, packageManager, storiesPaths, @@ -55,7 +55,7 @@ export const doAutomigrate = async (options: AutofixOptionsFromCLI) => { packageManagerName: options.packageManager, }); - if (!storybookVersion) { + if (!versionInstalled) { throw new Error('Could not determine Storybook version'); } @@ -66,8 +66,7 @@ export const doAutomigrate = async (options: AutofixOptionsFromCLI) => { const outcome = await automigrate({ ...options, packageManager, - storybookVersion, - beforeVersion: storybookVersion, + storybookVersion: versionInstalled, mainConfigPath, mainConfig, previewConfigPath, @@ -114,7 +113,6 @@ export const automigrate = async ({ mainConfigPath, previewConfigPath, storybookVersion, - beforeVersion, renderer: rendererPackage, skipInstall, hideMigrationSummary = false, @@ -184,7 +182,6 @@ export const automigrate = async ({ mainConfig, mainConfigPath, storybookVersion, - beforeVersion, isUpgrade: !!isUpgrade, dryRun, yes, @@ -219,7 +216,6 @@ type RunFixesOptions = { previewConfigPath?: string; mainConfig: StorybookConfigRaw; storybookVersion: string; - beforeVersion: string; isUpgrade?: boolean; }; diff --git a/code/lib/cli-storybook/src/automigrate/types.ts b/code/lib/cli-storybook/src/automigrate/types.ts index 627f810097ee..3a58893c1ed6 100644 --- a/code/lib/cli-storybook/src/automigrate/types.ts +++ b/code/lib/cli-storybook/src/automigrate/types.ts @@ -77,8 +77,6 @@ export interface AutofixOptions extends Omit { const results: DoctorCheckResult[] = []; diff --git a/code/lib/cli-storybook/src/doctor/types.ts b/code/lib/cli-storybook/src/doctor/types.ts index 677c7f2cef30..35854a0119b4 100644 --- a/code/lib/cli-storybook/src/doctor/types.ts +++ b/code/lib/cli-storybook/src/doctor/types.ts @@ -9,7 +9,7 @@ export interface DoctorOptions { export interface ProjectDoctorData { configDir: string; packageManager: JsPackageManager; - storybookVersion: string; + storybookVersion?: string; mainConfig: StorybookConfigRaw; } diff --git a/code/lib/cli-storybook/src/util.ts b/code/lib/cli-storybook/src/util.ts index a1009b5f056c..df825575a6c4 100644 --- a/code/lib/cli-storybook/src/util.ts +++ b/code/lib/cli-storybook/src/util.ts @@ -290,15 +290,15 @@ const processProject = async ({ packageManager, previewConfigPath, storiesPaths, - storybookVersion: beforeVersion, + versionInstalled, } = await getStorybookData({ configDir }); // Validate version and upgrade compatibility - logger.debug(`${name} - Validating before version... ${beforeVersion}`); - validateVersion(beforeVersion); - const isCanary = isCanaryVersion(currentCLIVersion) || isCanaryVersion(beforeVersion); + logger.debug(`${name} - Validating before version... ${versionInstalled}`); + validateVersion(versionInstalled); + const isCanary = isCanaryVersion(currentCLIVersion) || isCanaryVersion(versionInstalled); logger.debug(`${name} - Validating upgrade compatibility...`); - validateUpgradeCompatibility(currentCLIVersion, beforeVersion, isCanary); + validateUpgradeCompatibility(currentCLIVersion, versionInstalled, isCanary); // Get version information from NPM logger.debug(`${name} - Fetching NPM version information...`); @@ -312,7 +312,7 @@ const processProject = async ({ const isCLIExactLatest = currentCLIVersion === latestCLIVersionOnNPM; const isCLIPrerelease = prerelease(currentCLIVersion) !== null; const isCLIExactPrerelease = currentCLIVersion === latestPrereleaseCLIVersionOnNPM; - const isUpgrade = lt(beforeVersion, currentCLIVersion); + const isUpgrade = lt(versionInstalled, currentCLIVersion); // Check for blockers let autoblockerCheckResults: AutoblockerResult[] | null = null; @@ -341,7 +341,7 @@ const processProject = async ({ isCLIPrerelease, isCLIExactLatest, isUpgrade, - beforeVersion, + beforeVersion: versionInstalled, currentCLIVersion, latestCLIVersionOnNPM: latestCLIVersionOnNPM!, isCLIExactPrerelease, diff --git a/code/lib/create-storybook/src/scaffold-new-project.ts b/code/lib/create-storybook/src/scaffold-new-project.ts index 11db471d014c..5e12c445d030 100644 --- a/code/lib/create-storybook/src/scaffold-new-project.ts +++ b/code/lib/create-storybook/src/scaffold-new-project.ts @@ -179,7 +179,7 @@ export const scaffoldNewProject = async ( cwd: targetDir, }); } catch (e) { - spinner.stop( + spinner.error( `Failed to create a new "${projectDisplayName}" project with ${packageManagerName}` ); throw new GenerateNewProjectOnInitError({ diff --git a/code/package.json b/code/package.json index 9d62ee88aa24..346727613cba 100644 --- a/code/package.json +++ b/code/package.json @@ -286,5 +286,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.1.0-beta.4" } diff --git a/code/renderers/react/src/componentManifest/fixtures.ts b/code/renderers/react/src/componentManifest/fixtures.ts index 075557eb84d1..e7597601f306 100644 --- a/code/renderers/react/src/componentManifest/fixtures.ts +++ b/code/renderers/react/src/componentManifest/fixtures.ts @@ -31,7 +31,7 @@ export const fsMocks = { /** * Primary UI component for user interaction - * @import import { Button } from '@design-system/components/Button'; + * @import import { Button } from '@design-system/components/override'; */ export const Button = ({ primary = false, @@ -60,7 +60,6 @@ export const fsMocks = { /** * Description from meta and very long. * @summary Component summary - * @import import { Header } from '@design-system/components/Header'; */ const meta = { component: Header, @@ -85,9 +84,6 @@ export const fsMocks = { onCreateAccount?: () => void; } - /** - * @import import { Header } from '@design-system/components/Header'; - */ export default ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (

    diff --git a/code/renderers/react/src/componentManifest/generator.test.ts b/code/renderers/react/src/componentManifest/generator.test.ts index 405034954ed9..a6a7f9602a6a 100644 --- a/code/renderers/react/src/componentManifest/generator.test.ts +++ b/code/renderers/react/src/componentManifest/generator.test.ts @@ -26,10 +26,10 @@ test('componentManifestGenerator generates correct id, name, description and exa "description": "Primary UI component for user interaction", "error": undefined, "id": "example-button", - "import": "import { Button } from \"@design-system/components/Button\";", + "import": "import { Button } from "@design-system/components/override";", "jsDocTags": { "import": [ - "import { Button } from '@design-system/components/Button';", + "import { Button } from '@design-system/components/override';", ], }, "name": "Button", @@ -38,7 +38,7 @@ test('componentManifestGenerator generates correct id, name, description and exa "actualName": "Button", "definedInFile": "./src/stories/Button.tsx", "description": "Primary UI component for user interaction - @import import { Button } from '@design-system/components/Button';", + @import import { Button } from '@design-system/components/override';", "displayName": "Button", "exportName": "Button", "methods": [], @@ -143,11 +143,8 @@ test('componentManifestGenerator generates correct id, name, description and exa "description": "Description from meta and very long.", "error": undefined, "id": "example-header", - "import": "import { Header } from \"@design-system/components/Header\";", + "import": "import { Header } from "some-package";", "jsDocTags": { - "import": [ - "import { Header } from '@design-system/components/Header';", - ], "summary": [ "Component summary", ], @@ -157,7 +154,7 @@ test('componentManifestGenerator generates correct id, name, description and exa "reactDocgen": { "actualName": "", "definedInFile": "./src/stories/Header.tsx", - "description": "@import import { Header } from '@design-system/components/Header';", + "description": "", "exportName": "default", "methods": [], "props": { diff --git a/code/renderers/react/src/componentManifest/getComponentImports.test.ts b/code/renderers/react/src/componentManifest/getComponentImports.test.ts index 83c07a4b71cd..b1602043bcde 100644 --- a/code/renderers/react/src/componentManifest/getComponentImports.test.ts +++ b/code/renderers/react/src/componentManifest/getComponentImports.test.ts @@ -13,17 +13,20 @@ beforeEach(() => { vol.fromJSON(fsMocks, '/app'); }); -const getImports = (code: string, packageName?: string, storyFilePath?: string) => - getComponentData({ +const getImports = (code: string, packageName?: string, storyFilePath?: string) => { + storyFilePath ??= '/app/src/stories/Button.stories.tsx'; + const { components, imports } = getComponentData({ csf: loadCsf(code, { makeTitle: (t?: string) => t ?? 'title' }).parse(), packageName, storyFilePath, }); + return { components: components.map(({ reactDocgen, ...rest }) => rest), imports }; +}; test('Get imports from multiple components', () => { const code = dedent` import type { Meta } from '@storybook/react'; - import { ButtonGroup } from '@design-system/button-group'; + import { ButtonGroup } from './button-group'; import { Button } from '@design-system/button'; const meta: Meta = { @@ -43,18 +46,22 @@ test('Get imports from multiple components', () => { "componentName": "Button", "importId": "@design-system/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, { "componentName": "ButtonGroup", - "importId": "@design-system/button-group", + "importId": "./button-group", "importName": "ButtonGroup", + "isPackage": false, "localImportName": "ButtonGroup", }, ], "imports": [ - "import { Button } from "@design-system/button";", - "import { ButtonGroup } from "@design-system/button-group";", + "import { Button } from "@design-system/components/override";", + "import { ButtonGroup } from "./button-group";", ], } ` @@ -63,7 +70,7 @@ test('Get imports from multiple components', () => { test('Namespace import with member usage', () => { const code = dedent` - import * as Accordion from '@ds/accordion'; + import * as Accordion from './accordion'; const meta = {}; export default meta; @@ -75,14 +82,15 @@ test('Namespace import with member usage', () => { "components": [ { "componentName": "Accordion.Root", - "importId": "@ds/accordion", + "importId": "./accordion", "importName": "Root", + "isPackage": false, "localImportName": "Accordion", "namespace": "Accordion", }, ], "imports": [ - "import * as Accordion from "@ds/accordion";", + "import * as Accordion from "./accordion";", ], } ` @@ -91,7 +99,7 @@ test('Namespace import with member usage', () => { test('Named import used as namespace object', () => { const code = dedent` - import { Accordion } from '@ds/accordion'; + import { Accordion } from './accordion'; const meta = {}; export default meta; @@ -103,13 +111,14 @@ test('Named import used as namespace object', () => { "components": [ { "componentName": "Accordion.Root", - "importId": "@ds/accordion", + "importId": "./accordion", "importName": "Accordion", + "isPackage": false, "localImportName": "Accordion", }, ], "imports": [ - "import { Accordion } from "@ds/accordion";", + "import { Accordion } from "./accordion";", ], } ` @@ -132,11 +141,14 @@ test('Default import', () => { "componentName": "Button", "importId": "@ds/button", "importName": "default", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import Button from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -145,7 +157,8 @@ test('Default import', () => { test('Alias named import and meta.component inclusion', () => { const code = dedent` - import DefaultComponent, { Button as Btn, Other } from '@ds/button'; + import DefaultComponent, { Button as Btn } from '@ds/button'; + import { Other } from './other'; const meta = { component: Btn }; export default meta; @@ -159,17 +172,22 @@ test('Alias named import and meta.component inclusion', () => { "componentName": "Btn", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Btn", + "path": "./src/stories/Button.tsx", }, { "componentName": "Other", - "importId": "@ds/button", + "importId": "./other", "importName": "Other", + "isPackage": false, "localImportName": "Other", }, ], "imports": [ - "import { Button as Btn, Other } from "@ds/button";", + "import { Button as Btn } from "@design-system/components/override";", + "import { Other } from "./other";", ], } ` @@ -192,11 +210,14 @@ test('Strip unused specifiers from the same import statement', () => { "componentName": "Btn", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Btn", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import { Button as Btn } from "@ds/button";", + "import { Button as Btn } from "@design-system/components/override";", ], } ` @@ -205,7 +226,7 @@ test('Strip unused specifiers from the same import statement', () => { test('Meta component with member and star import', () => { const code = dedent` - import * as Accordion from '@ds/accordion'; + import * as Accordion from './accordion'; const meta = { component: Accordion.Root }; export default meta; @@ -216,14 +237,15 @@ test('Meta component with member and star import', () => { "components": [ { "componentName": "Accordion.Root", - "importId": "@ds/accordion", + "importId": "./accordion", "importName": "Root", + "isPackage": false, "localImportName": "Accordion", "namespace": "Accordion", }, ], "imports": [ - "import * as Accordion from "@ds/accordion";", + "import * as Accordion from "./accordion";", ], } ` @@ -232,7 +254,8 @@ test('Meta component with member and star import', () => { test('Keeps multiple named specifiers and drops unused ones from same import', () => { const code = dedent` - import { Button, ButtonGroup, useHook } from '@ds/button'; + import { Button, useHook } from '@ds/button'; + import { ButtonGroup } from './button-group'; const meta = {}; export default meta; @@ -246,17 +269,22 @@ test('Keeps multiple named specifiers and drops unused ones from same import', ( "componentName": "Button", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, { "componentName": "ButtonGroup", - "importId": "@ds/button", + "importId": "./button-group", "importName": "ButtonGroup", + "isPackage": false, "localImportName": "ButtonGroup", }, ], "imports": [ - "import { Button, ButtonGroup } from "@ds/button";", + "import { Button } from "@design-system/components/override";", + "import { ButtonGroup } from "./button-group";", ], } ` @@ -279,11 +307,14 @@ test('Mixed default + named import: keep only default when only default used', ( "componentName": "Button", "importId": "@ds/button", "importName": "default", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import Button from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -306,11 +337,14 @@ test('Mixed default + named import: keep only named when only named (alias) used "componentName": "Btn", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Btn", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import { Button as Btn } from "@ds/button";", + "import { Button as Btn } from "@design-system/components/override";", ], } ` @@ -334,11 +368,14 @@ test('Per-specifier type import is dropped when mixing with value specifiers', ( "componentName": "Button", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import { Button } from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -347,7 +384,7 @@ test('Per-specifier type import is dropped when mixing with value specifiers', ( test('Namespace import used for multiple members kept once', () => { const code = dedent` - import * as DS from '@ds/ds'; + import * as DS from './ds'; const meta = {}; export default meta; @@ -359,21 +396,23 @@ test('Namespace import used for multiple members kept once', () => { "components": [ { "componentName": "DS.A", - "importId": "@ds/ds", + "importId": "./ds", "importName": "A", + "isPackage": false, "localImportName": "DS", "namespace": "DS", }, { "componentName": "DS.B", - "importId": "@ds/ds", + "importId": "./ds", "importName": "B", + "isPackage": false, "localImportName": "DS", "namespace": "DS", }, ], "imports": [ - "import * as DS from "@ds/ds";", + "import * as DS from "./ds";", ], } ` @@ -395,11 +434,14 @@ test('Default import kept when referenced only via meta.component', () => { "componentName": "Button", "importId": "@ds/button", "importName": "default", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import Button from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -423,11 +465,14 @@ test('Side-effect-only import is ignored', () => { "componentName": "Button", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import { Button } from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -438,93 +483,30 @@ test('Side-effect-only import is ignored', () => { test('Converts default relative import to import override when provided', () => { const code = dedent` - import Header from './Header'; + import Button from './Button'; const meta = {}; export default meta; - export const S =
    ; + export const S =