Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,59 @@ describe('ProjectDetectionCommand', () => {
expect(mockProjectTypeService.autoDetectProjectType).not.toHaveBeenCalled();
});

describe('undetected project type (#35045)', () => {
it('should install HTML framework when user selects it', async () => {
vi.mocked(mockProjectTypeService.autoDetectProjectType).mockResolvedValue(
ProjectType.UNDETECTED
);
vi.mocked(prompt.select).mockResolvedValueOnce('html');

const command = new ProjectDetectionCommand({} as CommandOptions, mockPackageManager);
const result = await command.execute();

expect(result.projectType).toBe(ProjectType.HTML);
});

it('should let the user choose another framework', async () => {
vi.mocked(mockProjectTypeService.autoDetectProjectType).mockResolvedValue(
ProjectType.UNDETECTED
);
vi.mocked(prompt.select)
.mockResolvedValueOnce('select')
.mockResolvedValueOnce(ProjectType.VUE3);

const command = new ProjectDetectionCommand({} as CommandOptions, mockPackageManager);
const result = await command.execute();

expect(result.projectType).toBe(ProjectType.VUE3);
});

it('should fail when the user cancels the installation', async () => {
vi.mocked(mockProjectTypeService.autoDetectProjectType).mockResolvedValue(
ProjectType.UNDETECTED
);
vi.mocked(prompt.select).mockResolvedValueOnce('cancel');

const command = new ProjectDetectionCommand({} as CommandOptions, mockPackageManager);

await expect(command.execute()).rejects.toThrow();
});

it('should fail without prompting in non-interactive mode', async () => {
vi.mocked(mockProjectTypeService.autoDetectProjectType).mockResolvedValue(
ProjectType.UNDETECTED
);

const command = new ProjectDetectionCommand(
{ yes: true } as CommandOptions,
mockPackageManager
);

await expect(command.execute()).rejects.toThrow();
expect(prompt.select).not.toHaveBeenCalled();
});
});

