diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index e679b4025a4b..16331cb510af 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,8 @@ +## 8.6.0-beta.2 + +- CLI: Reimplement features prompt logic to handle `--yes` and fix `--features` - [#30534](https://github.com/storybookjs/storybook/pull/30534), thanks @ghengeveld! +- Telemetry: Don't count example stories towards CSF feature stats - [#30561](https://github.com/storybookjs/storybook/pull/30561), thanks @shilman! + ## 8.6.0-beta.1 - Builder-Vite: Fix defaulting to allowing all hosts - [#30523](https://github.com/storybookjs/storybook/pull/30523), thanks @JReinhold! diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts index 38e7212379fc..7f231fb9407e 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts @@ -345,6 +345,19 @@ describe('StoryIndexGenerator', () => { "title": "D", "type": "story", }, + "example-button--story-one": { + "componentPath": undefined, + "id": "example-button--story-one", + "importPath": "./src/Button.stories.ts", + "name": "Story One", + "tags": [ + "dev", + "test", + "foobar", + ], + "title": "Example/Button", + "type": "story", + }, "first-nested-deeply-f--story-one": { "componentPath": undefined, "id": "first-nested-deeply-f--story-one", @@ -599,6 +612,19 @@ describe('StoryIndexGenerator', () => { "title": "D", "type": "story", }, + "example-button--story-one": { + "componentPath": undefined, + "id": "example-button--story-one", + "importPath": "./src/Button.stories.ts", + "name": "Story One", + "tags": [ + "dev", + "test", + "foobar", + ], + "title": "Example/Button", + "type": "story", + }, "first-nested-deeply-f--story-one": { "componentPath": undefined, "id": "first-nested-deeply-f--story-one", @@ -767,6 +793,8 @@ describe('StoryIndexGenerator', () => { "a--story-one", "b--docs", "b--story-one", + "example-button--docs", + "example-button--story-one", "d--docs", "d--story-one", "h--docs", @@ -809,6 +837,8 @@ describe('StoryIndexGenerator', () => { "a--story-one", "b--docs", "b--story-one", + "example-button--docs", + "example-button--story-one", "d--docs", "d--story-one", "h--docs", @@ -1797,6 +1827,7 @@ describe('StoryIndexGenerator', () => { "second-nested-g--story-one", "componentreference--docs", "notitle--docs", + "example-button--story-one", "h--story-one", "componentpath-extension--story-one", "componentpath-noextension--story-one", @@ -1825,7 +1856,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(11); + expect(readCsfMock).toHaveBeenCalledTimes(12); readCsfMock.mockClear(); await generator.getIndex(); @@ -1883,7 +1914,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(11); + expect(readCsfMock).toHaveBeenCalledTimes(12); generator.invalidate(specifier, './src/B.stories.ts', false); @@ -1968,7 +1999,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(11); + expect(readCsfMock).toHaveBeenCalledTimes(12); generator.invalidate(specifier, './src/B.stories.ts', true); @@ -2007,7 +2038,7 @@ describe('StoryIndexGenerator', () => { const generator = new StoryIndexGenerator([specifier], options); await generator.initialize(); await generator.getIndex(); - expect(readCsfMock).toHaveBeenCalledTimes(11); + expect(readCsfMock).toHaveBeenCalledTimes(12); generator.invalidate(specifier, './src/B.stories.ts', true); diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.ts b/code/core/src/core-server/utils/StoryIndexGenerator.ts index 6b8ccf381794..882bf094dc85 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.ts @@ -5,6 +5,7 @@ import { dirname, extname, join, normalize, relative, resolve, sep } from 'node: import { commonGlobOptions, normalizeStoryPath } from '@storybook/core/common'; import { combineTags, storyNameFromExport, toId } from '@storybook/core/csf'; +import { isExampleStoryId } from '@storybook/core/telemetry'; import type { DocsIndexEntry, DocsOptions, @@ -269,7 +270,10 @@ export class StoryIndexGenerator { return item; } - addStats(item.extra.stats, statsSummary); + // don't count example stories towards feature usage stats + if (!isExampleStoryId(item.id)) { + addStats(item.extra.stats, statsSummary); + } // Drop extra data used for internal bookkeeping const { extra, ...existing } = item; diff --git a/code/core/src/core-server/utils/__mockdata__/src/Button.stories.ts b/code/core/src/core-server/utils/__mockdata__/src/Button.stories.ts new file mode 100644 index 000000000000..80d4b4e95cdf --- /dev/null +++ b/code/core/src/core-server/utils/__mockdata__/src/Button.stories.ts @@ -0,0 +1,8 @@ +const component = {}; +export default { + title: 'Example/Button', + component, + tags: ['foobar'], +}; + +export const StoryOne = {}; diff --git a/code/core/src/core-server/utils/stories-json.test.ts b/code/core/src/core-server/utils/stories-json.test.ts index d5ed690f1405..f2246080c9e5 100644 --- a/code/core/src/core-server/utils/stories-json.test.ts +++ b/code/core/src/core-server/utils/stories-json.test.ts @@ -255,6 +255,18 @@ describe('useStoriesJson', () => { "title": "docs2/Yabbadabbadooo", "type": "docs", }, + "example-button--story-one": { + "id": "example-button--story-one", + "importPath": "./src/Button.stories.ts", + "name": "Story One", + "tags": [ + "dev", + "test", + "foobar", + ], + "title": "Example/Button", + "type": "story", + }, "first-nested-deeply-f--story-one": { "id": "first-nested-deeply-f--story-one", "importPath": "./src/first-nested/deeply/F.stories.js", diff --git a/code/lib/create-storybook/src/bin/index.ts b/code/lib/create-storybook/src/bin/index.ts index 67f730404159..d3176590c93e 100644 --- a/code/lib/create-storybook/src/bin/index.ts +++ b/code/lib/create-storybook/src/bin/index.ts @@ -25,7 +25,7 @@ const createStorybookProgram = program // default value is false, but if the user sets STORYBOOK_DISABLE_TELEMETRY, it can be true process.env.STORYBOOK_DISABLE_TELEMETRY && process.env.STORYBOOK_DISABLE_TELEMETRY !== 'false' ) - .option('--features <...list>', 'What features of storybook are you interested in?') + .option('--features ', 'What features of storybook are you interested in?') .option('--debug', 'Get more logs in debug mode') .option('--enable-crash-reports', 'Enable sending crash reports to telemetry data') .option('-f --force', 'Force add Storybook') diff --git a/code/lib/create-storybook/src/generators/types.ts b/code/lib/create-storybook/src/generators/types.ts index cafcd91ad475..50947f7bbf8f 100644 --- a/code/lib/create-storybook/src/generators/types.ts +++ b/code/lib/create-storybook/src/generators/types.ts @@ -48,10 +48,12 @@ export type Generator = ( commandOptions?: CommandOptions ) => Promise; +export type GeneratorFeature = 'docs' | 'test'; + export type CommandOptions = { packageManager: PackageManagerName; usePnp?: boolean; - features: string[]; + features: GeneratorFeature[]; type?: ProjectType; force?: any; html?: boolean; diff --git a/code/lib/create-storybook/src/initiate.ts b/code/lib/create-storybook/src/initiate.ts index 27be8b720365..8bac8e8fe8b6 100644 --- a/code/lib/create-storybook/src/initiate.ts +++ b/code/lib/create-storybook/src/initiate.ts @@ -45,7 +45,7 @@ import svelteKitGenerator from './generators/SVELTEKIT'; import vue3Generator from './generators/VUE3'; import webComponentsGenerator from './generators/WEB-COMPONENTS'; import webpackReactGenerator from './generators/WEBPACK_REACT'; -import type { CommandOptions, GeneratorOptions } from './generators/types'; +import type { CommandOptions, GeneratorFeature, GeneratorOptions } from './generators/types'; import { packageVersions } from './ink/steps/checks/packageVersions'; import { vitestConfigFiles } from './ink/steps/checks/vitestConfigFiles'; import { currentDirectoryIsEmpty, scaffoldNewProject } from './scaffold-new-project'; @@ -294,27 +294,49 @@ export async function doInitiate(options: CommandOptions): Promise< const isInteractive = process.stdout.isTTY && !process.env.CI; - let features = options.features || isInteractive ? ['dev', 'docs', 'test'] : ['dev', 'docs']; + const selectableFeatures: Record = { + docs: 'Documentation', + test: 'Testing', + }; + let selectedFeatures = new Set(); + selectedFeatures.toString = () => + selectedFeatures.size === 0 + ? 'none' + : Array.from(selectedFeatures) + .map((f) => selectableFeatures[f]) + .join(', '); + + if (options.features?.length > 0) { + if (options.features.includes('docs')) { + selectedFeatures.add('docs'); + } + if (options.features.includes('test')) { + selectedFeatures.add('test'); + } + logger.log(`Selected features: ${selectedFeatures}`); + } else if (options.yes || !isInteractive) { + selectedFeatures.add('docs'); - if (isInteractive && !options.features) { + if (isInteractive) { + // Don't automatically add test feature in CI + selectedFeatures.add('test'); + } + logger.log(`Selected features: ${selectedFeatures}`); + } else { const out = await prompts({ type: 'multiselect', name: 'features', message: `What are you using Storybook for?`, - choices: [ - { title: 'Development', value: 'dev', selected: true, disabled: true }, - { title: 'Documentation', value: 'docs', selected: true }, - { title: 'Testing', value: 'test', selected: true }, - ], + choices: Object.entries(selectableFeatures).map(([value, title]) => ({ + title, + value, + selected: true, + })), }); - features = out.features; + selectedFeatures = new Set(out.features); } - if (!features.includes('dev')) { - features.push('dev'); - } - - const telemetryFeatures = [...features]; + const telemetryFeatures = ['dev', ...selectedFeatures]; // Check if the current directory is empty. if (options.force !== true && currentDirectoryIsEmpty(packageManager.type)) { @@ -377,7 +399,7 @@ export async function doInitiate(options: CommandOptions): Promise< } } - if (features.includes('test')) { + if (selectedFeatures.has('test')) { const packageVersionsData = await packageVersions.condition({ packageManager }, {} as any); if (packageVersionsData.type === 'incompatible') { const { ignorePackageVersions } = isInteractive @@ -393,14 +415,14 @@ export async function doInitiate(options: CommandOptions): Promise< ]) : { ignorePackageVersions: true }; if (ignorePackageVersions) { - features.splice(features.indexOf('test'), 1); + selectedFeatures.delete('test'); } else { process.exit(0); } } } - if (features.includes('test')) { + if (selectedFeatures.has('test')) { const vitestConfigFilesData = await vitestConfigFiles.condition( { babel, findUp, fs } as any, { directory: process.cwd() } as any @@ -419,7 +441,7 @@ export async function doInitiate(options: CommandOptions): Promise< ]) : { ignoreVitestConfigFiles: true }; if (ignoreVitestConfigFiles) { - features.splice(features.indexOf('test'), 1); + selectedFeatures.delete('test'); } else { process.exit(0); } @@ -430,11 +452,14 @@ export async function doInitiate(options: CommandOptions): Promise< await packageManager.installDependencies(); } - // update the mutated value - options.features = features; + // Update the options object with the selected features before passing it down to the generator + options.features = Array.from(selectedFeatures); const installResult = await installStorybook(projectType as ProjectType, packageManager, options); + // Sync features back because they may have been mutated by the generator (e.g. in case of undetected project type) + selectedFeatures = new Set(options.features); + if (!options.skipInstall) { await packageManager.installDependencies(); } @@ -484,7 +509,7 @@ export async function doInitiate(options: CommandOptions): Promise< ? `ng run ${installResult.projectName}:storybook` : packageManager.getRunStorybookCommand(); - if (features.includes('test')) { + if (selectedFeatures.has('test')) { logger.log( `> npx storybook@${versions.storybook} add @storybook/experimental-addon-test@${versions['@storybook/experimental-addon-test']}` ); @@ -498,6 +523,8 @@ export async function doInitiate(options: CommandOptions): Promise< boxen( dedent` Storybook was successfully installed in your project! 🎉 + Additional features: ${selectedFeatures} + To run Storybook manually, run ${picocolors.yellow( picocolors.bold(storybookCommand) )}. CTRL+C to stop. diff --git a/code/package.json b/code/package.json index 3fdee0a9c79b..e4b027212a25 100644 --- a/code/package.json +++ b/code/package.json @@ -303,5 +303,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.6.0-beta.2" } diff --git a/docs/configure/telemetry.mdx b/docs/configure/telemetry.mdx index 56a6ef8cc707..5b0327ed46ed 100644 --- a/docs/configure/telemetry.mdx +++ b/docs/configure/telemetry.mdx @@ -42,7 +42,7 @@ Specifically, we track the following information in our telemetry events: * Testing tools (e.g. [Jest](https://jestjs.io/), [Vitest](https://vitest.dev/), [Playwright](https://playwright.dev/)). * Package manager information (e.g., `npm`, `yarn`). * Monorepo information (e.g., [NX](https://nx.dev/), [Turborepo](https://turborepo.org/)). -* In-app events (e.g., [Storybook guided tour](https://github.com/storybookjs/addon-onboarding)). +* In-app events (e.g., [Storybook guided tour](https://github.com/storybookjs/addon-onboarding), [UI test run](https://storybook.js.org/docs/writing-tests/test-addon#storybook-ui)). Access to the raw data is highly controlled, limited to select members of Storybook's core team who maintain the telemetry. We cannot identify individual users from the dataset: it is anonymized and untraceable back to the user. @@ -75,9 +75,10 @@ Will generate the following output: "exampleDocsCount": 3, "onboardingStoryCount": 0, "onboardingDocsCount": 0, - "version": 4 + "version": 5 }, "storyStats": { + "factory": 0, "play": 0, "render": 1, "loaders": 0, @@ -85,7 +86,8 @@ Will generate the following output: "globals": 0, "storyFn": 5, "mount": 0, - "moduleMock": 0 + "moduleMock": 0, + "tags": 0 } }, "metadata": { diff --git a/docs/versions/next.json b/docs/versions/next.json index 0472092899df..b50c6656f439 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.6.0-beta.1","info":{"plain":"- Builder-Vite: Fix defaulting to allowing all hosts - [#30523](https://github.com/storybookjs/storybook/pull/30523), thanks @JReinhold!\n- React: Fix incorrect import in preview.ts - [#30542](https://github.com/storybookjs/storybook/pull/30542), thanks @mrginglymus!\n- Tags: Add story/meta usage telemetry - [#30555](https://github.com/storybookjs/storybook/pull/30555), thanks @shilman!\n- UI: Fix tags sort for browser back-compat - [#30547](https://github.com/storybookjs/storybook/pull/30547), thanks @shilman!"}} +{"version":"8.6.0-beta.2","info":{"plain":"- CLI: Reimplement features prompt logic to handle `--yes` and fix `--features` - [#30534](https://github.com/storybookjs/storybook/pull/30534), thanks @ghengeveld!\n- Telemetry: Don't count example stories towards CSF feature stats - [#30561](https://github.com/storybookjs/storybook/pull/30561), thanks @shilman!"}}