From 978b7cfe769d475483afc207df6caf9825dd5d07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:13:51 +0000 Subject: [PATCH 1/6] Initial plan From 59be610739b2c02f0c63065be3552812772ed271 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:19:39 +0000 Subject: [PATCH 2/6] fix: improve detectLanguage() to show specific failing checks and treat canary eslint-plugin-storybook as compatible - Replace vague "TypeScript < 4.9 or incompatible tooling" warning with detailed per-check failure messages listing which packages are below required versions - Treat Storybook canary/prerelease eslint-plugin-storybook versions (0.0.0-*) as compatible since they are development builds, not outdated stable releases - Add tests for single-tool failure messages and canary version handling Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/03429f2f-58e1-4038-abe3-5bf054c6e37b Co-authored-by: yannbf <1671563+yannbf@users.noreply.github.com> --- .../src/services/ProjectTypeService.test.ts | 44 ++++++++++++++++++- .../src/services/ProjectTypeService.ts | 39 +++++++++++++--- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/code/lib/create-storybook/src/services/ProjectTypeService.test.ts b/code/lib/create-storybook/src/services/ProjectTypeService.test.ts index b9d3e1aa0773..b5c8f8c04873 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.test.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.test.ts @@ -247,7 +247,49 @@ describe('ProjectTypeService', () => { const warnSpy = vi.spyOn(logger, 'warn'); const service = new ProjectTypeService(pm); await expect(service.detectLanguage()).resolves.toBe('javascript'); - expect(warnSpy).toHaveBeenCalled(); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('typescript 4.8.4 is below 4.9.0') + ); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('prettier 2.7.1 is below 2.8.0') + ); + }); + + it('warns with specific failing check when only one tool is incompatible', async () => { + (pm.getAllDependencies as any) = vi.fn(() => ({ typescript: '^5.0.0' })); + (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { + const versions: Record = { + typescript: '5.2.0', + prettier: '2.6.2', // only prettier is below 2.8.0 + '@babel/plugin-transform-typescript': '7.23.0', + '@typescript-eslint/parser': '6.7.0', + 'eslint-plugin-storybook': '0.7.0', + }; + return { version: versions[name] } as any; + }); + const warnSpy = vi.spyOn(logger, 'warn'); + const service = new ProjectTypeService(pm); + await expect(service.detectLanguage()).resolves.toBe('javascript'); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('prettier 2.6.2 is below 2.8.0') + ); + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('typescript')); + }); + + it('treats canary eslint-plugin-storybook versions as compatible', async () => { + (pm.getAllDependencies as any) = vi.fn(() => ({ typescript: '^5.0.0' })); + (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { + const versions: Record = { + typescript: '5.2.0', + prettier: '3.3.0', + '@babel/plugin-transform-typescript': '7.23.0', + '@typescript-eslint/parser': '6.7.0', + 'eslint-plugin-storybook': '0.0.0-pr-34552-sha-a34e9165', + }; + return { version: versions[name] } as any; + }); + const service = new ProjectTypeService(pm); + await expect(service.detectLanguage()).resolves.toBe('typescript'); }); }); }); diff --git a/code/lib/create-storybook/src/services/ProjectTypeService.ts b/code/lib/create-storybook/src/services/ProjectTypeService.ts index 39e2699bcb6d..88cbf037286b 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.ts @@ -232,18 +232,43 @@ export class ProjectTypeService { }; if (isTypescriptDirectDependency && typescriptVersion) { + const incompatibleReasons: string[] = []; + + if (!satisfies(typescriptVersion, '>=4.9.0')) { + incompatibleReasons.push(`typescript ${typescriptVersion} is below 4.9.0`); + } + if (prettierVersion && !semver.gte(prettierVersion, '2.8.0')) { + incompatibleReasons.push(`prettier ${prettierVersion} is below 2.8.0`); + } + if ( + babelPluginTransformTypescriptVersion && + !satisfies(babelPluginTransformTypescriptVersion, '>=7.20.0') + ) { + incompatibleReasons.push( + `@babel/plugin-transform-typescript ${babelPluginTransformTypescriptVersion} is below 7.20.0` + ); + } + if (typescriptEslintParserVersion && !satisfies(typescriptEslintParserVersion, '>=5.44.0')) { + incompatibleReasons.push( + `@typescript-eslint/parser ${typescriptEslintParserVersion} is below 5.44.0` + ); + } + // Treat Storybook canary/prerelease versions (e.g. 0.0.0-pr-*) as compatible if ( - satisfies(typescriptVersion, '>=4.9.0') && - (!prettierVersion || semver.gte(prettierVersion, '2.8.0')) && - (!babelPluginTransformTypescriptVersion || - satisfies(babelPluginTransformTypescriptVersion, '>=7.20.0')) && - (!typescriptEslintParserVersion || satisfies(typescriptEslintParserVersion, '>=5.44.0')) && - (!eslintPluginStorybookVersion || satisfies(eslintPluginStorybookVersion, '>=0.6.8')) + eslintPluginStorybookVersion && + !eslintPluginStorybookVersion.startsWith('0.0.0-') && + !satisfies(eslintPluginStorybookVersion, '>=0.6.8') ) { + incompatibleReasons.push( + `eslint-plugin-storybook ${eslintPluginStorybookVersion} is below 0.6.8` + ); + } + + if (incompatibleReasons.length === 0) { language = SupportedLanguage.TYPESCRIPT; } else { logger.warn( - 'Detected TypeScript < 4.9 or incompatible tooling, populating with JavaScript examples' + `Populating with JavaScript examples due to incompatible package versions:\n${incompatibleReasons.map((r) => ` - ${r}`).join('\n')}` ); } } else { From bc15365630a79db037adc17e9f7b7d21452cb5c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:39:57 +0000 Subject: [PATCH 3/6] fix: update detectLanguage() warning to not mention examples since the function only detects language Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/80bff2fc-8f6f-41a4-b594-af7818bc61da Co-authored-by: yannbf <1671563+yannbf@users.noreply.github.com> --- code/core/src/manager/globals/exports.ts | 1 - code/lib/create-storybook/src/services/ProjectTypeService.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts index 67edbe97d873..5bb91836ab8e 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -670,7 +670,6 @@ export default { 'CoreWebpackCompiler', 'Feature', 'SupportedBuilder', - 'SupportedFramework', 'SupportedLanguage', 'SupportedRenderer', ], diff --git a/code/lib/create-storybook/src/services/ProjectTypeService.ts b/code/lib/create-storybook/src/services/ProjectTypeService.ts index 88cbf037286b..3791e49253b1 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.ts @@ -268,7 +268,7 @@ export class ProjectTypeService { language = SupportedLanguage.TYPESCRIPT; } else { logger.warn( - `Populating with JavaScript examples due to incompatible package versions:\n${incompatibleReasons.map((r) => ` - ${r}`).join('\n')}` + `Detected incompatible package versions, falling back to JavaScript:\n${incompatibleReasons.map((r) => ` - ${r}`).join('\n')}` ); } } else { From 53524712a76245ea8c39b6b44d62ab57c1ecaf1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:37:17 +0000 Subject: [PATCH 4/6] plan: move warning from detectLanguage() to caller Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/427e0f3f-5743-49c5-9033-b8dab6a237ef Co-authored-by: yannbf <1671563+yannbf@users.noreply.github.com> --- code/core/src/manager/globals/exports.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts index 5bb91836ab8e..67edbe97d873 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -670,6 +670,7 @@ export default { 'CoreWebpackCompiler', 'Feature', 'SupportedBuilder', + 'SupportedFramework', 'SupportedLanguage', 'SupportedRenderer', ], From f7fcfb37101ad6a71b2308c415f8b0a2ebda6163 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:41:00 +0000 Subject: [PATCH 5/6] refactor: move warning from detectLanguage() to caller for proper separation of concerns Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/427e0f3f-5743-49c5-9033-b8dab6a237ef Co-authored-by: yannbf <1671563+yannbf@users.noreply.github.com> --- .../commands/ProjectDetectionCommand.test.ts | 33 ++++++++++++++++--- .../src/commands/ProjectDetectionCommand.ts | 15 ++++++++- .../src/services/ProjectTypeService.test.ts | 31 +++++++++-------- .../src/services/ProjectTypeService.ts | 15 +++++---- 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts b/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts index 424921416226..956c058126ad 100644 --- a/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts +++ b/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts @@ -35,7 +35,9 @@ describe('ProjectDetectionCommand', () => { validateProvidedType: vi.fn(), autoDetectProjectType: vi.fn(), isStorybookInstantiated: vi.fn().mockReturnValue(false), - detectLanguage: vi.fn().mockResolvedValue(SupportedLanguage.JAVASCRIPT), + detectLanguage: vi + .fn() + .mockResolvedValue({ language: SupportedLanguage.JAVASCRIPT, incompatibleReasons: [] }), }; vi.mocked(ProjectTypeService).mockImplementation(function () { @@ -227,14 +229,37 @@ describe('ProjectDetectionCommand', () => { options.type = undefined; options.language = undefined; vi.mocked(mockProjectTypeService.autoDetectProjectType).mockResolvedValue(ProjectType.REACT); - vi.mocked(mockProjectTypeService.detectLanguage).mockResolvedValue( - SupportedLanguage.TYPESCRIPT - ); + vi.mocked(mockProjectTypeService.detectLanguage).mockResolvedValue({ + language: SupportedLanguage.TYPESCRIPT, + incompatibleReasons: [], + }); const result = await command.execute(); expect(result.language).toBe(SupportedLanguage.TYPESCRIPT); expect(mockProjectTypeService.detectLanguage).toHaveBeenCalled(); }); + + it('should warn about incompatible packages when falling back to JavaScript', async () => { + options.type = undefined; + options.language = undefined; + vi.mocked(mockProjectTypeService.autoDetectProjectType).mockResolvedValue(ProjectType.REACT); + vi.mocked(mockProjectTypeService.detectLanguage).mockResolvedValue({ + language: SupportedLanguage.JAVASCRIPT, + incompatibleReasons: ['prettier 2.6.2 is below 2.8.0'], + }); + + const result = await command.execute(); + + expect(result.language).toBe(SupportedLanguage.JAVASCRIPT); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining( + 'Populating with JavaScript examples due to incompatible package versions' + ) + ); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('prettier 2.6.2 is below 2.8.0') + ); + }); }); }); diff --git a/code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts b/code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts index c445383801cb..97b62aff9f52 100644 --- a/code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts +++ b/code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts @@ -48,11 +48,24 @@ export class ProjectDetectionCommand { // Check for existing installation await this.checkExistingInstallation(projectType); - const language = this.options.language || (await this.projectTypeService.detectLanguage()); + const language = this.options.language || (await this.detectAndReportLanguage()); return { projectType, language }; } + /** Detect language and warn about incompatible packages */ + private async detectAndReportLanguage(): Promise { + const { language, incompatibleReasons } = await this.projectTypeService.detectLanguage(); + + if (incompatibleReasons.length > 0) { + logger.warn( + `Populating with JavaScript examples due to incompatible package versions:\n${incompatibleReasons.map((r) => ` - ${r}`).join('\n')}` + ); + } + + return language; + } + /** Prompt user to select React Native variant */ private async promptReactNativeVariant(): Promise { const manualType = await prompt.select({ diff --git a/code/lib/create-storybook/src/services/ProjectTypeService.test.ts b/code/lib/create-storybook/src/services/ProjectTypeService.test.ts index b5c8f8c04873..f4b4cf151973 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.test.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.test.ts @@ -229,10 +229,12 @@ describe('ProjectTypeService', () => { return { version: versions[name] } as any; }); const service = new ProjectTypeService(pm); - await expect(service.detectLanguage()).resolves.toBe('typescript'); + const result = await service.detectLanguage(); + expect(result.language).toBe('typescript'); + expect(result.incompatibleReasons).toEqual([]); }); - it('warns and returns javascript when TS/tooling versions incompatible', async () => { + it('returns javascript with incompatible reasons when TS/tooling versions are incompatible', async () => { (pm.getAllDependencies as any) = vi.fn(() => ({ typescript: '^4.8.0' })); (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { const versions: Record = { @@ -244,18 +246,18 @@ describe('ProjectTypeService', () => { }; return { version: versions[name] } as any; }); - const warnSpy = vi.spyOn(logger, 'warn'); const service = new ProjectTypeService(pm); - await expect(service.detectLanguage()).resolves.toBe('javascript'); - expect(warnSpy).toHaveBeenCalledWith( + const result = await service.detectLanguage(); + expect(result.language).toBe('javascript'); + expect(result.incompatibleReasons).toContainEqual( expect.stringContaining('typescript 4.8.4 is below 4.9.0') ); - expect(warnSpy).toHaveBeenCalledWith( + expect(result.incompatibleReasons).toContainEqual( expect.stringContaining('prettier 2.7.1 is below 2.8.0') ); }); - it('warns with specific failing check when only one tool is incompatible', async () => { + it('returns specific failing reason when only one tool is incompatible', async () => { (pm.getAllDependencies as any) = vi.fn(() => ({ typescript: '^5.0.0' })); (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { const versions: Record = { @@ -267,13 +269,12 @@ describe('ProjectTypeService', () => { }; return { version: versions[name] } as any; }); - const warnSpy = vi.spyOn(logger, 'warn'); const service = new ProjectTypeService(pm); - await expect(service.detectLanguage()).resolves.toBe('javascript'); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('prettier 2.6.2 is below 2.8.0') - ); - expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('typescript')); + const result = await service.detectLanguage(); + expect(result.language).toBe('javascript'); + expect(result.incompatibleReasons).toEqual([ + expect.stringContaining('prettier 2.6.2 is below 2.8.0'), + ]); }); it('treats canary eslint-plugin-storybook versions as compatible', async () => { @@ -289,7 +290,9 @@ describe('ProjectTypeService', () => { return { version: versions[name] } as any; }); const service = new ProjectTypeService(pm); - await expect(service.detectLanguage()).resolves.toBe('typescript'); + const result = await service.detectLanguage(); + expect(result.language).toBe('typescript'); + expect(result.incompatibleReasons).toEqual([]); }); }); }); diff --git a/code/lib/create-storybook/src/services/ProjectTypeService.ts b/code/lib/create-storybook/src/services/ProjectTypeService.ts index 3791e49253b1..11d3e2d98004 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.ts @@ -197,11 +197,14 @@ export class ProjectTypeService { } } - async detectLanguage(): Promise { + async detectLanguage(): Promise<{ + language: SupportedLanguage; + incompatibleReasons: string[]; + }> { let language = SupportedLanguage.JAVASCRIPT; if (existsSync('jsconfig.json')) { - return language; + return { language, incompatibleReasons: [] }; } const isTypescriptDirectDependency = !!this.jsPackageManager.getAllDependencies().typescript; @@ -266,11 +269,9 @@ export class ProjectTypeService { if (incompatibleReasons.length === 0) { language = SupportedLanguage.TYPESCRIPT; - } else { - logger.warn( - `Detected incompatible package versions, falling back to JavaScript:\n${incompatibleReasons.map((r) => ` - ${r}`).join('\n')}` - ); } + + return { language, incompatibleReasons }; } else { // No direct dependency on TypeScript, but could be a transitive dependency // This is eg the case for Nuxt projects, which support a recent version of TypeScript @@ -280,7 +281,7 @@ export class ProjectTypeService { } } - return language; + return { language, incompatibleReasons: [] }; } private eqMajor(versionRange: string, major: number) { From 8de213003e43f564719457a6f709345167cc0a2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:57:07 +0000 Subject: [PATCH 6/6] refactor: separate detectLanguage() and detectIncompatiblePackageVersions() into two clean functions Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/234aa538-77b9-452c-834c-9419ca6a7730 Co-authored-by: yannbf <1671563+yannbf@users.noreply.github.com> --- .../commands/ProjectDetectionCommand.test.ts | 23 ++-- .../src/commands/ProjectDetectionCommand.ts | 15 +-- .../src/services/ProjectTypeService.test.ts | 96 +++++++++++++---- .../src/services/ProjectTypeService.ts | 101 +++++++++--------- 4 files changed, 146 insertions(+), 89 deletions(-) diff --git a/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts b/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts index 956c058126ad..23b2979ea754 100644 --- a/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts +++ b/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts @@ -23,6 +23,7 @@ describe('ProjectDetectionCommand', () => { autoDetectProjectType: ReturnType; isStorybookInstantiated: ReturnType; detectLanguage: ReturnType; + detectIncompatiblePackageVersions: ReturnType; }; let options: CommandOptions; @@ -35,9 +36,8 @@ describe('ProjectDetectionCommand', () => { validateProvidedType: vi.fn(), autoDetectProjectType: vi.fn(), isStorybookInstantiated: vi.fn().mockReturnValue(false), - detectLanguage: vi - .fn() - .mockResolvedValue({ language: SupportedLanguage.JAVASCRIPT, incompatibleReasons: [] }), + detectLanguage: vi.fn().mockResolvedValue(SupportedLanguage.JAVASCRIPT), + detectIncompatiblePackageVersions: vi.fn().mockResolvedValue([]), }; vi.mocked(ProjectTypeService).mockImplementation(function () { @@ -229,10 +229,9 @@ describe('ProjectDetectionCommand', () => { options.type = undefined; options.language = undefined; vi.mocked(mockProjectTypeService.autoDetectProjectType).mockResolvedValue(ProjectType.REACT); - vi.mocked(mockProjectTypeService.detectLanguage).mockResolvedValue({ - language: SupportedLanguage.TYPESCRIPT, - incompatibleReasons: [], - }); + vi.mocked(mockProjectTypeService.detectLanguage).mockResolvedValue( + SupportedLanguage.TYPESCRIPT + ); const result = await command.execute(); @@ -244,10 +243,12 @@ describe('ProjectDetectionCommand', () => { options.type = undefined; options.language = undefined; vi.mocked(mockProjectTypeService.autoDetectProjectType).mockResolvedValue(ProjectType.REACT); - vi.mocked(mockProjectTypeService.detectLanguage).mockResolvedValue({ - language: SupportedLanguage.JAVASCRIPT, - incompatibleReasons: ['prettier 2.6.2 is below 2.8.0'], - }); + vi.mocked(mockProjectTypeService.detectLanguage).mockResolvedValue( + SupportedLanguage.JAVASCRIPT + ); + vi.mocked(mockProjectTypeService.detectIncompatiblePackageVersions).mockResolvedValue([ + 'prettier 2.6.2 is below 2.8.0', + ]); const result = await command.execute(); diff --git a/code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts b/code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts index 97b62aff9f52..34cb08493690 100644 --- a/code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts +++ b/code/lib/create-storybook/src/commands/ProjectDetectionCommand.ts @@ -2,7 +2,7 @@ import { ProjectType } from 'storybook/internal/cli'; import type { JsPackageManager } from 'storybook/internal/common'; import { logger, prompt } from 'storybook/internal/node-logger'; import { telemetry } from 'storybook/internal/telemetry'; -import type { SupportedLanguage } from 'storybook/internal/types'; +import { SupportedLanguage } from 'storybook/internal/types'; import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; @@ -55,12 +55,15 @@ export class ProjectDetectionCommand { /** Detect language and warn about incompatible packages */ private async detectAndReportLanguage(): Promise { - const { language, incompatibleReasons } = await this.projectTypeService.detectLanguage(); + const language = await this.projectTypeService.detectLanguage(); - if (incompatibleReasons.length > 0) { - logger.warn( - `Populating with JavaScript examples due to incompatible package versions:\n${incompatibleReasons.map((r) => ` - ${r}`).join('\n')}` - ); + if (language === SupportedLanguage.JAVASCRIPT) { + const incompatibleReasons = await this.projectTypeService.detectIncompatiblePackageVersions(); + if (incompatibleReasons.length > 0) { + logger.warn( + `Populating with JavaScript examples due to incompatible package versions:\n${incompatibleReasons.map((r) => ` - ${r}`).join('\n')}` + ); + } } return language; diff --git a/code/lib/create-storybook/src/services/ProjectTypeService.test.ts b/code/lib/create-storybook/src/services/ProjectTypeService.test.ts index f4b4cf151973..f06a8313f2c8 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.test.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.test.ts @@ -229,12 +229,10 @@ describe('ProjectTypeService', () => { return { version: versions[name] } as any; }); const service = new ProjectTypeService(pm); - const result = await service.detectLanguage(); - expect(result.language).toBe('typescript'); - expect(result.incompatibleReasons).toEqual([]); + await expect(service.detectLanguage()).resolves.toBe('typescript'); }); - it('returns javascript with incompatible reasons when TS/tooling versions are incompatible', async () => { + it('returns javascript when TS/tooling versions are incompatible', async () => { (pm.getAllDependencies as any) = vi.fn(() => ({ typescript: '^4.8.0' })); (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { const versions: Record = { @@ -247,17 +245,10 @@ describe('ProjectTypeService', () => { return { version: versions[name] } as any; }); const service = new ProjectTypeService(pm); - const result = await service.detectLanguage(); - expect(result.language).toBe('javascript'); - expect(result.incompatibleReasons).toContainEqual( - expect.stringContaining('typescript 4.8.4 is below 4.9.0') - ); - expect(result.incompatibleReasons).toContainEqual( - expect.stringContaining('prettier 2.7.1 is below 2.8.0') - ); + await expect(service.detectLanguage()).resolves.toBe('javascript'); }); - it('returns specific failing reason when only one tool is incompatible', async () => { + it('returns javascript when only one tool is incompatible', async () => { (pm.getAllDependencies as any) = vi.fn(() => ({ typescript: '^5.0.0' })); (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { const versions: Record = { @@ -270,14 +261,10 @@ describe('ProjectTypeService', () => { return { version: versions[name] } as any; }); const service = new ProjectTypeService(pm); - const result = await service.detectLanguage(); - expect(result.language).toBe('javascript'); - expect(result.incompatibleReasons).toEqual([ - expect.stringContaining('prettier 2.6.2 is below 2.8.0'), - ]); + await expect(service.detectLanguage()).resolves.toBe('javascript'); }); - it('treats canary eslint-plugin-storybook versions as compatible', async () => { + it('returns typescript with canary eslint-plugin-storybook versions', async () => { (pm.getAllDependencies as any) = vi.fn(() => ({ typescript: '^5.0.0' })); (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { const versions: Record = { @@ -290,9 +277,74 @@ describe('ProjectTypeService', () => { return { version: versions[name] } as any; }); const service = new ProjectTypeService(pm); - const result = await service.detectLanguage(); - expect(result.language).toBe('typescript'); - expect(result.incompatibleReasons).toEqual([]); + await expect(service.detectLanguage()).resolves.toBe('typescript'); + }); + }); + + describe('detectIncompatiblePackageVersions', () => { + it('returns empty array when all tooling is compatible', async () => { + (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { + const versions: Record = { + typescript: '5.2.0', + prettier: '3.3.0', + '@babel/plugin-transform-typescript': '7.23.0', + '@typescript-eslint/parser': '6.7.0', + 'eslint-plugin-storybook': '0.7.0', + }; + return { version: versions[name] } as any; + }); + const service = new ProjectTypeService(pm); + const reasons = await service.detectIncompatiblePackageVersions(); + expect(reasons).toEqual([]); + }); + + it('returns specific reasons for each incompatible package', async () => { + (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { + const versions: Record = { + typescript: '4.8.4', + prettier: '2.7.1', + '@babel/plugin-transform-typescript': '7.19.0', + '@typescript-eslint/parser': '5.43.0', + 'eslint-plugin-storybook': '0.6.7', + }; + return { version: versions[name] } as any; + }); + const service = new ProjectTypeService(pm); + const reasons = await service.detectIncompatiblePackageVersions(); + expect(reasons).toContainEqual(expect.stringContaining('typescript 4.8.4 is below 4.9.0')); + expect(reasons).toContainEqual(expect.stringContaining('prettier 2.7.1 is below 2.8.0')); + }); + + it('returns only the specific failing package', async () => { + (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { + const versions: Record = { + typescript: '5.2.0', + prettier: '2.6.2', // only prettier is below 2.8.0 + '@babel/plugin-transform-typescript': '7.23.0', + '@typescript-eslint/parser': '6.7.0', + 'eslint-plugin-storybook': '0.7.0', + }; + return { version: versions[name] } as any; + }); + const service = new ProjectTypeService(pm); + const reasons = await service.detectIncompatiblePackageVersions(); + expect(reasons).toEqual([expect.stringContaining('prettier 2.6.2 is below 2.8.0')]); + }); + + it('treats canary eslint-plugin-storybook versions as compatible', async () => { + (pm.getModulePackageJSON as any) = vi.fn(async (name: string) => { + const versions: Record = { + typescript: '5.2.0', + prettier: '3.3.0', + '@babel/plugin-transform-typescript': '7.23.0', + '@typescript-eslint/parser': '6.7.0', + 'eslint-plugin-storybook': '0.0.0-pr-34552-sha-a34e9165', + }; + return { version: versions[name] } as any; + }); + const service = new ProjectTypeService(pm); + const reasons = await service.detectIncompatiblePackageVersions(); + expect(reasons).toEqual([]); }); }); }); diff --git a/code/lib/create-storybook/src/services/ProjectTypeService.ts b/code/lib/create-storybook/src/services/ProjectTypeService.ts index 11d3e2d98004..b169a34737ea 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.ts @@ -197,18 +197,34 @@ export class ProjectTypeService { } } - async detectLanguage(): Promise<{ - language: SupportedLanguage; - incompatibleReasons: string[]; - }> { + async detectLanguage(): Promise { let language = SupportedLanguage.JAVASCRIPT; if (existsSync('jsconfig.json')) { - return { language, incompatibleReasons: [] }; + return language; } const isTypescriptDirectDependency = !!this.jsPackageManager.getAllDependencies().typescript; + if (isTypescriptDirectDependency) { + const incompatibleReasons = await this.detectIncompatiblePackageVersions(); + if (incompatibleReasons.length === 0) { + language = SupportedLanguage.TYPESCRIPT; + } + } else { + // No direct dependency on TypeScript, but could be a transitive dependency + // This is eg the case for Nuxt projects, which support a recent version of TypeScript + // Check for tsconfig.json (https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) + if (existsSync('tsconfig.json')) { + language = SupportedLanguage.TYPESCRIPT; + } + } + + return language; + } + + /** Check installed tooling versions for TypeScript compatibility constraints */ + async detectIncompatiblePackageVersions(): Promise { const getModulePackageJSONVersion = async (pkg: string) => { return (await this.jsPackageManager.getModulePackageJSON(pkg))?.version ?? null; }; @@ -234,54 +250,39 @@ export class ProjectTypeService { return semver.satisfies(version, range, { includePrerelease: true }); }; - if (isTypescriptDirectDependency && typescriptVersion) { - const incompatibleReasons: string[] = []; - - if (!satisfies(typescriptVersion, '>=4.9.0')) { - incompatibleReasons.push(`typescript ${typescriptVersion} is below 4.9.0`); - } - if (prettierVersion && !semver.gte(prettierVersion, '2.8.0')) { - incompatibleReasons.push(`prettier ${prettierVersion} is below 2.8.0`); - } - if ( - babelPluginTransformTypescriptVersion && - !satisfies(babelPluginTransformTypescriptVersion, '>=7.20.0') - ) { - incompatibleReasons.push( - `@babel/plugin-transform-typescript ${babelPluginTransformTypescriptVersion} is below 7.20.0` - ); - } - if (typescriptEslintParserVersion && !satisfies(typescriptEslintParserVersion, '>=5.44.0')) { - incompatibleReasons.push( - `@typescript-eslint/parser ${typescriptEslintParserVersion} is below 5.44.0` - ); - } - // Treat Storybook canary/prerelease versions (e.g. 0.0.0-pr-*) as compatible - if ( - eslintPluginStorybookVersion && - !eslintPluginStorybookVersion.startsWith('0.0.0-') && - !satisfies(eslintPluginStorybookVersion, '>=0.6.8') - ) { - incompatibleReasons.push( - `eslint-plugin-storybook ${eslintPluginStorybookVersion} is below 0.6.8` - ); - } - - if (incompatibleReasons.length === 0) { - language = SupportedLanguage.TYPESCRIPT; - } + const incompatibleReasons: string[] = []; - return { language, incompatibleReasons }; - } else { - // No direct dependency on TypeScript, but could be a transitive dependency - // This is eg the case for Nuxt projects, which support a recent version of TypeScript - // Check for tsconfig.json (https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) - if (existsSync('tsconfig.json')) { - language = SupportedLanguage.TYPESCRIPT; - } + if (typescriptVersion && !satisfies(typescriptVersion, '>=4.9.0')) { + incompatibleReasons.push(`typescript ${typescriptVersion} is below 4.9.0`); + } + if (prettierVersion && !semver.gte(prettierVersion, '2.8.0')) { + incompatibleReasons.push(`prettier ${prettierVersion} is below 2.8.0`); + } + if ( + babelPluginTransformTypescriptVersion && + !satisfies(babelPluginTransformTypescriptVersion, '>=7.20.0') + ) { + incompatibleReasons.push( + `@babel/plugin-transform-typescript ${babelPluginTransformTypescriptVersion} is below 7.20.0` + ); + } + if (typescriptEslintParserVersion && !satisfies(typescriptEslintParserVersion, '>=5.44.0')) { + incompatibleReasons.push( + `@typescript-eslint/parser ${typescriptEslintParserVersion} is below 5.44.0` + ); + } + // Treat Storybook canary/prerelease versions (e.g. 0.0.0-pr-*) as compatible + if ( + eslintPluginStorybookVersion && + !eslintPluginStorybookVersion.startsWith('0.0.0-') && + !satisfies(eslintPluginStorybookVersion, '>=0.6.8') + ) { + incompatibleReasons.push( + `eslint-plugin-storybook ${eslintPluginStorybookVersion} is below 0.6.8` + ); } - return { language, incompatibleReasons: [] }; + return incompatibleReasons; } private eqMajor(versionRange: string, major: number) {