it('should auto-detect project type when not provided', async () => {
options.type = undefined;
vi.mocked(mockProjectTypeService.autoDetectProjectType).mockResolvedValue(ProjectType.VUE3);
Expand Down
58 changes: 58 additions & 0 deletions code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ProjectType } from 'storybook/internal/cli';
import { HandledError } from 'storybook/internal/common';
import type { JsPackageManager } from 'storybook/internal/common';
import { logger, prompt } from 'storybook/internal/node-logger';
import { telemetry } from 'storybook/internal/telemetry';
Expand Down Expand Up @@ -42,6 +43,9 @@ export class ProjectDetectionCommand {
} else {
const detected = await this.projectTypeService.autoDetectProjectType(this.options);
projectType = detected;
if (detected === ProjectType.UNDETECTED) {
projectType = await this.promptUndetectedProjectType();
}
if (detected === ProjectType.REACT_NATIVE && !this.options.yes) {
projectType = await this.promptReactNativeVariant();
}
Comment on lines +46 to 51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and surrounding symbols.
ast-grep outline code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts --view expanded

# Show the relevant section with line numbers.
sed -n '1,220p' code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts

# Find the project-type prompt logic and enum values used by the command.
rg -n "promptUndetectedProjectType|promptReactNativeVariant|ProjectType.REACT_NATIVE|REACT_NATIVE_WEB|REACT_NATIVE_AND_RNW|Choose a framework to install|undetected|unsupported|nx" code/lib/create-storybook/src -S

Repository: storybookjs/storybook

Length of output: 15410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '160,260p' code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts
sed -n '1,260p' code/lib/create-storybook/src/services/ProjectTypeService.ts

Repository: storybookjs/storybook

Length of output: 12891


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,160p' code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts

Repository: storybookjs/storybook

Length of output: 6164


Use projectType for the React Native variant check
The undetected/manual select flow can return ProjectType.REACT_NATIVE, but this branch still checks the stale detected value, so the variant prompt is skipped there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts` around
lines 46 - 51, The React Native variant prompt in ProjectDetectionCommand is
checking the original detected value instead of the updated projectType selected
by the user. Update the conditional in the project detection flow so the React
Native variant check uses projectType after promptUndetectedProjectType() may
have changed it, while keeping the existing yes-option guard. This fix should be
applied in ProjectDetectionCommand where promptUndetectedProjectType() and
promptReactNativeVariant() are used.

Expand Down Expand Up @@ -72,6 +76,60 @@ export class ProjectDetectionCommand {
return language;
}

/**
* Handle the case where no supported framework could be detected: educate about the `--type`
* flag and, when interactive, offer to install the HTML framework, pick another framework, or
* cancel. Non-interactive runs keep the previous fail-with-error behavior.
*/
private async promptUndetectedProjectType(): Promise<ProjectType> {
logger.error(dedent`
Unable to detect a supported framework in this directory.

Storybook couldn't detect a supported framework or configuration for your project. Make sure you're inside a framework project (e.g., React, Vue, Svelte, Angular, Next.js) and that its dependencies are installed.

You can tell Storybook which framework to use with the ${picocolors.bold('--type')} flag, e.g. ${picocolors.bold('--type html')} for projects without a framework.
`);

if (this.options.yes) {
throw new HandledError('Storybook failed to detect your project type');
}

const choice = await prompt.select(
{
message: 'How would you like to proceed?',
options: [
{
label: `Install the ${picocolors.bold('HTML')} framework (for projects without a framework)`,
value: 'html',
},
{ label: 'Choose a framework to install', value: 'select' },
{ label: 'Cancel the installation', value: 'cancel' },
],
},
createPromptCancelOptions(this.telemetryService, 'undetected-project-type')
);

if (choice === 'html') {
return ProjectType.HTML;
}

if (choice === 'select') {
const installable = Object.values(ProjectType).filter(
(t) => !['undetected', 'unsupported', 'nx'].includes(String(t))
);
const manualType = await prompt.select(
{
message: 'Which framework would you like to install?',
options: installable.map((t) => ({ label: String(t), value: t })),
},
createPromptCancelOptions(this.telemetryService, 'undetected-project-type-framework')
);
return manualType as ProjectType;
}

throw new HandledError('Storybook failed to detect your project type');
}

/** Prompt user to select React Native variant */
private async promptReactNativeVariant(): Promise<ProjectType> {
const manualType = await prompt.select(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,15 @@ describe('ProjectTypeService', () => {
});

describe('autoDetectProjectType', () => {
it('logs a helpful message when framework cannot be detected', async () => {
it('returns UNDETECTED when framework cannot be detected', async () => {
const service = new ProjectTypeService(pm);
const options = { html: false } as unknown as CommandOptions;
// @ts-expect-error accessing private for test
vi.spyOn(service, 'detectProjectType').mockResolvedValue(ProjectType.UNDETECTED);

await expect(service.autoDetectProjectType(options)).rejects.toThrowError(
'Storybook failed to detect your project type'
);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining('Unable to initialize Storybook in this directory.')
);
// messaging and prompting for undetected projects is handled by the command layer
await expect(service.autoDetectProjectType(options)).resolves.toBe(ProjectType.UNDETECTED);
expect(logger.error).not.toHaveBeenCalled();
});

it('throws NxProjectDetectedError when NX project is detected', async () => {
Expand Down Expand Up @@ -277,7 +274,7 @@ describe('ProjectTypeService', () => {

await expect(
service.autoDetectProjectType({ html: false } as CommandOptions)
).rejects.toThrowError('Storybook failed to detect your project type');
).resolves.toBe(ProjectType.UNDETECTED);
});
});

Expand Down
14 changes: 2 additions & 12 deletions code/lib/create-storybook/src/services/ProjectTypeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,19 +181,9 @@ export class ProjectTypeService {
try {
const detectedType = await this.detectProjectType(options);

// prompting handled by command layer

// messaging and prompting for undetected projects is handled by the command layer
if (detectedType === ProjectType.UNDETECTED || detectedType === null) {
logger.error(dedent`
Unable to initialize Storybook in this directory.

Storybook couldn't detect a supported framework or configuration for your project. Make sure you're inside a framework project (e.g., React, Vue, Svelte, Angular, Next.js) and that its dependencies are installed.

Tips:
- Run init in an empty directory or create a new framework app first.
- If this directory contains unrelated files, try a new directory for Storybook.
`);
throw new HandledError('Storybook failed to detect your project type');
return ProjectType.UNDETECTED;
}

if (detectedType === ProjectType.NX) {
Expand Down
Loading