diff --git a/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts b/code/lib/create-storybook/src/commands/ProjectDetectionCommand.test.ts index 424921416226..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; @@ -36,6 +37,7 @@ describe('ProjectDetectionCommand', () => { autoDetectProjectType: vi.fn(), isStorybookInstantiated: vi.fn().mockReturnValue(false), detectLanguage: vi.fn().mockResolvedValue(SupportedLanguage.JAVASCRIPT), + detectIncompatiblePackageVersions: vi.fn().mockResolvedValue([]), }; vi.mocked(ProjectTypeService).mockImplementation(function () { @@ -236,5 +238,29 @@ describe('ProjectDetectionCommand', () => { 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( + SupportedLanguage.JAVASCRIPT + ); + vi.mocked(mockProjectTypeService.detectIncompatiblePackageVersions).mockResolvedValue([ + '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..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'; @@ -48,11 +48,27 @@ 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 = await this.projectTypeService.detectLanguage(); + + 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; + } + /** 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 be14e17dfe20..cef92e839112 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.test.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.test.ts @@ -243,7 +243,7 @@ describe('ProjectTypeService', () => { await expect(service.detectLanguage()).resolves.toBe('typescript'); }); - it('warns and returns javascript when TS/tooling versions 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 = { @@ -255,10 +255,107 @@ 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).toHaveBeenCalled(); + }); + + 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 = { + 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); + await expect(service.detectLanguage()).resolves.toBe('javascript'); + }); + + 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 = { + 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'); + }); + }); + + 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 ea95f94bcdcf..7ab6e303c473 100644 --- a/code/lib/create-storybook/src/services/ProjectTypeService.ts +++ b/code/lib/create-storybook/src/services/ProjectTypeService.ts @@ -213,6 +213,25 @@ export class ProjectTypeService { 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; }; @@ -238,31 +257,39 @@ export class ProjectTypeService { return semver.satisfies(version, range, { includePrerelease: true }); }; - if (isTypescriptDirectDependency && typescriptVersion) { - 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')) - ) { - language = SupportedLanguage.TYPESCRIPT; - } else { - logger.warn( - 'Detected TypeScript < 4.9 or incompatible tooling, populating with JavaScript examples' - ); - } - } 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; - } + const incompatibleReasons: string[] = []; + + 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; + return incompatibleReasons; } private eqMajor(versionRange: string, major: number) {