diff --git a/code/addons/vitest/src/node/vitest-manager.ts b/code/addons/vitest/src/node/vitest-manager.ts index de3bac4eeab0..b2d1d9493436 100644 --- a/code/addons/vitest/src/node/vitest-manager.ts +++ b/code/addons/vitest/src/node/vitest-manager.ts @@ -9,7 +9,7 @@ import type { WorkspaceProject, } from 'vitest/node'; -import { resolvePathInStorybookCache } from 'storybook/internal/common'; +import { getProjectRoot, resolvePathInStorybookCache } from 'storybook/internal/common'; import type { StoryId, StoryIndex, StoryIndexEntry } from 'storybook/internal/types'; import { findUp } from 'find-up'; @@ -65,10 +65,13 @@ export class VitestManager { : { enabled: false } ) as CoverageOptions; - const vitestWorkspaceConfig = await findUp([ - ...VITEST_WORKSPACE_FILE_EXTENSION.map((ext) => `vitest.workspace.${ext}`), - ...VITEST_CONFIG_FILE_EXTENSIONS.map((ext) => `vitest.config.${ext}`), - ]); + const vitestWorkspaceConfig = await findUp( + [ + ...VITEST_WORKSPACE_FILE_EXTENSION.map((ext) => `vitest.workspace.${ext}`), + ...VITEST_CONFIG_FILE_EXTENSIONS.map((ext) => `vitest.config.${ext}`), + ], + { stopAt: getProjectRoot() } + ); const projectName = 'storybook:' + process.env.STORYBOOK_CONFIG_DIR; diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index 3f2865d97bb4..3c3a3e8e6d11 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -7,6 +7,7 @@ import { JsPackageManagerFactory, extractProperFrameworkName, formatFileContent, + getProjectRoot, loadAllPresets, loadMainConfig, scanAndTransformFiles, @@ -39,7 +40,10 @@ const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.cts', '.mts', '.cjs', '.mjs' const addonA11yName = '@storybook/addon-a11y'; const findFile = async (basename: string, extensions = EXTENSIONS) => - findUp(extensions.map((ext) => basename + ext)); + findUp( + extensions.map((ext) => basename + ext), + { stopAt: getProjectRoot() } + ); export default async function postInstall(options: PostinstallOptions) { printSuccess( @@ -56,13 +60,11 @@ export default async function postInstall(options: PostinstallOptions) { }); const info = await getStorybookInfo(options); - const allDeps = await packageManager.getAllDependencies(); + const allDeps = packageManager.getAllDependencies(); // only install these dependencies if they are not already installed const dependencies = ['vitest', '@vitest/browser', 'playwright'].filter((p) => !allDeps[p]); const vitestVersionSpecifier = await packageManager.getInstalledVersion('vitest'); const coercedVitestVersion = vitestVersionSpecifier ? coerce(vitestVersionSpecifier) : null; - // if Vitest is installed, we use the same version to keep consistency across Vitest packages - const vitestVersionToInstall = vitestVersionSpecifier ?? 'latest'; const mainJsPath = serverResolve(resolve(options.configDir, 'main')) as string; const config = await readConfig(mainJsPath); @@ -88,11 +90,11 @@ export default async function postInstall(options: PostinstallOptions) { }); if (out.migrateToNextjsVite) { - await packageManager.addDependencies({ installAsDevDependencies: true }, [ + await packageManager.addDependencies({ installAsDevDependencies: true, skipInstall: true }, [ `@storybook/nextjs-vite@${versions['@storybook/nextjs-vite']}`, ]); - await packageManager.removeDependencies({}, ['@storybook/nextjs']); + await packageManager.removeDependencies(['@storybook/nextjs']); traverse(config._ast, { StringLiteral(path) { @@ -255,20 +257,24 @@ export default async function postInstall(options: PostinstallOptions) { const versionedDependencies = dependencies.map((p) => { if (p.includes('vitest')) { - return `${p}@${vitestVersionToInstall ?? 'latest'}`; + return vitestVersionSpecifier ? `${p}@${vitestVersionSpecifier}` : p; } return p; }); if (versionedDependencies.length > 0) { + await packageManager.addDependencies( + { installAsDevDependencies: true, skipInstall: true }, + versionedDependencies + ); logger.line(1); logger.plain(`${step} Installing dependencies:`); logger.plain(colors.gray(' ' + versionedDependencies.join(', '))); - - await packageManager.addDependencies({ installAsDevDependencies: true }, versionedDependencies); } + await packageManager.installDependencies(); + logger.line(1); logger.plain(`${step} Configuring Playwright with Chromium (this might take some time):`); logger.plain(colors.gray(' npx playwright install chromium --with-deps')); @@ -521,8 +527,8 @@ export default async function postInstall(options: PostinstallOptions) { } async function getStorybookInfo({ configDir, packageManager: pkgMgr }: PostinstallOptions) { - const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); - const packageJson = await packageManager.retrievePackageJson(); + const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr, configDir }); + const { packageJson } = packageManager.primaryPackageJson; const config = await loadMainConfig({ configDir, noCache: true }); const { framework } = config; @@ -536,8 +542,8 @@ async function getStorybookInfo({ configDir, packageManager: pkgMgr }: Postinsta overridePresets: [ require.resolve('storybook/internal/core-server/presets/common-override-preset'), ], - configDir, packageJson, + configDir, isCritical: true, }); diff --git a/code/core/src/cli/detect.test.ts b/code/core/src/cli/detect.test.ts index 260b57880389..d5f81f531a63 100644 --- a/code/core/src/cli/detect.test.ts +++ b/code/core/src/cli/detect.test.ts @@ -12,21 +12,16 @@ vi.mock('./helpers', () => ({ isNxProject: vi.fn(), })); -vi.mock('fs', () => ({ - existsSync: vi.fn(), - stat: vi.fn(), - lstat: vi.fn(), - access: vi.fn(), - realpathSync: vi.fn(), - lstatSync: vi.fn(), - readdir: vi.fn(), - readdirSync: vi.fn(), - readlinkSync: vi.fn(), - default: vi.fn(), - mkdirSync: vi.fn(), -})); +vi.mock(import('fs'), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + existsSync: vi.fn(), + }; +}); vi.mock('storybook/internal/node-logger'); +vi.mock('find-up'); const MOCK_FRAMEWORK_FILES: { name: string; @@ -236,9 +231,17 @@ const MOCK_FRAMEWORK_FILES: { describe('Detect', () => { it(`should return type HTML if html option is passed`, async () => { const packageManager = { - retrievePackageJson: () => Promise.resolve({ dependencies: {}, devDependencies: {} }), - getAllDependencies: () => Promise.resolve({}), - getPackageVersion: () => Promise.resolve(null), + primaryPackageJson: { + packageJson: { + dependencies: {}, + devDependencies: {}, + peerDependencies: {}, + }, + packageJsonPath: 'some/path', + operationDir: 'some/path', + }, + getAllDependencies: () => ({}), + getModulePackageJSON: () => null, } as Partial; await expect(detect(packageManager as any, { html: true })).resolves.toBe(ProjectType.HTML); @@ -248,23 +251,17 @@ describe('Detect', () => { vi.mocked(logger.warn).mockClear(); const packageManager = { - retrievePackageJson: () => - Promise.resolve({ - dependencies: {}, - devDependencies: { - typescript: '1.0.0', - }, - }), - getAllDependencies: () => - Promise.resolve({ - typescript: '1.0.0', - }), - getPackageVersion: (packageName) => { + getAllDependencies: () => ({ + typescript: '1.0.0', + }), + getModulePackageJSON: (packageName) => { switch (packageName) { case 'typescript': - return Promise.resolve('1.0.0'); + return { + version: '1.0.0', + }; default: - return Promise.resolve(null); + return null; } }, } as Partial; @@ -276,136 +273,107 @@ describe('Detect', () => { }); it(`should return language javascript if the TS dependency is <4.9`, async () => { - await expect( - detectLanguage({ - retrievePackageJson: () => - Promise.resolve({ - dependencies: {}, - devDependencies: { - typescript: '4.8.0', - }, - }), - getAllDependencies: () => - Promise.resolve({ - typescript: '4.8.0', - }), - getPackageVersion: (packageName: string) => { - switch (packageName) { - case 'typescript': - return Promise.resolve('4.8.0'); - default: - return Promise.resolve(null); - } - }, - } as Partial as JsPackageManager) - ).resolves.toBe(SupportedLanguage.JAVASCRIPT); + const packageManager = { + getAllDependencies: () => ({ + typescript: '4.8.0', + }), + getModulePackageJSON: (packageName: string) => { + switch (packageName) { + case 'typescript': + return { + version: '4.8.0', + }; + default: + return null; + } + }, + } as Partial; + await expect(detectLanguage(packageManager as any)).resolves.toBe(SupportedLanguage.JAVASCRIPT); }); it(`should return language typescript-4-9 if the dependency is >TS4.9`, async () => { - await expect( - detectLanguage({ - retrievePackageJson: () => - Promise.resolve({ - dependencies: {}, - devDependencies: { - typescript: '4.9.1', - }, - }), - getAllDependencies: () => - Promise.resolve({ - typescript: '4.9.1', - }), - getPackageVersion: (packageName: string) => { - switch (packageName) { - case 'typescript': - return Promise.resolve('4.9.1'); - default: - return Promise.resolve(null); - } - }, - } as Partial as JsPackageManager) - ).resolves.toBe(SupportedLanguage.TYPESCRIPT); + const packageManager = { + getAllDependencies: () => ({ + typescript: '4.9.1', + }), + getModulePackageJSON: (packageName: string) => { + switch (packageName) { + case 'typescript': + return { + version: '4.9.1', + }; + default: + return null; + } + }, + } as Partial; + await expect(detectLanguage(packageManager as any)).resolves.toBe(SupportedLanguage.TYPESCRIPT); }); it(`should return language typescript if the dependency is =TS4.9`, async () => { - await expect( - detectLanguage({ - retrievePackageJson: () => - Promise.resolve({ - dependencies: {}, - devDependencies: { - typescript: '4.9.0', - }, - }), - getAllDependencies: () => - Promise.resolve({ - typescript: '4.9.0', - }), - getPackageVersion: (packageName: string) => { - switch (packageName) { - case 'typescript': - return Promise.resolve('4.9.0'); - default: - return Promise.resolve(null); - } - }, - } as Partial as JsPackageManager) - ).resolves.toBe(SupportedLanguage.TYPESCRIPT); + const packageManager = { + getAllDependencies: () => ({ + typescript: '4.9.0', + }), + getModulePackageJSON: (packageName: string) => { + switch (packageName) { + case 'typescript': + return { + version: '4.9.0', + }; + default: + return null; + } + }, + } as Partial; + await expect(detectLanguage(packageManager as any)).resolves.toBe(SupportedLanguage.TYPESCRIPT); }); it(`should return language JavaScript if the dependency is =TS4.9beta`, async () => { - await expect( - detectLanguage({ - retrievePackageJson: () => - Promise.resolve({ - dependencies: {}, - devDependencies: { - typescript: '4.9.0-beta', - }, - }), - getAllDependencies: () => - Promise.resolve({ - typescript: '4.9.0-beta', - }), - getPackageVersion: (packageName: string) => { - switch (packageName) { - case 'typescript': - return Promise.resolve('4.9.0-beta'); - default: - return Promise.resolve(null); - } - }, - } as Partial as JsPackageManager) - ).resolves.toBe(SupportedLanguage.JAVASCRIPT); + const packageManager = { + getAllDependencies: () => ({ + typescript: '4.9.0-beta', + }), + getModulePackageJSON: (packageName: string) => { + switch (packageName) { + case 'typescript': + return { + version: '4.9.0-beta', + }; + default: + return null; + } + }, + } as Partial; + + await expect(detectLanguage(packageManager as any)).resolves.toBe(SupportedLanguage.JAVASCRIPT); }); it(`should return language javascript by default`, async () => { - await expect( - detectLanguage({ - retrievePackageJson: () => Promise.resolve({ dependencies: {}, devDependencies: {} }), - getAllDependencies: () => Promise.resolve({}), - getPackageVersion: () => { - return Promise.resolve(null); - }, - } as Partial as JsPackageManager) - ).resolves.toBe(SupportedLanguage.JAVASCRIPT); + const packageManager = { + getAllDependencies: () => ({}), + getModulePackageJSON: () => null, + } as Partial; + + await expect(detectLanguage(packageManager as any)).resolves.toBe(SupportedLanguage.JAVASCRIPT); }); it(`should return language Javascript even when Typescript is detected in the node_modules but not listed as a direct dependency`, async () => { - await expect( - detectLanguage({ - retrievePackageJson: () => Promise.resolve({ dependencies: {}, devDependencies: {} }), - getAllDependencies: () => Promise.resolve({}), - getPackageVersion: (packageName) => { - switch (packageName) { - case 'typescript': - return Promise.resolve('4.9.0'); - default: - return Promise.resolve(null); - } - }, - } as Partial as JsPackageManager) - ).resolves.toBe(SupportedLanguage.JAVASCRIPT); + const packageManager = { + getAllDependencies: () => ({}), + getModulePackageJSON: (packageName) => { + switch (packageName) { + case 'typescript': + return { + version: '4.9.0', + }; + default: + return null; + } + }, + } as Partial; + + await expect(detectLanguage(packageManager as any)).resolves.toBe(SupportedLanguage.JAVASCRIPT); }); describe('detectFrameworkPreset should return', () => { diff --git a/code/core/src/cli/detect.ts b/code/core/src/cli/detect.ts index 9f8eeac5ca00..ac9b15923799 100644 --- a/code/core/src/cli/detect.ts +++ b/code/core/src/cli/detect.ts @@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import type { JsPackageManager, PackageJsonWithMaybeDeps } from 'storybook/internal/common'; -import { HandledError, commandLog } from 'storybook/internal/common'; +import { HandledError, commandLog, getProjectRoot } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; import { findUpSync } from 'find-up'; @@ -112,9 +112,9 @@ export function detectFrameworkPreset( * @returns CoreBuilder */ export async function detectBuilder(packageManager: JsPackageManager, projectType: ProjectType) { - const viteConfig = findUpSync(viteConfigFiles); - const webpackConfig = findUpSync(webpackConfigFiles); - const dependencies = await packageManager.getAllDependencies(); + const viteConfig = findUpSync(viteConfigFiles, { stopAt: getProjectRoot() }); + const webpackConfig = findUpSync(webpackConfigFiles, { stopAt: getProjectRoot() }); + const dependencies = packageManager.getAllDependencies(); if (viteConfig || (dependencies.vite && dependencies.webpack === undefined)) { commandLog('Detected Vite project. Setting builder to Vite')(); @@ -181,21 +181,19 @@ export async function detectLanguage(packageManager: JsPackageManager) { return language; } - const isTypescriptDirectDependency = await packageManager - .getAllDependencies() - .then((deps) => Boolean(deps.typescript)); + const isTypescriptDirectDependency = !!packageManager.getAllDependencies().typescript; - const typescriptVersion = await packageManager.getPackageVersion('typescript'); - const prettierVersion = await packageManager.getPackageVersion('prettier'); - const babelPluginTransformTypescriptVersion = await packageManager.getPackageVersion( + const getModulePackageJSONVersion = (pkg: string) => { + return packageManager.getModulePackageJSON(pkg)?.version ?? null; + }; + + const typescriptVersion = getModulePackageJSONVersion('typescript'); + const prettierVersion = getModulePackageJSONVersion('prettier'); + const babelPluginTransformTypescriptVersion = getModulePackageJSONVersion( '@babel/plugin-transform-typescript' ); - const typescriptEslintParserVersion = await packageManager.getPackageVersion( - '@typescript-eslint/parser' - ); - - const eslintPluginStorybookVersion = - await packageManager.getPackageVersion('eslint-plugin-storybook'); + const typescriptEslintParserVersion = getModulePackageJSONVersion('@typescript-eslint/parser'); + const eslintPluginStorybookVersion = getModulePackageJSONVersion('eslint-plugin-storybook'); if (isTypescriptDirectDependency && typescriptVersion) { if ( @@ -228,19 +226,19 @@ export async function detect( packageManager: JsPackageManager, options: { force?: boolean; html?: boolean } = {} ) { - const packageJson = await packageManager.retrievePackageJson(); + try { + if (await isNxProject()) { + return ProjectType.NX; + } - if (!packageJson) { - return ProjectType.UNDETECTED; - } + if (options.html) { + return ProjectType.HTML; + } - if (await isNxProject()) { - return ProjectType.NX; - } + const { packageJson } = packageManager.primaryPackageJson; - if (options.html) { - return ProjectType.HTML; + return detectFrameworkPreset(packageJson); + } catch (e) { + return ProjectType.UNDETECTED; } - - return detectFrameworkPreset(packageJson); } diff --git a/code/core/src/cli/eslintPlugin.test.ts b/code/core/src/cli/eslintPlugin.test.ts index 90ecb611e291..a315f98b7430 100644 --- a/code/core/src/cli/eslintPlugin.test.ts +++ b/code/core/src/cli/eslintPlugin.test.ts @@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { findUp } from 'find-up'; import { dedent } from 'ts-dedent'; +import type { PackageJsonWithDepsAndDevDeps } from '../common'; import type { JsPackageManager } from '../common/js-package-manager/JsPackageManager'; import { configureEslintPlugin, @@ -29,13 +30,16 @@ vi.mock(import('node:fs/promises'), async (importOriginal) => { describe('extractEslintInfo', () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), + primaryPackageJson: { + packageJson: { dependencies: {}, devDependencies: {} } as PackageJsonWithDepsAndDevDeps, + packageJsonPath: '/some/path', + operationDir: '/some/path', + }, } satisfies Partial; beforeEach(() => { vi.mocked(findUp).mockClear(); mockPackageManager.getAllDependencies.mockClear(); - mockPackageManager.retrievePackageJson.mockClear(); }); it('should find ESLint config file with supported extension', async () => { @@ -65,8 +69,8 @@ describe('extractEslintInfo', () => { }); it('should handle missing ESLint config and no dependencies correctly', async () => { - mockPackageManager.getAllDependencies.mockResolvedValue({}); - mockPackageManager.retrievePackageJson.mockResolvedValue({}); + mockPackageManager.getAllDependencies.mockReturnValue({}); + mockPackageManager.primaryPackageJson.packageJson = { dependencies: {}, devDependencies: {} }; vi.mocked(findUp).mockImplementation(async () => undefined); @@ -78,13 +82,19 @@ describe('extractEslintInfo', () => { }); it('should extract ESLint info and detect ESLint config and Storybook plugin', async () => { - mockPackageManager.getAllDependencies.mockResolvedValue({ + mockPackageManager.getAllDependencies.mockReturnValue({ 'eslint-plugin-storybook': '1.0.0', eslint: '7.0.0', }); - mockPackageManager.retrievePackageJson.mockResolvedValue({ - eslintConfig: '.eslintrc.js', - }); + mockPackageManager.primaryPackageJson = { + packageJson: { + devDependencies: {}, + dependencies: {}, + eslintConfig: '.eslintrc.js', + }, + packageJsonPath: '/some/path', + operationDir: '/some/path', + }; vi.mocked(findUp).mockImplementation(async (fileName) => String(fileName) === '.eslintrc.js' ? String(fileName) : undefined @@ -104,7 +114,6 @@ describe('configureEslintPlugin', () => { it('should not configure ESLint plugin if it is already configured', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent`{ @@ -124,7 +133,6 @@ describe('configureEslintPlugin', () => { it('should configure ESLint plugin correctly', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent`{ @@ -153,7 +161,6 @@ describe('configureEslintPlugin', () => { it('should correctly parse, configure, and preserve comments in comment-json .eslintrc.json', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; // Mock file content with JSON5 features (comments, trailing comma) @@ -229,7 +236,6 @@ describe('configureEslintPlugin', () => { it('should not configure ESLint plugin if it is already configured', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent` @@ -251,7 +257,6 @@ describe('configureEslintPlugin', () => { it('should configure ESLint plugin correctly', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent` @@ -280,7 +285,6 @@ describe('configureEslintPlugin', () => { it('should configure ESLint plugin correctly with default JS flat config', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent` @@ -310,7 +314,6 @@ describe('configureEslintPlugin', () => { it('should configure ESLint plugin correctly with typescript-eslint flat config', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent` @@ -340,7 +343,6 @@ describe('configureEslintPlugin', () => { it('should configure ESLint plugin correctly with reexported const declaration', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent`import eslint from "@eslint/js"; @@ -373,7 +375,6 @@ describe('configureEslintPlugin', () => { it('should configure ESLint plugin correctly with TS aliased config', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent`import eslint from "@eslint/js"; @@ -405,7 +406,6 @@ describe('configureEslintPlugin', () => { it('should configure ESLint plugin correctly with TS satisfies config', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent`import eslint from "@eslint/js"; @@ -433,7 +433,6 @@ describe('configureEslintPlugin', () => { it('should just add an import if config is of custom unknown format', async () => { const mockPackageManager = { getAllDependencies: vi.fn(), - retrievePackageJson: vi.fn(), } satisfies Partial; const mockConfigFile = dedent`import someCustomConfig from 'my-eslint-config'; diff --git a/code/core/src/cli/eslintPlugin.ts b/code/core/src/cli/eslintPlugin.ts index ec35f782526f..db2361875674 100644 --- a/code/core/src/cli/eslintPlugin.ts +++ b/code/core/src/cli/eslintPlugin.ts @@ -1,6 +1,6 @@ import { readFile, writeFile } from 'node:fs/promises'; -import { paddedLog } from 'storybook/internal/common'; +import { type JsPackageManager, paddedLog } from 'storybook/internal/common'; import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; import commentJson from 'comment-json'; @@ -12,10 +12,6 @@ import { dedent } from 'ts-dedent'; import { babelParse, recast, types as t, traverse } from '../babel'; -// TODO: @kasperpeulen check how to use the JSPackageManager type from common later -// Right now there is a mismatch issue with the types because conflicts with baseGenerator.ts -type JsPackageManager = any; - export const SUPPORTED_ESLINT_EXTENSIONS = ['ts', 'mts', 'cts', 'mjs', 'js', 'cjs', 'json']; const UNSUPPORTED_ESLINT_EXTENSIONS = ['yaml', 'yml']; @@ -156,8 +152,8 @@ export async function extractEslintInfo(packageManager: JsPackageManager): Promi isFlatConfig: boolean; }> { let unsupportedExtension = undefined; - const allDependencies = await packageManager.getAllDependencies(); - const packageJson = await packageManager.retrievePackageJson(); + const allDependencies = packageManager.getAllDependencies(); + const { packageJson } = packageManager.primaryPackageJson; let eslintConfigFile: string | undefined = undefined; try { @@ -245,10 +241,10 @@ export async function configureEslintPlugin({ } } else { paddedLog(`Configuring eslint-plugin-storybook in your package.json`); - const packageJson = await packageManager.retrievePackageJson(); + const { packageJson } = packageManager.primaryPackageJson; const existingExtends = normalizeExtends(packageJson.eslintConfig?.extends).filter(Boolean); - await packageManager.writePackageJson({ + packageManager.writePackageJson({ ...packageJson, eslintConfig: { ...packageJson.eslintConfig, diff --git a/code/core/src/cli/helpers.test.ts b/code/core/src/cli/helpers.test.ts index b68a6a85dba4..a93910e38542 100644 --- a/code/core/src/cli/helpers.test.ts +++ b/code/core/src/cli/helpers.test.ts @@ -67,7 +67,11 @@ vi.mock('path', async (importOriginal) => { }); const packageManagerMock = { - retrievePackageJson: async () => ({ dependencies: {}, devDependencies: {} }), + primaryPackageJson: { + packageJson: { dependencies: {}, devDependencies: {} }, + packageJsonPath: '/some/path', + operationDir: '/some/path', + }, } as JsPackageManager; describe('Helpers', () => { @@ -88,7 +92,7 @@ describe('Helpers', () => { const packageManager = { getInstalledVersion: async (pkg: string) => pkg === 'svelte' ? svelteVersion : undefined, - getAllDependencies: async () => ({ svelte: `^${svelteVersion}` }), + getAllDependencies: () => ({ svelte: `^${svelteVersion}` }), } as any as JsPackageManager; await expect(helpers.getVersionSafe(packageManager, 'svelte')).resolves.toBe( expectedAddonSpecifier @@ -107,7 +111,7 @@ describe('Helpers', () => { ])('svelte %s => %s', async (svelteSpecifier, expectedAddonSpecifier) => { const packageManager = { getInstalledVersion: async (pkg: string) => undefined, - getAllDependencies: async () => ({ svelte: svelteSpecifier }), + getAllDependencies: () => ({ svelte: svelteSpecifier }), } as any as JsPackageManager; await expect(helpers.getVersionSafe(packageManager, 'svelte')).resolves.toBe( expectedAddonSpecifier @@ -251,15 +255,15 @@ describe('Helpers', () => { describe('hasStorybookDependencies', () => { it(`should return true when any storybook dependency exists`, async () => { - const result = await helpers.hasStorybookDependencies({ - getAllDependencies: async () => ({ storybook: 'x.y.z' }), + const result = helpers.hasStorybookDependencies({ + getAllDependencies: () => ({ storybook: 'x.y.z' }), } as unknown as JsPackageManager); expect(result).toEqual(true); }); it(`should return false when no storybook dependency exists`, async () => { - const result = await helpers.hasStorybookDependencies({ - getAllDependencies: async () => ({ axios: 'x.y.z' }), + const result = helpers.hasStorybookDependencies({ + getAllDependencies: () => ({ axios: 'x.y.z' }), } as unknown as JsPackageManager); expect(result).toEqual(false); }); diff --git a/code/core/src/cli/helpers.ts b/code/core/src/cli/helpers.ts index f13f990a5993..d9bfcbc96268 100644 --- a/code/core/src/cli/helpers.ts +++ b/code/core/src/cli/helpers.ts @@ -7,13 +7,14 @@ import { type PackageJson, type PackageJsonWithDepsAndDevDeps, frameworkToRenderer, + getProjectRoot, } from 'storybook/internal/common'; import { versions as storybookMonorepoPackages } from 'storybook/internal/common'; import type { SupportedFrameworks, SupportedRenderers } from 'storybook/internal/types'; import { findUpSync } from 'find-up'; import picocolors from 'picocolors'; -import { coerce, major, satisfies } from 'semver'; +import { coerce, satisfies } from 'semver'; import stripJsonComments from 'strip-json-comments'; import invariant from 'tiny-invariant'; @@ -67,21 +68,17 @@ export const writeFileAsJson = (jsonPath: string, content: unknown) => { * ]); * ``` * - * @param {Object} packageJson The current package.json so we can inspect its contents - * @returns {Array} Contains the packages and versions that need to be installed + * @param packageJson The current package.json so we can inspect its contents + * @returns Contains the packages and versions that need to be installed */ -export async function getBabelDependencies( - packageManager: JsPackageManager, - packageJson: PackageJsonWithDepsAndDevDeps -) { +export async function getBabelDependencies(packageManager: JsPackageManager) { const dependenciesToAdd = []; let babelLoaderVersion = '^8.0.0-0'; - const babelCoreVersion = - packageJson.dependencies['babel-core'] || packageJson.devDependencies['babel-core']; + const babelCoreVersion = packageManager.getDependencyVersion('babel-core'); if (!babelCoreVersion) { - if (!packageJson.dependencies['@babel/core'] && !packageJson.devDependencies['@babel/core']) { + if (!packageManager.getDependencyVersion('@babel/core')) { const babelCoreInstallVersion = await packageManager.getVersion('@babel/core'); dependenciesToAdd.push(`@babel/core@${babelCoreInstallVersion}`); } @@ -96,7 +93,7 @@ export async function getBabelDependencies( } } - if (!packageJson.dependencies['babel-loader'] && !packageJson.devDependencies['babel-loader']) { + if (!packageManager.getDependencyVersion('babel-loader')) { const babelLoaderInstallVersion = await packageManager.getVersion( 'babel-loader', babelLoaderVersion @@ -176,7 +173,7 @@ export async function getVersionSafe(packageManager: JsPackageManager, packageNa try { let version = await packageManager.getInstalledVersion(packageName); if (!version) { - const deps = await packageManager.getAllDependencies(); + const deps = packageManager.getAllDependencies(); const versionSpecifier = deps[packageName]; version = versionSpecifier ?? ''; } @@ -284,7 +281,7 @@ export function getStorybookVersionSpecifier(packageJson: PackageJsonWithDepsAnd } export async function isNxProject() { - return findUpSync('nx.json'); + return findUpSync('nx.json', { stopAt: getProjectRoot() }); } export function coerceSemver(version: string) { @@ -293,8 +290,8 @@ export function coerceSemver(version: string) { return coercedSemver; } -export async function hasStorybookDependencies(packageManager: JsPackageManager) { - const currentPackageDeps = await packageManager.getAllDependencies(); +export function hasStorybookDependencies(packageManager: JsPackageManager) { + const currentPackageDeps = packageManager.getAllDependencies(); return Object.keys(currentPackageDeps).some((dep) => dep.includes('storybook')); } diff --git a/code/core/src/common/js-package-manager/BUNProxy.ts b/code/core/src/common/js-package-manager/BUNProxy.ts index 543d10abe1b9..1176bdfccf33 100644 --- a/code/core/src/common/js-package-manager/BUNProxy.ts +++ b/code/core/src/common/js-package-manager/BUNProxy.ts @@ -5,11 +5,12 @@ import { join } from 'node:path'; import { logger } from 'storybook/internal/node-logger'; import { FindPackageVersionsError } from 'storybook/internal/server-errors'; -import { findUp } from 'find-up'; +import { findUpSync } from 'find-up'; import sort from 'semver/functions/sort.js'; import { dedent } from 'ts-dedent'; import { createLogStream } from '../utils/cli'; +import { getProjectRoot } from '../utils/paths'; import { JsPackageManager } from './JsPackageManager'; import type { PackageJson } from './PackageJson'; import type { InstallationMetadata, PackageMetadata } from './types'; @@ -81,20 +82,17 @@ export class BUNProxy extends JsPackageManager { return `bun run ${command}`; } - getRemoteRunCommand(): string { - return 'bunx'; + getRemoteRunCommand(pkg: string, args: string[], specifier?: string): string { + return `bunx ${pkg}${specifier ? `@${specifier}` : ''} ${args.join(' ')}`; } - public async getPackageJSON( - packageName: string, - basePath = this.cwd - ): Promise { - const packageJsonPath = await findUp( + public getModulePackageJSON(packageName: string): PackageJson | null { + const packageJsonPath = findUpSync( (dir) => { const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); return existsSync(possiblePath) ? possiblePath : undefined; }, - { cwd: basePath } + { cwd: this.cwd, stopAt: getProjectRoot() } ); if (!packageJsonPath) { @@ -180,6 +178,7 @@ export class BUNProxy extends JsPackageManager { command: 'bun', args: ['install', ...this.getInstallArgs()], stdio: 'inherit', + cwd: this.cwd, }); } @@ -211,6 +210,7 @@ export class BUNProxy extends JsPackageManager { command: 'bun', args: ['add', ...args, ...this.getInstallArgs()], stdio: process.env.CI || !writeOutputToFile ? 'inherit' : ['ignore', logStream, logStream], + cwd: this.primaryPackageJson.operationDir, }); } catch (err) { if (!writeOutputToFile) { @@ -229,13 +229,14 @@ export class BUNProxy extends JsPackageManager { await removeLogFile(); } - protected async runRemoveDeps(dependencies: string[]) { + protected async runRemoveDeps(dependencies: string[], cwd = this.cwd) { const args = [...dependencies]; await this.executeCommand({ command: 'bun', args: ['remove', ...args, ...this.getInstallArgs()], stdio: 'inherit', + cwd, }); } diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts index 3f9b7a0aafaa..62ea3cb21c4b 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.ts @@ -1,15 +1,15 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { readFile, writeFile } from 'node:fs/promises'; -import { dirname, resolve } from 'node:path'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; // eslint-disable-next-line depend/ban-dependencies import { type CommonOptions, execaCommand, execaCommandSync } from 'execa'; +import { findUpMultipleSync, findUpSync } from 'find-up'; import picocolors from 'picocolors'; import { gt, satisfies } from 'semver'; import invariant from 'tiny-invariant'; -import { dedent } from 'ts-dedent'; import { HandledError } from '../utils/HandledError'; +import { getProjectRoot } from '../utils/paths'; import storybookPackagesVersions from '../versions'; import type { PackageJson, PackageJsonWithDepsAndDevDeps } from './PackageJson'; import type { InstallationMetadata } from './types'; @@ -46,83 +46,76 @@ export function getPackageDetails(pkg: string): [string, string?] { interface JsPackageManagerOptions { cwd?: string; + configDir?: string; } -export abstract class JsPackageManager { - public abstract readonly type: PackageManagerName; - - public abstract initPackageJson(): Promise; - - public abstract getRunStorybookCommand(): string; - public abstract getRunCommand(command: string): string; +export abstract class JsPackageManager { + abstract readonly type: PackageManagerName; - public abstract getRemoteRunCommand(): string; + /** The path to the primary package.json file (contains the `storybook` dependency). */ + readonly primaryPackageJson: { + packageJsonPath: string; + operationDir: string; + packageJson: PackageJsonWithDepsAndDevDeps; + }; - public readonly cwd?: string; + /** The paths to all package.json files in the project root. */ + readonly packageJsonPaths: string[]; - public abstract getPackageJSON( - packageName: string, - basePath?: string - ): Promise; + /** + * The path to the Storybook instance directory. This is used to find the primary package.json + * file in a repository. + */ + readonly #instanceDir: string; - /** Get the INSTALLED version of a package from the package.json file */ - async getPackageVersion(packageName: string, basePath = this.cwd): Promise { - const packageJSON = await this.getPackageJSON(packageName, basePath); - return packageJSON ? (packageJSON.version ?? null) : null; - } + /** The current working directory. */ + protected readonly cwd: string; constructor(options?: JsPackageManagerOptions) { this.cwd = options?.cwd || process.cwd(); + this.#instanceDir = options?.configDir ? dirname(join(this.cwd, options.configDir)) : this.cwd; + this.packageJsonPaths = JsPackageManager.listAllPackageJsonPaths(this.#instanceDir); + this.primaryPackageJson = this.#getPrimaryPackageJson(); } + /** Runs arbitrary package scripts. */ + abstract getRunCommand(command: string): string; /** - * Detect whether Storybook gets initialized in a mono-repository/workspace environment The cwd - * doesn't have to be the root of the monorepo, it can be a subdirectory - * - * @returns `true`, if Storybook is initialized inside a mono-repository/workspace + * Run a command from a local or remote. Fetches a package from the registry without installing it + * as a dependency, hotloads it, and runs whatever default command binary it exposes. */ - public isStorybookInMonorepo() { - let cwd = process.cwd(); + abstract getRemoteRunCommand(pkg: string, args: string[], specifier?: string): string; - while (true) { - try { - const turboJsonPath = `${cwd}/turbo.json`; - const rushJsonPath = `${cwd}/rush.json`; - - if (existsSync(turboJsonPath) || existsSync(rushJsonPath)) { - return true; - } + /** Get the package.json file for a given module. */ + abstract getModulePackageJSON(packageName: string): PackageJson | null; - const packageJsonPath = require.resolve(`${cwd}/package.json`); + isStorybookInMonorepo() { + const turboJsonPath = findUpSync(`turbo.json`, { stopAt: getProjectRoot() }); + const rushJsonPath = findUpSync(`rush.json`, { stopAt: getProjectRoot() }); + const nxJsonPath = findUpSync(`nx.json`, { stopAt: getProjectRoot() }); - // read packagejson with readFileSync - const packageJsonFile = readFileSync(packageJsonPath, 'utf8'); - const packageJson = JSON.parse(packageJsonFile) as PackageJsonWithDepsAndDevDeps; + if (turboJsonPath || rushJsonPath || nxJsonPath) { + return true; + } - if (packageJson.workspaces) { - return true; - } - } catch (err) { - // Package.json not found or invalid in current directory - } + const packageJsonPaths = findUpMultipleSync(`package.json`, { stopAt: getProjectRoot() }); + if (packageJsonPaths.length === 0) { + return false; + } - // Move up to the parent directory - const parentDir = dirname(cwd); + for (const packageJsonPath of packageJsonPaths) { + const packageJsonFile = readFileSync(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageJsonFile) as PackageJsonWithDepsAndDevDeps; - // Check if we have reached the root of the filesystem - if (parentDir === cwd) { - break; + if (packageJson.workspaces) { + return true; } - - // Update cwd to the parent directory - cwd = parentDir; } return false; } - /** Install dependencies listed in `package.json` */ - public async installDependencies() { + async installDependencies() { logger.log('Installing dependencies...'); logger.log(); @@ -134,88 +127,43 @@ export abstract class JsPackageManager { } } - packageJsonPath(): string { - if (!this.cwd) { - throw new Error('Missing cwd'); - } - return resolve(this.cwd, 'package.json'); - } - - async readPackageJson(): Promise { - const packageJsonPath = this.packageJsonPath(); - if (!existsSync(packageJsonPath)) { - throw new Error(`Could not read package.json file at ${packageJsonPath}`); - } + /** Read the `package.json` file available in the provided directory */ + static getPackageJson(packageJsonPath: string): PackageJsonWithDepsAndDevDeps { + const jsonContent = readFileSync(packageJsonPath, 'utf8'); + const packageJSON = JSON.parse(jsonContent); - const jsonContent = await readFile(packageJsonPath, 'utf8'); - return JSON.parse(jsonContent); + return { + ...packageJSON, + dependencies: { ...packageJSON.dependencies }, + devDependencies: { ...packageJSON.devDependencies }, + peerDependencies: { ...packageJSON.peerDependencies }, + }; } - async writePackageJson(packageJson: PackageJson) { + writePackageJson(packageJson: PackageJson, directory = this.cwd) { const packageJsonToWrite = { ...packageJson }; - // make sure to not accidentally add empty fields - if ( - packageJsonToWrite.dependencies && - Object.keys(packageJsonToWrite.dependencies).length === 0 - ) { - delete packageJsonToWrite.dependencies; - } - if ( - packageJsonToWrite.devDependencies && - Object.keys(packageJsonToWrite.devDependencies).length === 0 - ) { - delete packageJsonToWrite.devDependencies; - } - if ( - packageJsonToWrite.peerDependencies && - Object.keys(packageJsonToWrite.peerDependencies).length === 0 - ) { - delete packageJsonToWrite.peerDependencies; - } - - const content = `${JSON.stringify(packageJsonToWrite, null, 2)}\n`; - await writeFile(this.packageJsonPath(), content, 'utf8'); - } + const dependencyTypes = ['dependencies', 'devDependencies', 'peerDependencies'] as const; - /** - * Read the `package.json` file available in the directory the command was call from If there is - * no `package.json` it will create one. - */ - public async retrievePackageJson(): Promise { - let packageJson; - try { - packageJson = await this.readPackageJson(); - } catch (err) { - const errMessage = String(err); - if (errMessage.includes('Could not read package.json')) { - await this.initPackageJson(); - packageJson = await this.readPackageJson(); - } else { - throw new Error( - dedent` - There was an error while reading the package.json file at ${this.packageJsonPath()}: ${errMessage} - Please fix the error and try again. - ` - ); + // Remove empty dependency objects + dependencyTypes.forEach((type) => { + if (packageJsonToWrite[type] && Object.keys(packageJsonToWrite[type]).length === 0) { + delete packageJsonToWrite[type]; } - } + }); - return { - ...packageJson, - dependencies: { ...packageJson.dependencies }, - devDependencies: { ...packageJson.devDependencies }, - peerDependencies: { ...packageJson.peerDependencies }, - }; + const content = `${JSON.stringify(packageJsonToWrite, null, 2)}\n`; + writeFileSync(resolve(directory, 'package.json'), content, 'utf8'); } - public async getAllDependencies(): Promise>> { - const { dependencies, devDependencies, peerDependencies } = await this.retrievePackageJson(); + getAllDependencies() { + const { packageJson } = this.primaryPackageJson; + const { dependencies, devDependencies, peerDependencies } = packageJson; return { ...dependencies, ...devDependencies, ...peerDependencies, - }; + } as Record; } /** @@ -238,34 +186,29 @@ export abstract class JsPackageManager { options: { skipInstall?: boolean; installAsDevDependencies?: boolean; - packageJson?: PackageJson; writeOutputToFile?: boolean; }, dependencies: string[] ) { const { skipInstall, writeOutputToFile = true } = options; + const { operationDir, packageJson } = this.primaryPackageJson; + if (skipInstall) { - const { packageJson } = options; - invariant(packageJson, 'Missing packageJson.'); + const dependenciesMap: Record = {}; - const dependenciesMap = dependencies.reduce((acc, dep) => { + for (const dep of dependencies) { const [packageName, packageVersion] = getPackageDetails(dep); - return { ...acc, [packageName]: packageVersion }; - }, {}); - - if (options.installAsDevDependencies) { - packageJson.devDependencies = { - ...packageJson.devDependencies, - ...dependenciesMap, - }; - } else { - packageJson.dependencies = { - ...packageJson.dependencies, - ...dependenciesMap, - }; + const latestVersion = await this.getVersion(packageName); + dependenciesMap[packageName] = packageVersion ?? latestVersion; } - await this.writePackageJson(packageJson); + + const targetDeps = options.installAsDevDependencies + ? packageJson.devDependencies + : packageJson.dependencies; + + Object.assign(targetDeps, dependenciesMap); + this.writePackageJson(packageJson, operationDir); } else { try { await this.runAddDeps( @@ -282,48 +225,42 @@ export abstract class JsPackageManager { } /** - * Remove dependencies from a project using `yarn remove` or `npm uninstall`. + * Removing dependencies from the package.json file, which is found first starting from the + * instance root. The method does not run a package manager install like `npm install`. * * @example * * ```ts - * removeDependencies(options, [`@storybook/react`]); + * removeDependencies([`@storybook/react`]); * ``` * - * @param {Object} options Contains `skipInstall`, `packageJson` and `installAsDevDependencies` - * which we use to determine how we install packages. - * @param {Array} dependencies Contains a list of packages to remove. + * @param dependencies Contains a list of packages to remove. */ - public async removeDependencies( - options: { - skipInstall?: boolean; - packageJson?: PackageJson; - }, - dependencies: string[] - ): Promise { - const { skipInstall } = options; - - if (skipInstall) { - const { packageJson } = options; - - invariant(packageJson, 'Missing packageJson.'); - dependencies.forEach((dep) => { - if (packageJson.devDependencies) { - delete packageJson.devDependencies[dep]; - } - if (packageJson.dependencies) { - delete packageJson.dependencies[dep]; - } - }); - - await this.writePackageJson(packageJson); - } else { + async removeDependencies(dependencies: string[]): Promise { + for (const pjPath of this.packageJsonPaths) { try { - await this.runRemoveDeps(dependencies); + const currentPackageJson = JsPackageManager.getPackageJson(pjPath); + let modified = false; + dependencies.forEach((dep) => { + if (currentPackageJson.dependencies && currentPackageJson.dependencies[dep]) { + delete currentPackageJson.dependencies[dep]; + modified = true; + } + if (currentPackageJson.devDependencies && currentPackageJson.devDependencies[dep]) { + delete currentPackageJson.devDependencies[dep]; + modified = true; + } + if (currentPackageJson.peerDependencies && currentPackageJson.peerDependencies[dep]) { + delete currentPackageJson.peerDependencies[dep]; + modified = true; + } + }); + if (modified) { + this.writePackageJson(currentPackageJson, dirname(pjPath)); + break; + } } catch (e) { - logger.error('An error occurred while removing dependencies.'); - logger.log(String(e)); - throw new HandledError(e); + logger.warn(`Could not process ${pjPath} for dependency removal: ${String(e)}`); } } } @@ -434,6 +371,7 @@ export abstract class JsPackageManager { const latestVersionSatisfyingTheConstraint = versions .reverse() .find((version) => satisfies(version, constraint)); + invariant( latestVersionSatisfyingTheConstraint != null, `No version satisfying the constraint: ${packageName}${constraint}` @@ -441,36 +379,40 @@ export abstract class JsPackageManager { return latestVersionSatisfyingTheConstraint; } - public async addStorybookCommandInScripts(options?: { port: number; preCommand?: string }) { + public addStorybookCommandInScripts(options?: { port: number; preCommand?: string }) { const sbPort = options?.port ?? 6006; const storybookCmd = `storybook dev -p ${sbPort}`; - const buildStorybookCmd = `storybook build`; const preCommand = options?.preCommand ? this.getRunCommand(options.preCommand) : undefined; - await this.addScripts({ + + this.addScripts({ storybook: [preCommand, storybookCmd].filter(Boolean).join(' && '), 'build-storybook': [preCommand, buildStorybookCmd].filter(Boolean).join(' && '), }); } - public async addScripts(scripts: Record) { - const packageJson = await this.retrievePackageJson(); - await this.writePackageJson({ - ...packageJson, - scripts: { - ...packageJson.scripts, - ...scripts, + public addScripts(scripts: Record) { + const { operationDir, packageJson } = this.#getPrimaryPackageJson(); + + this.writePackageJson( + { + ...packageJson, + scripts: { + ...packageJson.scripts, + ...scripts, + }, }, - }); + operationDir + ); } - public async addPackageResolutions(versions: Record) { - const packageJson = await this.retrievePackageJson(); + public addPackageResolutions(versions: Record) { + const { operationDir, packageJson } = this.#getPrimaryPackageJson(); + const resolutions = this.getResolutions(packageJson, versions); - this.writePackageJson({ ...packageJson, ...resolutions }); + this.writePackageJson({ ...packageJson, ...resolutions }, operationDir); } - protected abstract runInstall(): Promise; protected abstract runAddDeps( @@ -479,7 +421,7 @@ export abstract class JsPackageManager { writeOutputToFile?: boolean ): Promise; - protected abstract runRemoveDeps(dependencies: string[]): Promise; + protected abstract runRemoveDeps(dependencies: string[], cwd?: string): Promise; protected abstract getResolutions( packageJson: PackageJson, @@ -601,4 +543,79 @@ export abstract class JsPackageManager { return ''; } } + + /** + * Searches for a dependency/devDependency in all package.json files and returns the version of + * the dependency. + */ + public getDependencyVersion(dependency: string): string | null { + const dependencyVersion = this.packageJsonPaths + .map((path) => { + const packageJson = JsPackageManager.getPackageJson(path); + return packageJson.dependencies?.[dependency] ?? packageJson.devDependencies?.[dependency]; + }) + .filter(Boolean); + return dependencyVersion[0] ?? null; + } + + // Helper to read and check a package.json for storybook dependency + static hasStorybookDependency(packageJsonPath: string): boolean { + try { + const content = readFileSync(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(content) as PackageJsonWithDepsAndDevDeps; + return !!( + (packageJson.dependencies && packageJson.dependencies['storybook']) || + (packageJson.devDependencies && packageJson.devDependencies['storybook']) + ); + } catch (error) { + return false; // If file doesn't exist or is unreadable, or JSON is invalid + } + } + + /** + * Find the primary package.json file in the project root. The primary package.json file is the + * one that contains the `storybook` dependency. If no primary package.json file is found, the + * function will return the package.json file in the project root. + */ + #findPrimaryPackageJsonPath(): string { + for (const packageJsonPath of this.packageJsonPaths) { + const hasStorybook = JsPackageManager.hasStorybookDependency(packageJsonPath); + if (hasStorybook) { + return packageJsonPath; + } + } + + // Fall back to cwd package.json + return this.packageJsonPaths[0] ?? resolve(this.cwd, 'package.json'); + } + + /** List all package.json files starting from the given directory and stopping at the project root. */ + static listAllPackageJsonPaths(instanceDir: string): string[] { + return findUpMultipleSync('package.json', { + cwd: instanceDir, + stopAt: getProjectRoot(), + }); + } + + /** + * Get the primary package.json file and its operation directory. The primary package.json file is + * the one that contains the storybook dependency. If the primary package.json file is not found, + * the function returns information about thepackage.json file in the current working directory. + */ + #getPrimaryPackageJson(): { + packageJsonPath: string; + operationDir: string; + packageJson: PackageJsonWithDepsAndDevDeps; + } { + const finalTargetPackageJsonPath = this.#findPrimaryPackageJsonPath(); + + const operationDir = dirname(finalTargetPackageJsonPath); + return { + packageJsonPath: finalTargetPackageJsonPath, + operationDir, + get packageJson() { + return JsPackageManager.getPackageJson(finalTargetPackageJsonPath); + }, + }; + } } diff --git a/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts b/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts index 888f18a598a4..092fd60c7f2f 100644 --- a/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts +++ b/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts @@ -3,7 +3,7 @@ import { join } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { sync as spawnSync } from 'cross-spawn'; -import { findUpSync } from 'find-up'; +import { findUpMultipleSync, findUpSync } from 'find-up'; import { BUNProxy } from './BUNProxy'; import { JsPackageManagerFactory } from './JsPackageManagerFactory'; @@ -17,10 +17,11 @@ const spawnSyncMock = vi.mocked(spawnSync); vi.mock('find-up'); const findUpSyncMock = vi.mocked(findUpSync); - +const findUpMultipleSyncMock = vi.mocked(findUpMultipleSync); describe('CLASS: JsPackageManagerFactory', () => { beforeEach(() => { findUpSyncMock.mockReturnValue(undefined); + findUpMultipleSyncMock.mockReturnValue([]); spawnSyncMock.mockReturnValue({ status: 1 } as any); delete process.env.npm_config_user_agent; }); diff --git a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts index 83b486fc4db5..8026a7579bb8 100644 --- a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts +++ b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts @@ -3,6 +3,7 @@ import { basename, parse, relative } from 'node:path'; import { sync as spawnSync } from 'cross-spawn'; import { findUpSync } from 'find-up'; +import { getProjectRoot } from '../utils/paths'; import { BUNProxy } from './BUNProxy'; import type { JsPackageManager, PackageManagerName } from './JsPackageManager'; import { COMMON_ENV_VARS } from './JsPackageManager'; @@ -10,12 +11,13 @@ import { NPMProxy } from './NPMProxy'; import { PNPMProxy } from './PNPMProxy'; import { Yarn1Proxy } from './Yarn1Proxy'; import { Yarn2Proxy } from './Yarn2Proxy'; - -const NPM_LOCKFILE = 'package-lock.json'; -const PNPM_LOCKFILE = 'pnpm-lock.yaml'; -const YARN_LOCKFILE = 'yarn.lock'; -const BUN_LOCKFILE = 'bun.lock'; -const BUN_LOCKFILE_BINARY = 'bun.lockb'; +import { + BUN_LOCKFILE, + BUN_LOCKFILE_BINARY, + NPM_LOCKFILE, + PNPM_LOCKFILE, + YARN_LOCKFILE, +} from './constants'; type PackageManagerProxy = | typeof NPMProxy @@ -26,20 +28,20 @@ type PackageManagerProxy = export class JsPackageManagerFactory { public static getPackageManager( - { force }: { force?: PackageManagerName } = {}, + { force, configDir = '.storybook' }: { force?: PackageManagerName; configDir?: string } = {}, cwd?: string ): JsPackageManager { // Option 1: If the user has provided a forcing flag, we use it if (force && force in this.PROXY_MAP) { - return new this.PROXY_MAP[force]({ cwd }); + return new this.PROXY_MAP[force]({ cwd, configDir }); } const lockFiles = [ - findUpSync(YARN_LOCKFILE, { cwd }), - findUpSync(PNPM_LOCKFILE, { cwd }), - findUpSync(NPM_LOCKFILE, { cwd }), - findUpSync(BUN_LOCKFILE, { cwd }), - findUpSync(BUN_LOCKFILE_BINARY, { cwd }), + findUpSync(YARN_LOCKFILE, { cwd, stopAt: getProjectRoot() }), + findUpSync(PNPM_LOCKFILE, { cwd, stopAt: getProjectRoot() }), + findUpSync(NPM_LOCKFILE, { cwd, stopAt: getProjectRoot() }), + findUpSync(BUN_LOCKFILE, { cwd, stopAt: getProjectRoot() }), + findUpSync(BUN_LOCKFILE_BINARY, { cwd, stopAt: getProjectRoot() }), ] .filter(Boolean) .sort((a, b) => { @@ -70,22 +72,24 @@ export class JsPackageManagerFactory { const yarnVersion = getYarnVersion(cwd); if (yarnVersion && (closestLockfile === YARN_LOCKFILE || (!hasNPMCommand && !hasPNPMCommand))) { - return yarnVersion === 1 ? new Yarn1Proxy({ cwd }) : new Yarn2Proxy({ cwd }); + return yarnVersion === 1 + ? new Yarn1Proxy({ cwd, configDir }) + : new Yarn2Proxy({ cwd, configDir }); } if (hasPNPMCommand && closestLockfile === PNPM_LOCKFILE) { - return new PNPMProxy({ cwd }); + return new PNPMProxy({ cwd, configDir }); } if (hasNPMCommand && closestLockfile === NPM_LOCKFILE) { - return new NPMProxy({ cwd }); + return new NPMProxy({ cwd, configDir }); } if ( hasBunCommand && (closestLockfile === BUN_LOCKFILE || closestLockfile === BUN_LOCKFILE_BINARY) ) { - return new BUNProxy({ cwd }); + return new BUNProxy({ cwd, configDir }); } // Option 3: If the user is running a command via npx/pnpx/yarn create/etc, we infer the package manager from the command @@ -97,7 +101,7 @@ export class JsPackageManagerFactory { // Default fallback, whenever users try to use something different than NPM, PNPM, Yarn, // but still have NPM installed if (hasNPMCommand) { - return new NPMProxy({ cwd }); + return new NPMProxy({ cwd, configDir }); } throw new Error('Unable to find a usable package manager within NPM, PNPM, Yarn and Yarn 2'); diff --git a/code/core/src/common/js-package-manager/NPMProxy.test.ts b/code/core/src/common/js-package-manager/NPMProxy.test.ts index 9182685351fa..d2782260eb12 100644 --- a/code/core/src/common/js-package-manager/NPMProxy.test.ts +++ b/code/core/src/common/js-package-manager/NPMProxy.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { JsPackageManager } from './JsPackageManager'; import { NPMProxy } from './NPMProxy'; // mock createLogStream @@ -17,24 +18,13 @@ describe('NPM Proxy', () => { beforeEach(() => { npmProxy = new NPMProxy(); + vi.spyOn(npmProxy, 'writePackageJson').mockImplementation(vi.fn()); }); it('type should be npm', () => { expect(npmProxy.type).toEqual('npm'); }); - describe('initPackageJson', () => { - it('should run `npm init -y`', async () => { - const executeCommandSpy = vi.spyOn(npmProxy, 'executeCommand').mockResolvedValueOnce(''); - - await npmProxy.initPackageJson(); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ command: 'npm', args: ['init', '-y'] }) - ); - }); - }); - describe('installDependencies', () => { describe('npm6', () => { it('should run `npm install`', async () => { @@ -135,65 +125,36 @@ describe('NPM Proxy', () => { }); describe('removeDependencies', () => { - describe('npm6', () => { - it('with devDep it should run `npm uninstall storybook`', async () => { - const executeCommandSpy = vi - .spyOn(npmProxy, 'executeCommand') - .mockResolvedValueOnce('6.0.0'); - - npmProxy.removeDependencies({}, ['storybook']); - - expect(executeCommandSpy).toHaveBeenLastCalledWith( - expect.objectContaining({ - command: 'npm', - args: ['uninstall', 'storybook'], - }) - ); - }); - }); - describe('npm7', () => { - it('with devDep it should run `npm uninstall storybook`', async () => { - const executeCommandSpy = vi - .spyOn(npmProxy, 'executeCommand') - .mockResolvedValueOnce('7.0.0'); - - await npmProxy.removeDependencies({}, ['storybook']); - - expect(executeCommandSpy).toHaveBeenLastCalledWith( - expect.objectContaining({ - command: 'npm', - args: ['uninstall', 'storybook'], - }) - ); - }); - }); describe('skipInstall', () => { it('should only change package.json without running install', async () => { const executeCommandSpy = vi .spyOn(npmProxy, 'executeCommand') .mockResolvedValueOnce('7.0.0'); - const writePackageSpy = vi - .spyOn(npmProxy, 'writePackageJson') - .mockImplementation(vi.fn()); - await npmProxy.removeDependencies( + vi.spyOn(npmProxy, 'packageJsonPaths', 'get').mockImplementation(() => ['package.json']); + + const writePackageSpy = vi.spyOn(npmProxy, 'writePackageJson').mockImplementation(vi.fn()); + vi.spyOn(JsPackageManager, 'getPackageJson').mockImplementation((args) => { + return { + dependencies: {}, + devDependencies: { + '@storybook/manager-webpack5': 'x.x.x', + '@storybook/react': 'x.x.x', + }, + }; + }); + + await npmProxy.removeDependencies(['@storybook/manager-webpack5']); + + expect(writePackageSpy).toHaveBeenCalledWith( { - skipInstall: true, - packageJson: { - devDependencies: { - '@storybook/manager-webpack5': 'x.x.x', - '@storybook/react': 'x.x.x', - }, + dependencies: {}, + devDependencies: { + '@storybook/react': 'x.x.x', }, }, - ['@storybook/manager-webpack5'] + expect.any(String) ); - - expect(writePackageSpy).toHaveBeenCalledWith({ - devDependencies: { - '@storybook/react': 'x.x.x', - }, - }); expect(executeCommandSpy).not.toHaveBeenCalled(); }); }); @@ -277,33 +238,32 @@ describe('NPM Proxy', () => { describe('addPackageResolutions', () => { it('adds resolutions to package.json and account for existing resolutions', async () => { - const writePackageSpy = vi - .spyOn(npmProxy, 'writePackageJson') - .mockImplementation(vi.fn()); + const writePackageSpy = vi.spyOn(npmProxy, 'writePackageJson').mockImplementation(vi.fn()); - vi.spyOn(npmProxy, 'retrievePackageJson').mockImplementation( - vi.fn(async () => ({ - dependencies: {}, - devDependencies: {}, - overrides: { - bar: 'x.x.x', - }, - })) - ); + vi.spyOn(JsPackageManager, 'getPackageJson').mockImplementation(() => ({ + dependencies: {}, + devDependencies: {}, + overrides: { + bar: 'x.x.x', + }, + })); const versions = { foo: 'x.x.x', }; - await npmProxy.addPackageResolutions(versions); + npmProxy.addPackageResolutions(versions); - expect(writePackageSpy).toHaveBeenCalledWith({ - dependencies: {}, - devDependencies: {}, - overrides: { - ...versions, - bar: 'x.x.x', + expect(writePackageSpy).toHaveBeenCalledWith( + { + dependencies: {}, + devDependencies: {}, + overrides: { + ...versions, + bar: 'x.x.x', + }, }, - }); + expect.any(String) + ); }); }); diff --git a/code/core/src/common/js-package-manager/NPMProxy.ts b/code/core/src/common/js-package-manager/NPMProxy.ts index 533cdc02abad..ae5628315fe7 100644 --- a/code/core/src/common/js-package-manager/NPMProxy.ts +++ b/code/core/src/common/js-package-manager/NPMProxy.ts @@ -5,11 +5,12 @@ import { join } from 'node:path'; import { logger } from 'storybook/internal/node-logger'; import { FindPackageVersionsError } from 'storybook/internal/server-errors'; -import { findUp } from 'find-up'; +import { findUpSync } from 'find-up'; import sort from 'semver/functions/sort.js'; import { dedent } from 'ts-dedent'; import { createLogStream } from '../utils/cli'; +import { getProjectRoot } from '../utils/paths'; import { JsPackageManager } from './JsPackageManager'; import type { PackageJson } from './PackageJson'; import type { InstallationMetadata, PackageMetadata } from './types'; @@ -69,32 +70,21 @@ export class NPMProxy extends JsPackageManager { installArgs: string[] | undefined; - async initPackageJson() { - await this.executeCommand({ command: 'npm', args: ['init', '-y'] }); - } - - getRunStorybookCommand(): string { - return 'npm run storybook'; - } - getRunCommand(command: string): string { return `npm run ${command}`; } - getRemoteRunCommand(): string { - return 'npx'; + getRemoteRunCommand(pkg: string, args: string[], specifier?: string): string { + return `npx ${pkg}${specifier ? `@${specifier}` : ''} ${args.join(' ')}`; } - public async getPackageJSON( - packageName: string, - basePath = this.cwd - ): Promise { - const packageJsonPath = await findUp( + getModulePackageJSON(packageName: string): PackageJson | null { + const packageJsonPath = findUpSync( (dir) => { const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); return existsSync(possiblePath) ? possiblePath : undefined; }, - { cwd: basePath } + { cwd: this.cwd, stopAt: getProjectRoot() } ); if (!packageJsonPath) { @@ -180,6 +170,7 @@ export class NPMProxy extends JsPackageManager { command: 'npm', args: ['install', ...this.getInstallArgs()], stdio: 'inherit', + cwd: this.cwd, }); } @@ -211,6 +202,7 @@ export class NPMProxy extends JsPackageManager { command: 'npm', args: ['install', ...args, ...this.getInstallArgs()], stdio: process.env.CI || !writeOutputToFile ? 'inherit' : ['ignore', logStream, logStream], + cwd: this.primaryPackageJson.operationDir, }); } catch (err) { if (!writeOutputToFile) { @@ -229,13 +221,14 @@ export class NPMProxy extends JsPackageManager { await removeLogFile(); } - protected async runRemoveDeps(dependencies: string[]) { + protected async runRemoveDeps(dependencies: string[], cwd = this.cwd) { const args = [...dependencies]; await this.executeCommand({ command: 'npm', args: ['uninstall', ...this.getInstallArgs(), ...args], stdio: 'inherit', + cwd, }); } diff --git a/code/core/src/common/js-package-manager/PNPMProxy.test.ts b/code/core/src/common/js-package-manager/PNPMProxy.test.ts index 2a6a186c9c90..ed6cc2274920 100644 --- a/code/core/src/common/js-package-manager/PNPMProxy.test.ts +++ b/code/core/src/common/js-package-manager/PNPMProxy.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { JsPackageManager } from './JsPackageManager'; import { PNPMProxy } from './PNPMProxy'; describe('PNPM Proxy', () => { @@ -7,24 +8,13 @@ describe('PNPM Proxy', () => { beforeEach(() => { pnpmProxy = new PNPMProxy(); + vi.spyOn(pnpmProxy, 'writePackageJson').mockImplementation(vi.fn()); }); it('type should be pnpm', () => { expect(pnpmProxy.type).toEqual('pnpm'); }); - describe('initPackageJson', () => { - it('should run `pnpm init`', async () => { - const executeCommandSpy = vi.spyOn(pnpmProxy, 'executeCommand').mockResolvedValueOnce(''); - - await pnpmProxy.initPackageJson(); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ command: 'pnpm', args: ['init'] }) - ); - }); - }); - describe('installDependencies', () => { it('should run `pnpm install`', async () => { const executeCommandSpy = vi @@ -74,50 +64,34 @@ describe('PNPM Proxy', () => { }); describe('removeDependencies', () => { - it('with devDep it should run `npm uninstall storybook`', async () => { + it('should only change package.json without running install', async () => { const executeCommandSpy = vi .spyOn(pnpmProxy, 'executeCommand') - .mockResolvedValueOnce('6.0.0'); - - await pnpmProxy.removeDependencies({}, ['storybook']); + .mockResolvedValueOnce('7.0.0'); + const writePackageSpy = vi.spyOn(pnpmProxy, 'writePackageJson').mockImplementation(vi.fn()); - expect(executeCommandSpy).toHaveBeenLastCalledWith( - expect.objectContaining({ - command: 'pnpm', - args: ['remove', 'storybook'], - }) - ); - }); - - describe('skipInstall', () => { - it('should only change package.json without running install', async () => { - const executeCommandSpy = vi - .spyOn(pnpmProxy, 'executeCommand') - .mockResolvedValueOnce('7.0.0'); - const writePackageSpy = vi - .spyOn(pnpmProxy, 'writePackageJson') - .mockImplementation(vi.fn()); - - await pnpmProxy.removeDependencies( - { - skipInstall: true, - packageJson: { - devDependencies: { - '@storybook/manager-webpack5': 'x.x.x', - '@storybook/react': 'x.x.x', - }, - }, + vi.spyOn(JsPackageManager, 'getPackageJson').mockImplementation((args) => { + return { + dependencies: {}, + devDependencies: { + '@storybook/manager-webpack5': 'x.x.x', + '@storybook/react': 'x.x.x', }, - ['@storybook/manager-webpack5'] - ); + }; + }); + + await pnpmProxy.removeDependencies(['@storybook/manager-webpack5']); - expect(writePackageSpy).toHaveBeenCalledWith({ + expect(writePackageSpy).toHaveBeenCalledWith( + { + dependencies: {}, devDependencies: { '@storybook/react': 'x.x.x', }, - }); - expect(executeCommandSpy).not.toHaveBeenCalled(); - }); + }, + expect.any(String) + ); + expect(executeCommandSpy).not.toHaveBeenCalled(); }); }); @@ -199,36 +173,36 @@ describe('PNPM Proxy', () => { describe('addPackageResolutions', () => { it('adds resolutions to package.json and account for existing resolutions', async () => { - const writePackageSpy = vi - .spyOn(pnpmProxy, 'writePackageJson') - .mockImplementation(vi.fn()); - const basePackageAttributes = { dependencies: {}, devDependencies: {}, }; - vi.spyOn(pnpmProxy, 'retrievePackageJson').mockImplementation( - vi.fn(async () => ({ - ...basePackageAttributes, - overrides: { - bar: 'x.x.x', - }, - })) - ); + const writePackageSpy = vi.spyOn(pnpmProxy, 'writePackageJson').mockImplementation(vi.fn()); + + vi.spyOn(JsPackageManager, 'getPackageJson').mockImplementation(() => ({ + dependencies: {}, + devDependencies: {}, + overrides: { + bar: 'x.x.x', + }, + })); const versions = { foo: 'x.x.x', }; - await pnpmProxy.addPackageResolutions(versions); + pnpmProxy.addPackageResolutions(versions); - expect(writePackageSpy).toHaveBeenCalledWith({ - ...basePackageAttributes, - overrides: { - ...versions, - bar: 'x.x.x', + expect(writePackageSpy).toHaveBeenCalledWith( + { + ...basePackageAttributes, + overrides: { + ...versions, + bar: 'x.x.x', + }, }, - }); + expect.any(String) + ); }); }); diff --git a/code/core/src/common/js-package-manager/PNPMProxy.ts b/code/core/src/common/js-package-manager/PNPMProxy.ts index 04c6b7a3b166..8f1b1a67728d 100644 --- a/code/core/src/common/js-package-manager/PNPMProxy.ts +++ b/code/core/src/common/js-package-manager/PNPMProxy.ts @@ -7,6 +7,7 @@ import { findUpSync } from 'find-up'; import { dedent } from 'ts-dedent'; import { createLogStream } from '../utils/cli'; +import { getProjectRoot } from '../utils/paths'; import { JsPackageManager } from './JsPackageManager'; import type { PackageJson } from './PackageJson'; import type { InstallationMetadata, PackageMetadata } from './types'; @@ -44,23 +45,12 @@ export class PNPMProxy extends JsPackageManager { return existsSync(pnpmWorkspaceYaml); } - async initPackageJson() { - await this.executeCommand({ - command: 'pnpm', - args: ['init'], - }); - } - - getRunStorybookCommand(): string { - return 'pnpm run storybook'; - } - getRunCommand(command: string): string { return `pnpm run ${command}`; } - getRemoteRunCommand(): string { - return 'pnpm dlx'; + getRemoteRunCommand(pkg: string, args: string[], specifier?: string): string { + return `pnpm dlx ${pkg}${specifier ? `@${specifier}` : ''} ${args.join(' ')}`; } async getPnpmVersion(): Promise { @@ -129,17 +119,17 @@ export class PNPMProxy extends JsPackageManager { } } - public async getPackageJSON( - packageName: string, - basePath = this.cwd - ): Promise { - const pnpapiPath = findUpSync(['.pnp.js', '.pnp.cjs'], { cwd: basePath }); + public getModulePackageJSON(packageName: string): PackageJson | null { + const pnpapiPath = findUpSync(['.pnp.js', '.pnp.cjs'], { + cwd: this.cwd, + stopAt: getProjectRoot(), + }); if (pnpapiPath) { try { const pnpApi = require(pnpapiPath); - const resolvedPath = await pnpApi.resolveToUnqualified(packageName, basePath, { + const resolvedPath = pnpApi.resolveToUnqualified(packageName, this.cwd, { considerBuiltins: false, }); @@ -159,12 +149,12 @@ export class PNPMProxy extends JsPackageManager { } } - const packageJsonPath = await findUpSync( + const packageJsonPath = findUpSync( (dir) => { const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); return existsSync(possiblePath) ? possiblePath : undefined; }, - { cwd: basePath } + { cwd: this.cwd, stopAt: getProjectRoot() } ); if (!packageJsonPath) { @@ -188,6 +178,7 @@ export class PNPMProxy extends JsPackageManager { command: 'pnpm', args: ['install', ...this.getInstallArgs()], stdio: 'inherit', + cwd: this.cwd, }); } @@ -201,13 +192,17 @@ export class PNPMProxy extends JsPackageManager { if (installAsDevDependencies) { args = ['-D', ...args]; } + + const commandArgs = ['add', ...args, ...this.getInstallArgs()]; + const { logStream, readLogFile, moveLogFile, removeLogFile } = await createLogStream(); try { await this.executeCommand({ command: 'pnpm', - args: ['add', ...args, ...this.getInstallArgs()], + args: commandArgs, stdio: process.env.CI || !writeOutputToFile ? 'inherit' : ['ignore', logStream, logStream], + cwd: this.primaryPackageJson.operationDir, }); } catch (err) { if (!writeOutputToFile) { @@ -226,13 +221,12 @@ export class PNPMProxy extends JsPackageManager { await removeLogFile(); } - protected async runRemoveDeps(dependencies: string[]) { - const args = [...dependencies]; - + protected async runRemoveDeps(dependencies: string[], cwd = this.cwd) { await this.executeCommand({ command: 'pnpm', - args: ['remove', ...args, ...this.getInstallArgs()], + args: ['remove', ...dependencies, ...this.getInstallArgs()], stdio: 'inherit', + cwd, }); } diff --git a/code/core/src/common/js-package-manager/Yarn1Proxy.test.ts b/code/core/src/common/js-package-manager/Yarn1Proxy.test.ts index 10fbee065ada..cf3da9c224db 100644 --- a/code/core/src/common/js-package-manager/Yarn1Proxy.test.ts +++ b/code/core/src/common/js-package-manager/Yarn1Proxy.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { dedent } from 'ts-dedent'; +import { JsPackageManager } from './JsPackageManager'; import { Yarn1Proxy } from './Yarn1Proxy'; describe('Yarn 1 Proxy', () => { @@ -9,24 +10,13 @@ describe('Yarn 1 Proxy', () => { beforeEach(() => { yarn1Proxy = new Yarn1Proxy(); + vi.spyOn(yarn1Proxy, 'writePackageJson').mockImplementation(vi.fn()); }); it('type should be yarn1', () => { expect(yarn1Proxy.type).toEqual('yarn1'); }); - describe('initPackageJson', () => { - it('should run `yarn init -y`', async () => { - const executeCommandSpy = vi.spyOn(yarn1Proxy, 'executeCommand').mockResolvedValueOnce(''); - - await yarn1Proxy.initPackageJson(); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ command: 'yarn', args: ['init', '-y'] }) - ); - }); - }); - describe('installDependencies', () => { it('should run `yarn`', async () => { const executeCommandSpy = vi.spyOn(yarn1Proxy, 'executeCommand').mockResolvedValueOnce(''); @@ -75,45 +65,33 @@ describe('Yarn 1 Proxy', () => { }); describe('removeDependencies', () => { - it('should run `yarn remove --ignore-workspace-root-check storybook`', async () => { - const executeCommandSpy = vi.spyOn(yarn1Proxy, 'executeCommand').mockResolvedValueOnce(''); - - yarn1Proxy.removeDependencies({}, ['storybook']); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'yarn', - args: ['remove', '--ignore-workspace-root-check', 'storybook'], - }) - ); - }); - it('skipInstall should only change package.json without running install', async () => { const executeCommandSpy = vi .spyOn(yarn1Proxy, 'executeCommand') .mockResolvedValueOnce('7.0.0'); - const writePackageSpy = vi - .spyOn(yarn1Proxy, 'writePackageJson') - .mockImplementation(vi.fn()); + const writePackageSpy = vi.spyOn(yarn1Proxy, 'writePackageJson').mockImplementation(vi.fn()); + + vi.spyOn(JsPackageManager, 'getPackageJson').mockImplementation((args) => { + return { + dependencies: {}, + devDependencies: { + '@storybook/manager-webpack5': 'x.x.x', + '@storybook/react': 'x.x.x', + }, + }; + }); - await yarn1Proxy.removeDependencies( + await yarn1Proxy.removeDependencies(['@storybook/manager-webpack5']); + + expect(writePackageSpy).toHaveBeenCalledWith( { - skipInstall: true, - packageJson: { - devDependencies: { - '@storybook/manager-webpack5': 'x.x.x', - '@storybook/react': 'x.x.x', - }, + dependencies: {}, + devDependencies: { + '@storybook/react': 'x.x.x', }, }, - ['@storybook/manager-webpack5'] + expect.any(String) ); - - expect(writePackageSpy).toHaveBeenCalledWith({ - devDependencies: { - '@storybook/react': 'x.x.x', - }, - }); expect(executeCommandSpy).not.toHaveBeenCalled(); }); }); @@ -160,33 +138,32 @@ describe('Yarn 1 Proxy', () => { describe('addPackageResolutions', () => { it('adds resolutions to package.json and account for existing resolutions', async () => { - const writePackageSpy = vi - .spyOn(yarn1Proxy, 'writePackageJson') - .mockImplementation(vi.fn()); + const writePackageSpy = vi.spyOn(yarn1Proxy, 'writePackageJson').mockImplementation(vi.fn()); - vi.spyOn(yarn1Proxy, 'retrievePackageJson').mockImplementation( - vi.fn(async () => ({ - dependencies: {}, - devDependencies: {}, - resolutions: { - bar: 'x.x.x', - }, - })) - ); + vi.spyOn(JsPackageManager, 'getPackageJson').mockImplementation(() => ({ + dependencies: {}, + devDependencies: {}, + resolutions: { + bar: 'x.x.x', + }, + })); const versions = { foo: 'x.x.x', }; - await yarn1Proxy.addPackageResolutions(versions); + yarn1Proxy.addPackageResolutions(versions); - expect(writePackageSpy).toHaveBeenCalledWith({ - dependencies: {}, - devDependencies: {}, - resolutions: { - ...versions, - bar: 'x.x.x', + expect(writePackageSpy).toHaveBeenCalledWith( + { + dependencies: {}, + devDependencies: {}, + resolutions: { + ...versions, + bar: 'x.x.x', + }, }, - }); + expect.any(String) + ); }); }); diff --git a/code/core/src/common/js-package-manager/Yarn1Proxy.ts b/code/core/src/common/js-package-manager/Yarn1Proxy.ts index f0b9768fc102..8571502c0382 100644 --- a/code/core/src/common/js-package-manager/Yarn1Proxy.ts +++ b/code/core/src/common/js-package-manager/Yarn1Proxy.ts @@ -3,10 +3,11 @@ import { join } from 'node:path'; import { FindPackageVersionsError } from 'storybook/internal/server-errors'; -import { findUp } from 'find-up'; +import { findUpSync } from 'find-up'; import { dedent } from 'ts-dedent'; import { createLogStream } from '../utils/cli'; +import { getProjectRoot } from '../utils/paths'; import { JsPackageManager } from './JsPackageManager'; import type { PackageJson } from './PackageJson'; import type { InstallationMetadata, PackageMetadata } from './types'; @@ -41,20 +42,12 @@ export class Yarn1Proxy extends JsPackageManager { return this.installArgs; } - async initPackageJson() { - await this.executeCommand({ command: 'yarn', args: ['init', '-y'] }); - } - - getRunStorybookCommand(): string { - return 'yarn storybook'; - } - getRunCommand(command: string): string { return `yarn ${command}`; } - getRemoteRunCommand(): string { - return 'npx'; + getRemoteRunCommand(pkg: string, args: string[], specifier?: string): string { + return `npx ${pkg}${specifier ? `@${specifier}` : ''} ${args.join(' ')}`; } public runPackageCommandSync( @@ -70,16 +63,13 @@ export class Yarn1Proxy extends JsPackageManager { return this.executeCommand({ command: `yarn`, args: ['exec', command, ...args], cwd }); } - public async getPackageJSON( - packageName: string, - basePath = this.cwd - ): Promise { - const packageJsonPath = await findUp( + public getModulePackageJSON(packageName: string): PackageJson | null { + const packageJsonPath = findUpSync( (dir) => { const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); return existsSync(possiblePath) ? possiblePath : undefined; }, - { cwd: basePath } + { cwd: this.cwd, stopAt: getProjectRoot() } ); if (!packageJsonPath) { @@ -135,6 +125,7 @@ export class Yarn1Proxy extends JsPackageManager { command: 'yarn', args: ['install', ...this.getInstallArgs()], stdio: 'inherit', + cwd: this.cwd, }); } @@ -156,6 +147,7 @@ export class Yarn1Proxy extends JsPackageManager { command: 'yarn', args: ['add', ...this.getInstallArgs(), ...args], stdio: process.env.CI || !writeOutputToFile ? 'inherit' : ['ignore', logStream, logStream], + cwd: this.primaryPackageJson.operationDir, }); } catch (err) { if (!writeOutputToFile) { @@ -174,13 +166,14 @@ export class Yarn1Proxy extends JsPackageManager { await removeLogFile(); } - protected async runRemoveDeps(dependencies: string[]) { + protected async runRemoveDeps(dependencies: string[], cwd = this.cwd) { const args = [...dependencies]; await this.executeCommand({ command: 'yarn', args: ['remove', ...this.getInstallArgs(), ...args], stdio: 'inherit', + cwd, }); } diff --git a/code/core/src/common/js-package-manager/Yarn2Proxy.test.ts b/code/core/src/common/js-package-manager/Yarn2Proxy.test.ts index e519a79bd02b..92baed2a03b2 100644 --- a/code/core/src/common/js-package-manager/Yarn2Proxy.test.ts +++ b/code/core/src/common/js-package-manager/Yarn2Proxy.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { JsPackageManager } from './JsPackageManager'; import { Yarn2Proxy } from './Yarn2Proxy'; describe('Yarn 2 Proxy', () => { @@ -7,24 +8,13 @@ describe('Yarn 2 Proxy', () => { beforeEach(() => { yarn2Proxy = new Yarn2Proxy(); + vi.spyOn(yarn2Proxy, 'writePackageJson').mockImplementation(vi.fn()); }); it('type should be yarn2', () => { expect(yarn2Proxy.type).toEqual('yarn2'); }); - describe('initPackageJson', () => { - it('should run `yarn init`', async () => { - const executeCommandSpy = vi.spyOn(yarn2Proxy, 'executeCommand').mockResolvedValueOnce(''); - - await yarn2Proxy.initPackageJson(); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ command: 'yarn', args: ['init'] }) - ); - }); - }); - describe('installDependencies', () => { it('should run `yarn`', async () => { const executeCommandSpy = vi.spyOn(yarn2Proxy, 'executeCommand').mockResolvedValueOnce(''); @@ -70,45 +60,33 @@ describe('Yarn 2 Proxy', () => { }); describe('removeDependencies', () => { - it('should run `yarn remove storybook`', async () => { - const executeCommandSpy = vi.spyOn(yarn2Proxy, 'executeCommand').mockResolvedValueOnce(''); - - await yarn2Proxy.removeDependencies({}, ['storybook']); - - expect(executeCommandSpy).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'yarn', - args: ['remove', 'storybook'], - }) - ); - }); - it('skipInstall should only change package.json without running install', async () => { const executeCommandSpy = vi .spyOn(yarn2Proxy, 'executeCommand') .mockResolvedValueOnce('7.0.0'); - const writePackageSpy = vi - .spyOn(yarn2Proxy, 'writePackageJson') - .mockImplementation(vi.fn()); + const writePackageSpy = vi.spyOn(yarn2Proxy, 'writePackageJson').mockImplementation(vi.fn()); + + vi.spyOn(JsPackageManager, 'getPackageJson').mockImplementation(() => { + return { + dependencies: {}, + devDependencies: { + '@storybook/manager-webpack5': 'x.x.x', + '@storybook/react': 'x.x.x', + }, + }; + }); - await yarn2Proxy.removeDependencies( + await yarn2Proxy.removeDependencies(['@storybook/manager-webpack5']); + + expect(writePackageSpy).toHaveBeenCalledWith( { - skipInstall: true, - packageJson: { - devDependencies: { - '@storybook/manager-webpack5': 'x.x.x', - '@storybook/react': 'x.x.x', - }, + dependencies: {}, + devDependencies: { + '@storybook/react': 'x.x.x', }, }, - ['@storybook/manager-webpack5'] + expect.any(String) ); - - expect(writePackageSpy).toHaveBeenCalledWith({ - devDependencies: { - '@storybook/react': 'x.x.x', - }, - }); expect(executeCommandSpy).not.toHaveBeenCalled(); }); }); @@ -157,34 +135,33 @@ describe('Yarn 2 Proxy', () => { describe('addPackageResolutions', () => { it('adds resolutions to package.json and account for existing resolutions', async () => { - const writePackageSpy = vi - .spyOn(yarn2Proxy, 'writePackageJson') - .mockImplementation(vi.fn()); + const writePackageSpy = vi.spyOn(yarn2Proxy, 'writePackageJson').mockImplementation(vi.fn()); - vi.spyOn(yarn2Proxy, 'retrievePackageJson').mockImplementation( - vi.fn(async () => ({ - dependencies: {}, - devDependencies: {}, - resolutions: { - bar: 'x.x.x', - }, - })) - ); + vi.spyOn(JsPackageManager, 'getPackageJson').mockImplementation(() => ({ + dependencies: {}, + devDependencies: {}, + resolutions: { + bar: 'x.x.x', + }, + })); const versions = { foo: 'x.x.x', }; - await yarn2Proxy.addPackageResolutions(versions); + yarn2Proxy.addPackageResolutions(versions); - expect(writePackageSpy).toHaveBeenCalledWith({ - dependencies: {}, - devDependencies: {}, - resolutions: { - ...versions, - bar: 'x.x.x', + expect(writePackageSpy).toHaveBeenCalledWith( + { + dependencies: {}, + devDependencies: {}, + resolutions: { + ...versions, + bar: 'x.x.x', + }, }, - }); + expect.any(String) + ); }); }); diff --git a/code/core/src/common/js-package-manager/Yarn2Proxy.ts b/code/core/src/common/js-package-manager/Yarn2Proxy.ts index 6021350eec90..3f9086844794 100644 --- a/code/core/src/common/js-package-manager/Yarn2Proxy.ts +++ b/code/core/src/common/js-package-manager/Yarn2Proxy.ts @@ -5,10 +5,11 @@ import { FindPackageVersionsError } from 'storybook/internal/server-errors'; import { PosixFS, VirtualFS, ZipOpenFS } from '@yarnpkg/fslib'; import { getLibzipSync } from '@yarnpkg/libzip'; -import { findUp, findUpSync } from 'find-up'; +import { findUpSync } from 'find-up'; import { dedent } from 'ts-dedent'; import { createLogStream } from '../utils/cli'; +import { getProjectRoot } from '../utils/paths'; import { JsPackageManager } from './JsPackageManager'; import type { PackageJson } from './PackageJson'; import type { InstallationMetadata, PackageMetadata } from './types'; @@ -84,20 +85,12 @@ export class Yarn2Proxy extends JsPackageManager { return this.installArgs; } - async initPackageJson() { - await this.executeCommand({ command: 'yarn', args: ['init'] }); - } - - getRunStorybookCommand(): string { - return 'yarn storybook'; - } - getRunCommand(command: string): string { return `yarn ${command}`; } - getRemoteRunCommand(): string { - return 'yarn dlx'; + getRemoteRunCommand(pkg: string, args: string[], specifier?: string): string { + return `yarn dlx ${pkg}${specifier ? `@${specifier}` : ''} ${args.join(' ')}`; } public runPackageCommandSync( @@ -135,14 +128,17 @@ export class Yarn2Proxy extends JsPackageManager { } } - async getPackageJSON(packageName: string, basePath = this.cwd): Promise { - const pnpapiPath = findUpSync(['.pnp.js', '.pnp.cjs'], { cwd: basePath }); + getModulePackageJSON(packageName: string): PackageJson | null { + const pnpapiPath = findUpSync(['.pnp.js', '.pnp.cjs'], { + cwd: this.cwd, + stopAt: getProjectRoot(), + }); if (pnpapiPath) { try { const pnpApi = require(pnpapiPath); - const resolvedPath = await pnpApi.resolveToUnqualified(packageName, basePath, { + const resolvedPath = pnpApi.resolveToUnqualified(packageName, this.cwd, { considerBuiltins: false, }); @@ -167,12 +163,12 @@ export class Yarn2Proxy extends JsPackageManager { } } - const packageJsonPath = await findUp( + const packageJsonPath = findUpSync( (dir) => { const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); return existsSync(possiblePath) ? possiblePath : undefined; }, - { cwd: basePath } + { cwd: this.cwd, stopAt: getProjectRoot() } ); if (!packageJsonPath) { @@ -197,6 +193,7 @@ export class Yarn2Proxy extends JsPackageManager { command: 'yarn', args: ['install', ...this.getInstallArgs()], stdio: 'inherit', + cwd: this.cwd, }); } @@ -218,6 +215,7 @@ export class Yarn2Proxy extends JsPackageManager { command: 'yarn', args: ['add', ...this.getInstallArgs(), ...args], stdio: process.env.CI || !writeOutputToFile ? 'inherit' : ['ignore', logStream, logStream], + cwd: this.primaryPackageJson.operationDir, }); } catch (err) { if (!writeOutputToFile) { @@ -245,13 +243,14 @@ export class Yarn2Proxy extends JsPackageManager { return url === 'undefined' ? undefined : url; } - protected async runRemoveDeps(dependencies: string[]) { + protected async runRemoveDeps(dependencies: string[], cwd = this.cwd) { const args = [...dependencies]; await this.executeCommand({ command: 'yarn', args: ['remove', ...this.getInstallArgs(), ...args], stdio: 'inherit', + cwd, }); } diff --git a/code/core/src/common/js-package-manager/constants.ts b/code/core/src/common/js-package-manager/constants.ts new file mode 100644 index 000000000000..1b1dad4ab9cb --- /dev/null +++ b/code/core/src/common/js-package-manager/constants.ts @@ -0,0 +1,13 @@ +export const NPM_LOCKFILE = 'package-lock.json'; +export const PNPM_LOCKFILE = 'pnpm-lock.yaml'; +export const YARN_LOCKFILE = 'yarn.lock'; +export const BUN_LOCKFILE = 'bun.lock'; +export const BUN_LOCKFILE_BINARY = 'bun.lockb'; + +export const LOCK_FILES = [ + NPM_LOCKFILE, + PNPM_LOCKFILE, + YARN_LOCKFILE, + BUN_LOCKFILE, + BUN_LOCKFILE_BINARY, +]; diff --git a/code/core/src/common/utils/cli.ts b/code/core/src/common/utils/cli.ts index 972056a4b288..260ebdad1ce9 100644 --- a/code/core/src/common/utils/cli.ts +++ b/code/core/src/common/utils/cli.ts @@ -78,7 +78,7 @@ export async function getCoercedStorybookVersion(packageManager: JsPackageManage await Promise.all( Object.keys(rendererPackages).map(async (pkg) => ({ name: pkg, - version: await packageManager.getPackageVersion(pkg), + version: packageManager.getModulePackageJSON(pkg)?.version ?? null, })) ) ).filter(({ version }) => !!version); diff --git a/code/core/src/common/utils/get-storybook-info.ts b/code/core/src/common/utils/get-storybook-info.ts index 71370126fe47..1d2e43491faa 100644 --- a/code/core/src/common/utils/get-storybook-info.ts +++ b/code/core/src/common/utils/get-storybook-info.ts @@ -1,9 +1,10 @@ -import { existsSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import type { SupportedFrameworks } from 'storybook/internal/types'; import type { CoreCommon_StorybookInfo, PackageJson } from 'storybook/internal/types'; +import { JsPackageManager } from '../js-package-manager/JsPackageManager'; import { getStorybookConfiguration } from './get-storybook-configuration'; export const rendererPackages: Record = { @@ -50,8 +51,6 @@ export const frameworkPackages: Record = { export const builderPackages = ['@storybook/builder-webpack5', '@storybook/builder-vite']; -const logger = console; - const findDependency = ( { dependencies, devDependencies, peerDependencies }: PackageJson, predicate: (entry: [string, string | undefined]) => string @@ -62,25 +61,26 @@ const findDependency = ( Object.entries(peerDependencies || {}).find(predicate), ] as const; -const getRendererInfo = (packageJson: PackageJson) => { - // Pull the viewlayer from dependencies in package.json - const [dep, devDep, peerDep] = findDependency(packageJson, ([key]) => rendererPackages[key]); - const [pkg, version] = dep || devDep || peerDep || []; +const getRendererInfo = (configDir: string) => { + const packageJsonPaths = JsPackageManager.listAllPackageJsonPaths(configDir); - if (dep && devDep && dep[0] === devDep[0]) { - logger.warn( - `Found "${dep[0]}" in both "dependencies" and "devDependencies". This is probably a mistake.` - ); - } - if (dep && peerDep && dep[0] === peerDep[0]) { - logger.warn( - `Found "${dep[0]}" in both "dependencies" and "peerDependencies". This is probably a mistake.` - ); + for (const packageJsonPath of packageJsonPaths) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + // Pull the viewlayer from dependencies in package.json + const [dep, devDep, peerDep] = findDependency(packageJson, ([key]) => rendererPackages[key]); + const [pkg, version] = dep || devDep || peerDep || []; + + if (pkg && version) { + return { + version, + frameworkPackage: pkg, + }; + } } return { - version, - frameworkPackage: pkg, + version: undefined, + frameworkPackage: undefined, }; }; @@ -92,28 +92,37 @@ export const findConfigFile = (prefix: string, configDir: string) => { return extension ? `${filePrefix}.${extension}` : null; }; -export const getConfigInfo = (packageJson: PackageJson, configDir?: string) => { +export const getConfigInfo = (configDir?: string) => { let storybookConfigDir = configDir ?? '.storybook'; - const storybookScript = packageJson.scripts?.storybook; - if (storybookScript && !configDir) { - const configParam = getStorybookConfiguration(storybookScript, '-c', '--config-dir'); - if (configParam) { - storybookConfigDir = configParam; + if (!existsSync(storybookConfigDir)) { + const packageJsonPaths = JsPackageManager.listAllPackageJsonPaths(storybookConfigDir); + + for (const packageJsonPath of packageJsonPaths) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const storybookScript = packageJson.scripts?.storybook; + if (storybookScript && !configDir) { + const configParam = getStorybookConfiguration(storybookScript, '-c', '--config-dir'); + + if (configParam) { + storybookConfigDir = configParam; + break; + } + } } } return { configDir: storybookConfigDir, - mainConfig: findConfigFile('main', storybookConfigDir), - previewConfig: findConfigFile('preview', storybookConfigDir), - managerConfig: findConfigFile('manager', storybookConfigDir), + mainConfigPath: findConfigFile('main', storybookConfigDir), + previewConfigPath: findConfigFile('preview', storybookConfigDir), + managerConfigPath: findConfigFile('manager', storybookConfigDir), }; }; -export const getStorybookInfo = (packageJson: PackageJson, configDir?: string) => { - const rendererInfo = getRendererInfo(packageJson); - const configInfo = getConfigInfo(packageJson, configDir); +export const getStorybookInfo = (configDir = '.storybook') => { + const rendererInfo = getRendererInfo(configDir); + const configInfo = getConfigInfo(configDir); return { ...rendererInfo, diff --git a/code/core/src/common/utils/get-storybook-refs.ts b/code/core/src/common/utils/get-storybook-refs.ts index 0b615b60a572..2b7033239119 100644 --- a/code/core/src/common/utils/get-storybook-refs.ts +++ b/code/core/src/common/utils/get-storybook-refs.ts @@ -7,8 +7,13 @@ import type { Options, Ref } from 'storybook/internal/types'; import { findUp } from 'find-up'; import resolveFrom from 'resolve-from'; +import { getProjectRoot } from './paths'; + export const getAutoRefs = async (options: Options): Promise> => { - const location = await findUp('package.json', { cwd: options.configDir }); + const location = await findUp('package.json', { + cwd: options.configDir, + stopAt: getProjectRoot(), + }); if (!location) { return {}; } diff --git a/code/core/src/common/utils/paths.ts b/code/core/src/common/utils/paths.ts index 1117c5cffb71..95657805df84 100644 --- a/code/core/src/common/utils/paths.ts +++ b/code/core/src/common/utils/paths.ts @@ -1,8 +1,16 @@ -import { join, resolve, sep } from 'node:path'; +import { join, relative, resolve, sep } from 'node:path'; import { findUpSync } from 'find-up'; +import { LOCK_FILES } from '../js-package-manager/constants'; + +let projectRoot: string | undefined; + export const getProjectRoot = () => { + if (projectRoot) { + return projectRoot; + } + let result; // Allow manual override in cases where auto-detect doesn't work if (process.env.STORYBOOK_PROJECT_ROOT) { @@ -17,6 +25,7 @@ export const getProjectRoot = () => { } catch (e) { // } + try { const found = findUpSync('.svn', { type: 'directory' }); if (found) { @@ -25,6 +34,7 @@ export const getProjectRoot = () => { } catch (e) { // } + try { const found = findUpSync('.hg', { type: 'directory' }); if (found) { @@ -36,13 +46,22 @@ export const getProjectRoot = () => { try { const splitDirname = __dirname.split('node_modules'); - result = result || (splitDirname.length >= 2 ? splitDirname[0] : undefined); + const isSplitDirnameReachable = !relative(splitDirname[0], process.cwd()).startsWith('..'); + result = + result || + (isSplitDirnameReachable + ? splitDirname.length >= 2 + ? splitDirname[0] + : undefined + : undefined); } catch (e) { // } try { - const found = findUpSync('.yarn', { type: 'directory' }); + const found = findUpSync(LOCK_FILES, { + type: 'file', + }); if (found) { result = result || join(found, '..'); } @@ -50,7 +69,13 @@ export const getProjectRoot = () => { // } - return result || process.cwd(); + projectRoot = result || process.cwd(); + + return projectRoot; +}; + +export const invalidateProjectRootCache = () => { + projectRoot = undefined; }; export const nodePathsToArray = (nodePath: string) => @@ -60,6 +85,7 @@ export const nodePathsToArray = (nodePath: string) => .map((p) => resolve('./', p)); const relativePattern = /^\.{1,2}([/\\]|$)/; + /** Ensures that a path starts with `./` or `../`, or is entirely `.` or `..` */ export function normalizeStoryPath(filename: string) { if (relativePattern.test(filename)) { diff --git a/code/core/src/common/utils/remove.ts b/code/core/src/common/utils/remove.ts index 91dda44dc357..471835a819cd 100644 --- a/code/core/src/common/utils/remove.ts +++ b/code/core/src/common/utils/remove.ts @@ -4,7 +4,7 @@ import { dedent } from 'ts-dedent'; import type { PackageManagerName } from '../js-package-manager'; import { JsPackageManagerFactory } from '../js-package-manager'; -import { getStorybookInfo } from './get-storybook-info'; +import { getConfigInfo } from './get-storybook-info'; const logger = console; @@ -19,13 +19,18 @@ const logger = console; */ export async function removeAddon( addon: string, - options: { packageManager?: PackageManagerName; cwd?: string; configDir?: string } = {} + options: { + packageManager?: PackageManagerName; + cwd?: string; + configDir?: string; + skipInstall?: boolean; + } = {} ) { - const { packageManager: pkgMgr } = options; + const { packageManager: pkgMgr, skipInstall } = options; const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }, options.cwd); - const packageJson = await packageManager.retrievePackageJson(); - const { mainConfig, configDir, ...rest } = getStorybookInfo(packageJson, options.configDir); + + const { mainConfigPath, configDir } = getConfigInfo(options.configDir); if (typeof configDir === 'undefined') { throw new Error(dedent` @@ -33,15 +38,19 @@ export async function removeAddon( `); } - if (!mainConfig) { + if (!mainConfigPath) { logger.error('Unable to find storybook main.js config'); return; } - const main = await readConfig(mainConfig); + const main = await readConfig(mainConfigPath); // remove from package.json logger.log(`Uninstalling ${addon}`); - await packageManager.removeDependencies({ packageJson }, [addon]); + await packageManager.removeDependencies([addon]); + + if (!skipInstall) { + await packageManager.installDependencies(); + } // add to main.js logger.log(`Removing '${addon}' from main.js addons field.`); diff --git a/code/core/src/common/utils/scan-and-transform-files.test.ts b/code/core/src/common/utils/scan-and-transform-files.test.ts index 8d3500b3a503..c5366ff54a6b 100644 --- a/code/core/src/common/utils/scan-and-transform-files.test.ts +++ b/code/core/src/common/utils/scan-and-transform-files.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { getProjectRoot } from './paths'; +import * as paths from './paths'; import { scanAndTransformFiles } from './scan-and-transform-files'; // Mock dependencies @@ -17,10 +17,6 @@ vi.mock('prompts', () => { }; }); -vi.mock('./paths', () => ({ - getProjectRoot: vi.fn(), -})); - vi.mock('./common-glob-options', () => ({ commonGlobOptions: mocks.commonGlobOptions, })); @@ -37,11 +33,10 @@ describe('scanAndTransformFiles', () => { vi.resetAllMocks(); // Import the mocked modules - const mockedGetProjectRoot = vi.mocked(getProjectRoot); + vi.spyOn(paths, 'getProjectRoot').mockReturnValue('/mock/project/root'); // Setup mock implementations mocks.prompts.mockResolvedValue({ glob: '**/*.{js,ts}' }); - mockedGetProjectRoot.mockReturnValue('/mock/project/root'); // Setup globby mock vi.doMock('globby', async () => { diff --git a/code/core/src/common/utils/scan-and-transform-files.ts b/code/core/src/common/utils/scan-and-transform-files.ts index 7615dcc7589f..88313f1f7020 100644 --- a/code/core/src/common/utils/scan-and-transform-files.ts +++ b/code/core/src/common/utils/scan-and-transform-files.ts @@ -39,8 +39,6 @@ export async function scanAndTransformFiles>({ initial: defaultGlob, }); - const projectRoot = getProjectRoot(); - console.log('Scanning for affected files...'); // eslint-disable-next-line depend/ban-dependencies @@ -50,7 +48,7 @@ export async function scanAndTransformFiles>({ ...commonGlobOptions(''), ignore: ['**/node_modules/**'], dot: true, - cwd: projectRoot, + cwd: getProjectRoot(), absolute: true, }); diff --git a/code/core/src/core-server/build-dev.ts b/code/core/src/core-server/build-dev.ts index 049ff852c9b8..10aecc8032c6 100644 --- a/code/core/src/core-server/build-dev.ts +++ b/code/core/src/core-server/build-dev.ts @@ -50,7 +50,7 @@ export async function buildDevStandalone( `Expected package.json#version to be defined in the "${packageJson.name}" package}` ); storybookVersion = packageJson.version; - previewConfigPath = getConfigInfo(packageJson, configDir).previewConfig ?? undefined; + previewConfigPath = getConfigInfo(configDir).previewConfigPath ?? undefined; } else { if (!storybookVersion) { storybookVersion = versions.storybook; @@ -76,8 +76,7 @@ export async function buildDevStandalone( } } - const rootDir = getProjectRoot(); - const cacheKey = oneWayHash(relative(rootDir, configDir)); + const cacheKey = oneWayHash(relative(getProjectRoot(), configDir)); const cacheOutputDir = resolvePathInStorybookCache('public', cacheKey); let outputDir = resolve(options.outputDir || cacheOutputDir); diff --git a/code/core/src/core-server/load.ts b/code/core/src/core-server/load.ts index a9c4321abe51..b3f7e6287df7 100644 --- a/code/core/src/core-server/load.ts +++ b/code/core/src/core-server/load.ts @@ -22,8 +22,7 @@ export async function loadStorybook( ): Promise { const configDir = resolve(options.configDir); - const rootDir = getProjectRoot(); - const cacheKey = oneWayHash(relative(rootDir, configDir)); + const cacheKey = oneWayHash(relative(getProjectRoot(), configDir)); options.configType = 'DEVELOPMENT'; options.configDir = configDir; diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts index 2fef386df613..961e111c84a5 100644 --- a/code/core/src/core-server/presets/common-preset.ts +++ b/code/core/src/core-server/presets/common-preset.ts @@ -14,7 +14,6 @@ import { readCsf } from 'storybook/internal/csf-tools'; import { logger } from 'storybook/internal/node-logger'; import { telemetry } from 'storybook/internal/telemetry'; import type { - CLIOptions, CoreConfig, Indexer, Options, diff --git a/code/core/src/core-server/server-channel/create-new-story-channel.test.ts b/code/core/src/core-server/server-channel/create-new-story-channel.test.ts index 2e80450c99b0..5ecf047fefc5 100644 --- a/code/core/src/core-server/server-channel/create-new-story-channel.test.ts +++ b/code/core/src/core-server/server-channel/create-new-story-channel.test.ts @@ -16,7 +16,7 @@ vi.mock('storybook/internal/common', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - getProjectRoot: vi.fn().mockReturnValue(process.cwd()), + getProjectRoot: () => process.cwd(), }; }); diff --git a/code/core/src/core-server/server-channel/file-search-channel.test.ts b/code/core/src/core-server/server-channel/file-search-channel.test.ts index 1a8c24697bea..ac9f15c195a1 100644 --- a/code/core/src/core-server/server-channel/file-search-channel.test.ts +++ b/code/core/src/core-server/server-channel/file-search-channel.test.ts @@ -3,11 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Channel } from 'storybook/internal/channels'; import type { ChannelTransport } from 'storybook/internal/channels'; -import { - extractProperRendererNameFromFramework, - getFrameworkName, - getProjectRoot, -} from 'storybook/internal/common'; +import * as common from 'storybook/internal/common'; import type { FileComponentSearchRequestPayload, RequestData, @@ -28,9 +24,9 @@ vi.mock('storybook/internal/common'); beforeEach(() => { vi.restoreAllMocks(); - vi.mocked(getFrameworkName).mockResolvedValue('@storybook/react'); - vi.mocked(extractProperRendererNameFromFramework).mockResolvedValue('react'); - vi.mocked(getProjectRoot).mockReturnValue( + vi.mocked(common.getFrameworkName).mockResolvedValue('@storybook/react'); + vi.mocked(common.extractProperRendererNameFromFramework).mockResolvedValue('react'); + vi.spyOn(common, 'getProjectRoot').mockReturnValue( require('path').join(__dirname, '..', 'utils', '__search-files-tests__') ); }); diff --git a/code/core/src/core-server/server-channel/file-search-channel.ts b/code/core/src/core-server/server-channel/file-search-channel.ts index 5343dcb387e1..239c8e77b8fa 100644 --- a/code/core/src/core-server/server-channel/file-search-channel.ts +++ b/code/core/src/core-server/server-channel/file-search-channel.ts @@ -45,22 +45,20 @@ export async function initFileSearchChannel( frameworkName )) as SupportedRenderers; - const projectRoot = getProjectRoot(); - const files = await searchFiles({ searchQuery, - cwd: projectRoot, + cwd: getProjectRoot(), }); const entries = files.map(async (file) => { const parser = getParser(rendererName); try { - const content = await readFile(join(projectRoot, file), 'utf-8'); - const { storyFileName } = getStoryMetadata(join(projectRoot, file)); + const content = await readFile(join(getProjectRoot(), file), 'utf-8'); + const { storyFileName } = getStoryMetadata(join(getProjectRoot(), file)); const dir = dirname(file); - const storyFileExists = doesStoryFileExist(join(projectRoot, dir), storyFileName); + const storyFileExists = doesStoryFileExist(join(getProjectRoot(), dir), storyFileName); const info = await parser.parse(content); diff --git a/code/core/src/core-server/utils/get-new-story-file.test.ts b/code/core/src/core-server/utils/get-new-story-file.test.ts index 99ee6eb8aa76..c110b423e33e 100644 --- a/code/core/src/core-server/utils/get-new-story-file.test.ts +++ b/code/core/src/core-server/utils/get-new-story-file.test.ts @@ -8,7 +8,7 @@ vi.mock('storybook/internal/common', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - getProjectRoot: vi.fn().mockReturnValue(require('path').join(__dirname)), + getProjectRoot: () => require('path').join(__dirname), }; }); diff --git a/code/core/src/core-server/utils/get-new-story-file.ts b/code/core/src/core-server/utils/get-new-story-file.ts index 967761dfd344..ebcb4e9a8f6c 100644 --- a/code/core/src/core-server/utils/get-new-story-file.ts +++ b/code/core/src/core-server/utils/get-new-story-file.ts @@ -4,11 +4,9 @@ import { basename, dirname, extname, join } from 'node:path'; import { extractProperFrameworkName, - extractProperRendererNameFromFramework, findConfigFile, getFrameworkName, getProjectRoot, - rendererPackages, } from 'storybook/internal/common'; import type { CreateNewStoryRequestPayload } from 'storybook/internal/core-events'; import { isCsfFactoryPreview } from 'storybook/internal/csf-tools'; @@ -28,8 +26,6 @@ export async function getNewStoryFile( }: CreateNewStoryRequestPayload, options: Options ) { - const cwd = getProjectRoot(); - const frameworkPackageName = await getFrameworkName(options); const sanitizedFrameworkPackageName = extractProperFrameworkName(frameworkPackageName); @@ -82,9 +78,9 @@ export async function getNewStoryFile( } const storyFilePath = - doesStoryFileExist(join(cwd, dir), storyFileName) && componentExportCount > 1 - ? join(cwd, dir, alternativeStoryFileNameWithExtension) - : join(cwd, dir, storyFileNameWithExtension); + doesStoryFileExist(join(getProjectRoot(), dir), storyFileName) && componentExportCount > 1 + ? join(getProjectRoot(), dir, alternativeStoryFileNameWithExtension) + : join(getProjectRoot(), dir, storyFileNameWithExtension); return { storyFilePath, exportedStoryName, storyFileContent, dirname }; } diff --git a/code/core/src/telemetry/anonymous-id.ts b/code/core/src/telemetry/anonymous-id.ts index acd807ffd95b..8ed5b641f306 100644 --- a/code/core/src/telemetry/anonymous-id.ts +++ b/code/core/src/telemetry/anonymous-id.ts @@ -38,9 +38,7 @@ export const getAnonymousProjectId = () => { } try { - const projectRoot = getProjectRoot(); - - const projectRootPath = relative(projectRoot, process.cwd()); + const projectRootPath = relative(getProjectRoot(), process.cwd()); const originBuffer = execSync(`git config --local --get remote.origin.url`, { timeout: 1000, diff --git a/code/core/src/telemetry/get-monorepo-type.ts b/code/core/src/telemetry/get-monorepo-type.ts index d3825f0f8063..159f7d79f1a8 100644 --- a/code/core/src/telemetry/get-monorepo-type.ts +++ b/code/core/src/telemetry/get-monorepo-type.ts @@ -15,15 +15,9 @@ export const monorepoConfigs = { export type MonorepoType = keyof typeof monorepoConfigs | 'Workspaces' | undefined; export const getMonorepoType = (): MonorepoType => { - const projectRootPath = getProjectRoot(); - - if (!projectRootPath) { - return undefined; - } - const keys = Object.keys(monorepoConfigs) as (keyof typeof monorepoConfigs)[]; const monorepoType: MonorepoType = keys.find((monorepo) => { - const configFile = join(projectRootPath, monorepoConfigs[monorepo]); + const configFile = join(getProjectRoot(), monorepoConfigs[monorepo]); return existsSync(configFile); }) as MonorepoType; @@ -31,12 +25,12 @@ export const getMonorepoType = (): MonorepoType => { return monorepoType; } - if (!existsSync(join(projectRootPath, 'package.json'))) { + if (!existsSync(join(getProjectRoot(), 'package.json'))) { return undefined; } const packageJson = JSON.parse( - readFileSync(join(projectRootPath, 'package.json'), { encoding: 'utf8' }) + readFileSync(join(getProjectRoot(), 'package.json'), { encoding: 'utf8' }) ) as PackageJson; if (packageJson?.workspaces) { diff --git a/code/core/src/telemetry/storybook-metadata.test.ts b/code/core/src/telemetry/storybook-metadata.test.ts index d7b0ec4221a6..549daea82c60 100644 --- a/code/core/src/telemetry/storybook-metadata.test.ts +++ b/code/core/src/telemetry/storybook-metadata.test.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import type { MockInstance } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { getProjectRoot } from 'storybook/internal/common'; +import { getStorybookInfo } from 'storybook/internal/common'; import type { PackageJson, StorybookConfig } from 'storybook/internal/types'; import { detect } from 'package-manager-detector'; @@ -33,14 +33,20 @@ const mainJsMock: StorybookConfig = { }; beforeEach(() => { + vi.mocked(getStorybookInfo).mockImplementation(() => ({ + version: '9.0.0', + framework: 'react', + frameworkPackage: '@storybook/react', + renderer: 'react', + rendererPackage: '@storybook/react', + })); + vi.mocked(detect).mockImplementation(async () => ({ name: 'yarn', version: '3.1.1', agent: 'yarn@berry', })); - vi.mocked(getProjectRoot).mockImplementation(() => process.cwd()); - vi.mocked(getMonorepoType).mockImplementation(() => 'Nx'); vi.mocked(getActualPackageJson).mockImplementation(async () => ({ @@ -129,6 +135,7 @@ describe('storybook-metadata', () => { const unixResult = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, framework: { @@ -148,6 +155,7 @@ describe('storybook-metadata', () => { const windowsResult = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, framework: { @@ -169,6 +177,7 @@ describe('storybook-metadata', () => { const unixResult = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, framework: { @@ -184,6 +193,7 @@ describe('storybook-metadata', () => { const windowsResult = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, framework: { @@ -205,6 +215,7 @@ describe('storybook-metadata', () => { const unixResult = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, framework: { @@ -223,6 +234,7 @@ describe('storybook-metadata', () => { const windowsResult = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, framework: { @@ -241,6 +253,7 @@ describe('storybook-metadata', () => { const reactResult = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, framework: { @@ -260,6 +273,7 @@ describe('storybook-metadata', () => { const angularResult = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, framework: { @@ -289,6 +303,7 @@ describe('storybook-metadata', () => { 'storybook-addon-deprecated': 'x.x.z', }, } as PackageJson, + configDir: '.storybook', packageJsonPath, mainConfig: { ...mainJsMock, @@ -331,6 +346,7 @@ describe('storybook-metadata', () => { const result = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, features, @@ -345,6 +361,7 @@ describe('storybook-metadata', () => { await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, framework: '@storybook/react-vite', @@ -361,6 +378,7 @@ describe('storybook-metadata', () => { const res = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, refs: { @@ -376,6 +394,7 @@ describe('storybook-metadata', () => { const res = await computeStorybookMetadata({ packageJson: packageJsonMock, packageJsonPath, + configDir: '.storybook', mainConfig: { ...mainJsMock, addons: [ @@ -404,6 +423,7 @@ describe('storybook-metadata', () => { 'should detect the supported metaframework: %s', async (metaFramework, name) => { const res = await computeStorybookMetadata({ + configDir: '.storybook', packageJson: { ...packageJsonMock, dependencies: { @@ -423,6 +443,7 @@ describe('storybook-metadata', () => { it('should detect userSince info', async () => { const res = await computeStorybookMetadata({ + configDir: '.storybook', packageJson: packageJsonMock, packageJsonPath, mainConfig: mainJsMock, diff --git a/code/core/src/telemetry/storybook-metadata.ts b/code/core/src/telemetry/storybook-metadata.ts index a7a9102e9c7e..58354a3f770c 100644 --- a/code/core/src/telemetry/storybook-metadata.ts +++ b/code/core/src/telemetry/storybook-metadata.ts @@ -49,10 +49,12 @@ export const computeStorybookMetadata = async ({ packageJsonPath, packageJson, mainConfig, + configDir, }: { packageJsonPath: string; packageJson: PackageJson; mainConfig?: StorybookConfig & Record; + configDir: string; }): Promise => { const settings = await globalSettings(); const metadata: Partial = { @@ -212,10 +214,10 @@ export const computeStorybookMetadata = async ({ const hasStorybookEslint = !!allDependencies['eslint-plugin-storybook']; - const storybookInfo = getStorybookInfo(packageJson); + const storybookInfo = getStorybookInfo(configDir); try { - const { previewConfig } = storybookInfo; + const { previewConfigPath: previewConfig } = storybookInfo; if (previewConfig) { const config = await readConfig(previewConfig); const usesGlobals = !!( @@ -281,6 +283,11 @@ export const getStorybookMetadata = async (_configDir?: string) => { ) as string)) ?? '.storybook'; const mainConfig = await loadMainConfig({ configDir }).catch(() => undefined); - cachedMetadata = await computeStorybookMetadata({ mainConfig, packageJson, packageJsonPath }); + cachedMetadata = await computeStorybookMetadata({ + mainConfig, + packageJson, + packageJsonPath, + configDir, + }); return cachedMetadata; }; diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 8af903d08632..caff102504df 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -636,9 +636,9 @@ export interface CoreCommon_StorybookInfo { renderer: string; rendererPackage: string; configDir?: string; - mainConfig?: string; - previewConfig?: string; - managerConfig?: string; + mainConfigPath?: string; + previewConfigPath?: string; + managerConfigPath?: string; } /** diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index 5a1553530ba7..85f6612c21b7 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -57,7 +57,7 @@ "@storybook/global": "^5.0.0", "@types/webpack-env": "^1.18.0", "fd-package-json": "^1.2.0", - "find-up": "^5.0.0", + "find-up": "^7.0.0", "telejson": "8.0.0", "ts-dedent": "^2.0.0", "tsconfig-paths-webpack-plugin": "^4.0.1", diff --git a/code/frameworks/angular/src/builders/build-storybook/index.ts b/code/frameworks/angular/src/builders/build-storybook/index.ts index 0e134155f271..16301df6a160 100644 --- a/code/frameworks/angular/src/builders/build-storybook/index.ts +++ b/code/frameworks/angular/src/builders/build-storybook/index.ts @@ -1,4 +1,4 @@ -import { getEnvConfig, versions } from 'storybook/internal/common'; +import { getEnvConfig, getProjectRoot, versions } from 'storybook/internal/common'; import { buildStaticStandalone, withTelemetry } from 'storybook/internal/core-server'; import { addToGlobalContext } from 'storybook/internal/telemetry'; import { CLIOptions } from 'storybook/internal/types'; @@ -21,7 +21,7 @@ import { } from '@angular-devkit/build-angular/src/builders/browser/schema'; import { JsonObject } from '@angular-devkit/core'; import { findPackageSync } from 'fd-package-json'; -import { sync as findUpSync } from 'find-up'; +import { findUpSync } from 'find-up'; import { from, of, throwError } from 'rxjs'; import { catchError, map, mapTo, switchMap } from 'rxjs/operators'; @@ -70,7 +70,10 @@ const commandBuilder: BuilderHandlerFn = ( ): BuilderOutputLike => { const builder = from(setup(options, context)).pipe( switchMap(({ tsConfig }) => { - const docTSConfig = findUpSync('tsconfig.doc.json', { cwd: options.configDir }); + const docTSConfig = findUpSync('tsconfig.doc.json', { + cwd: options.configDir, + stopAt: getProjectRoot(), + }); const runCompodoc$ = options.compodoc ? runCompodoc( { compodocArgs: options.compodocArgs, tsconfig: docTSConfig ?? tsConfig }, diff --git a/code/frameworks/angular/src/builders/start-storybook/index.ts b/code/frameworks/angular/src/builders/start-storybook/index.ts index d802b17c5474..26d6b2fe2148 100644 --- a/code/frameworks/angular/src/builders/start-storybook/index.ts +++ b/code/frameworks/angular/src/builders/start-storybook/index.ts @@ -1,4 +1,4 @@ -import { getEnvConfig, versions } from 'storybook/internal/common'; +import { getEnvConfig, getProjectRoot, versions } from 'storybook/internal/common'; import { buildDevStandalone, withTelemetry } from 'storybook/internal/core-server'; import { addToGlobalContext } from 'storybook/internal/telemetry'; import { CLIOptions } from 'storybook/internal/types'; @@ -20,7 +20,7 @@ import { } from '@angular-devkit/build-angular/src/builders/browser/schema'; import { JsonObject } from '@angular-devkit/core'; import { findPackageSync } from 'fd-package-json'; -import { sync as findUpSync } from 'find-up'; +import { findUpSync } from 'find-up'; import { Observable, from, of } from 'rxjs'; import { map, mapTo, switchMap } from 'rxjs/operators'; @@ -71,7 +71,10 @@ export type StorybookBuilderOutput = JsonObject & BuilderOutput & {}; const commandBuilder: BuilderHandlerFn = (options, context) => { const builder = from(setup(options, context)).pipe( switchMap(({ tsConfig }) => { - const docTSConfig = findUpSync('tsconfig.doc.json', { cwd: options.configDir }); + const docTSConfig = findUpSync('tsconfig.doc.json', { + cwd: options.configDir, + stopAt: getProjectRoot(), + }); const runCompodoc$ = options.compodoc ? runCompodoc( diff --git a/code/frameworks/angular/src/server/framework-preset-angular-cli.ts b/code/frameworks/angular/src/server/framework-preset-angular-cli.ts index fe148c5281d9..a05c744e7c28 100644 --- a/code/frameworks/angular/src/server/framework-preset-angular-cli.ts +++ b/code/frameworks/angular/src/server/framework-preset-angular-cli.ts @@ -4,12 +4,13 @@ import { WebpackDefinePlugin, WebpackIgnorePlugin } from '@storybook/builder-web import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect'; import { JsonObject, logging } from '@angular-devkit/core'; -import { sync as findUpSync } from 'find-up'; +import { findUpSync } from 'find-up'; import webpack from 'webpack'; import { getWebpackConfig as getCustomWebpackConfig } from './angular-cli-webpack'; import { PresetOptions } from './preset-options'; import { moduleIsAvailable } from './utils/module-is-available'; +import { getProjectRoot } from 'storybook/internal/common'; export async function webpackFinal(baseConfig: webpack.Configuration, options: PresetOptions) { if (!moduleIsAvailable('@angular-devkit/build-angular')) { @@ -88,7 +89,7 @@ async function getBuilderOptions(options: PresetOptions, builderContext: Builder ...options.angularBuilderOptions, tsConfig: options.tsConfig ?? - findUpSync('tsconfig.json', { cwd: options.configDir }) ?? + findUpSync('tsconfig.json', { cwd: options.configDir, stopAt: getProjectRoot() }) ?? browserTargetOptions.tsConfig, }; logger.info(`=> Using angular project with "tsConfig:${builderOptions.tsConfig}"`); diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json index 7c9e4066bfad..f5139acb49bb 100644 --- a/code/frameworks/ember/package.json +++ b/code/frameworks/ember/package.json @@ -52,7 +52,7 @@ "@storybook/builder-webpack5": "workspace:*", "@storybook/global": "^5.0.0", "babel-loader": "9.1.3", - "find-up": "^5.0.0" + "find-up": "^7.0.0" }, "devDependencies": { "ember-source": "~3.28.1", diff --git a/code/frameworks/ember/src/util.ts b/code/frameworks/ember/src/util.ts index 78cd557820af..012b79b3fc97 100644 --- a/code/frameworks/ember/src/util.ts +++ b/code/frameworks/ember/src/util.ts @@ -1,9 +1,11 @@ import { dirname, join } from 'node:path'; -import { sync as findUpSync } from 'find-up'; +import { getProjectRoot } from 'storybook/internal/common'; + +import { findUpSync } from 'find-up'; export const findDistFile = (cwd: string, relativePath: string) => { - const nearestPackageJson = findUpSync('package.json', { cwd }); + const nearestPackageJson = findUpSync('package.json', { cwd, stopAt: getProjectRoot() }); if (!nearestPackageJson) { throw new Error(`Could not find package.json in: ${cwd}`); } diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 6b90ae543a22..c4d9521ac5a7 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -48,10 +48,7 @@ export const core: PresetProperty<'core'> = async (config, options) => { }; }; -export const previewAnnotations: PresetProperty<'previewAnnotations'> = ( - entry = [], - { features } -) => { +export const previewAnnotations: PresetProperty<'previewAnnotations'> = (entry = []) => { const nextDir = dirname(require.resolve('@storybook/nextjs/package.json')); const result = [...entry, join(nextDir, 'dist/preview.mjs')]; return result; diff --git a/code/frameworks/nextjs/src/swc/loader.ts b/code/frameworks/nextjs/src/swc/loader.ts index 68a9c05e5029..9ddabc52b81b 100644 --- a/code/frameworks/nextjs/src/swc/loader.ts +++ b/code/frameworks/nextjs/src/swc/loader.ts @@ -16,11 +16,10 @@ export const configureSWCLoader = async ( ) => { const isDevelopment = options.configType !== 'PRODUCTION'; - const dir = getProjectRoot(); - const { virtualModules } = await getVirtualModules(options); + const projectRoot = getProjectRoot(); - const { jsConfig } = await loadJsConfig(dir, nextConfig as any); + const { jsConfig } = await loadJsConfig(projectRoot, nextConfig as any); const rawRule = baseConfig.module?.rules?.find( (rule) => typeof rule === 'object' && rule?.resourceQuery?.toString() === '/raw/' @@ -32,7 +31,7 @@ export const configureSWCLoader = async ( baseConfig.module?.rules?.push({ test: /\.((c|m)?(j|t)sx?)$/, - include: [getProjectRoot()], + include: [projectRoot], exclude: [/(node_modules)/, ...Object.keys(virtualModules)], use: { // we use our own patch because we need to remove tracing from the original code @@ -40,17 +39,17 @@ export const configureSWCLoader = async ( loader: require.resolve('./swc/next-swc-loader-patch.js'), options: { isServer: false, - rootDir: dir, - pagesDir: `${dir}/pages`, - appDir: `${dir}/apps`, + rootDir: projectRoot, + pagesDir: `${projectRoot}/pages`, + appDir: `${projectRoot}/apps`, hasReactRefresh: isDevelopment, jsConfig, nextConfig, supportedBrowsers: require('next/dist/build/utils').getSupportedBrowsers( - dir, + projectRoot, isDevelopment ), - swcCacheDir: join(dir, nextConfig?.distDir ?? '.next', 'cache', 'swc'), + swcCacheDir: join(projectRoot, nextConfig?.distDir ?? '.next', 'cache', 'swc'), bundleTarget: 'default', }, }, diff --git a/code/frameworks/nextjs/src/utils.ts b/code/frameworks/nextjs/src/utils.ts index 256c022d1621..c7d732f5fb9f 100644 --- a/code/frameworks/nextjs/src/utils.ts +++ b/code/frameworks/nextjs/src/utils.ts @@ -1,4 +1,4 @@ -import { dirname, resolve, sep } from 'node:path'; +import { dirname, sep } from 'node:path'; import { getProjectRoot } from 'storybook/internal/common'; diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index 7e265ae79fa6..b663ecbf0076 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -71,7 +71,7 @@ "@rollup/pluginutils": "^5.0.2", "@storybook/builder-vite": "workspace:*", "@storybook/react": "workspace:*", - "find-up": "^5.0.0", + "find-up": "^7.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", "resolve": "^1.22.8", diff --git a/code/frameworks/react-vite/src/plugins/react-docgen.ts b/code/frameworks/react-vite/src/plugins/react-docgen.ts index 7060d2991a02..d63e39b5aa4b 100644 --- a/code/frameworks/react-vite/src/plugins/react-docgen.ts +++ b/code/frameworks/react-vite/src/plugins/react-docgen.ts @@ -1,10 +1,11 @@ import { existsSync } from 'node:fs'; import { relative, sep } from 'node:path'; +import { getProjectRoot } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; import { createFilter } from '@rollup/pluginutils'; -import findUp from 'find-up'; +import { findUp } from 'find-up'; import MagicString from 'magic-string'; import type { Documentation } from 'react-docgen'; import { @@ -43,7 +44,7 @@ export async function reactDocgen({ const cwd = process.cwd(); const filter = createFilter(include, exclude); - const tsconfigPath = await findUp('tsconfig.json', { cwd }); + const tsconfigPath = await findUp('tsconfig.json', { cwd, stopAt: getProjectRoot() }); const tsconfig = TsconfigPaths.loadConfig(tsconfigPath); let matchPath: TsconfigPaths.MatchPath | undefined; diff --git a/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts b/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts index 82c9f4dc3109..345ff4d70415 100644 --- a/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts +++ b/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts @@ -1,7 +1,8 @@ import { readFile, stat } from 'node:fs/promises'; -import { dirname, join, parse, relative, resolve } from 'node:path'; +import { join, parse } from 'node:path'; + +import { getProjectRoot } from 'storybook/internal/common'; -import findPackageJson from 'find-package-json'; import MagicString from 'magic-string'; import type { ModuleNode, Plugin } from 'vite'; import { @@ -174,6 +175,7 @@ async function createVueComponentMetaChecker(tsconfigPath = 'tsconfig.json') { }; const projectRoot = getProjectRoot(); + const projectTsConfigPath = join(projectRoot, tsconfigPath); const defaultChecker = createCheckerByJson(projectRoot, { include: ['**/*'] }, checkerOptions); @@ -194,16 +196,6 @@ async function createVueComponentMetaChecker(tsconfigPath = 'tsconfig.json') { return defaultChecker; } -/** Gets the absolute path to the project root. */ -function getProjectRoot() { - const projectRoot = findPackageJson().next().value?.path ?? ''; - - const currentFileDir = dirname(__filename); - const relativePathToProjectRoot = relative(currentFileDir, projectRoot); - - return resolve(currentFileDir, relativePathToProjectRoot); -} - /** Gets the filename without file extension. */ function getFilenameWithoutExtension(filename: string) { return parse(filename).name; diff --git a/code/lib/cli-storybook/src/add.test.ts b/code/lib/cli-storybook/src/add.test.ts index 6bc053c563ef..858cd398fdbe 100644 --- a/code/lib/cli-storybook/src/add.test.ts +++ b/code/lib/cli-storybook/src/add.test.ts @@ -12,7 +12,14 @@ const MockedConfig = vi.hoisted(() => { }); const MockedPackageManager = vi.hoisted(() => { return { - retrievePackageJson: vi.fn(() => ({})), + getPrimaryPackageJson: vi.fn(() => ({ + packageJson: { + devDependencies: {}, + dependencies: {}, + }, + packageJsonPath: 'some/path', + operationDir: 'some/path', + })), latestVersion: vi.fn(() => '1.0.0'), addDependencies: vi.fn(() => {}), type: 'npm', @@ -35,6 +42,19 @@ const MockedConsole = { error: vi.fn(), } as any as Console; +const MockedMainConfigFileHelper = vi.hoisted(() => { + return { + getStorybookData: vi.fn(() => ({ + mainConfig: {}, + mainConfigPath: '.storybook/main.ts', + configDir: '.storybook', + previewConfigPath: '.storybook/preview.ts', + storybookVersion: '8.0.0', + packageManager: MockedPackageManager, + })), + }; +}); + vi.mock('storybook/internal/csf-tools', () => { return { readConfig: vi.fn(() => MockedConfig), @@ -47,6 +67,9 @@ vi.mock('./postinstallAddon', () => { vi.mock('./automigrate/fixes/wrap-require-utils', () => { return MockWrapRequireUtils; }); +vi.mock('./automigrate/helpers/mainConfigFile', () => { + return MockedMainConfigFileHelper; +}); vi.mock('./codemod/helpers/csf-factories-utils'); vi.mock('storybook/internal/common', () => { return { diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index 72163824d612..7e161740c420 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -1,7 +1,6 @@ import { isAbsolute, join } from 'node:path'; import { - JsPackageManagerFactory, type PackageManagerName, prompt, serverRequire, @@ -67,6 +66,7 @@ const isCoreAddon = (addonName: string) => Object.hasOwn(versions, addonName); type CLIOptions = { packageManager?: PackageManagerName; configDir?: string; + skipInstall?: boolean; skipPostinstall: boolean; yes?: boolean; }; @@ -86,17 +86,28 @@ type CLIOptions = { */ export async function add( addon: string, - { packageManager: pkgMgr, skipPostinstall, configDir: userSpecifiedConfigDir, yes }: CLIOptions, + { + packageManager: pkgMgr, + skipPostinstall, + configDir: userSpecifiedConfigDir, + yes, + skipInstall, + }: CLIOptions, logger = console ) { const [addonName, inputVersion] = getVersionSpecifier(addon); - const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); - const { mainConfig, mainConfigPath, configDir, previewConfigPath, storybookVersion } = - await getStorybookData({ - packageManager, - configDir: userSpecifiedConfigDir, - }); + const { + mainConfig, + mainConfigPath, + configDir, + previewConfigPath, + storybookVersion, + packageManager, + } = await getStorybookData({ + configDir: userSpecifiedConfigDir, + packageManagerName: pkgMgr, + }); if (typeof configDir === 'undefined') { throw new Error(dedent` @@ -148,8 +159,9 @@ export async function add( : `${addonName}@${version}`; logger.log(`Installing ${addonWithVersion}`); + await packageManager.addDependencies( - { installAsDevDependencies: true, writeOutputToFile: false }, + { installAsDevDependencies: true, writeOutputToFile: false, skipInstall }, [addonWithVersion] ); diff --git a/code/lib/cli-storybook/src/autoblock/block-dependencies-versions.ts b/code/lib/cli-storybook/src/autoblock/block-dependencies-versions.ts index d810de429528..721d944ceef4 100644 --- a/code/lib/cli-storybook/src/autoblock/block-dependencies-versions.ts +++ b/code/lib/cli-storybook/src/autoblock/block-dependencies-versions.ts @@ -34,7 +34,7 @@ export const blocker = createBlocker({ const list = await Promise.all( typedKeys(minimalVersionsMap).map(async (packageName) => ({ packageName, - installedVersion: await packageManager.getPackageVersion(packageName), + installedVersion: packageManager.getModulePackageJSON(packageName)?.version ?? null, minimumVersion: minimalVersionsMap[packageName], })) ); diff --git a/code/lib/cli-storybook/src/autoblock/block-major-version.test.ts b/code/lib/cli-storybook/src/autoblock/block-major-version.test.ts index 467607f15936..5e9a450fbff5 100644 --- a/code/lib/cli-storybook/src/autoblock/block-major-version.test.ts +++ b/code/lib/cli-storybook/src/autoblock/block-major-version.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { JsPackageManager } from 'storybook/internal/common'; import { blocker, checkUpgrade } from './block-major-version'; @@ -106,28 +108,41 @@ describe('checkUpgrade', () => { }); describe('blocker', () => { - const mockPackageManager = { - retrievePackageJson: vi.fn(), - }; + const mockPackageManager = vi.mocked(JsPackageManager.prototype); + + beforeEach(() => { + // @ts-expect-error Ignore readonly property + mockPackageManager.primaryPackageJson = { + packageJson: { devDependencies: {}, dependencies: {} }, + packageJsonPath: 'some/path', + operationDir: 'some/path', + }; + vi.clearAllMocks(); + }); it('check - returns false if no version found', async () => { - mockPackageManager.retrievePackageJson.mockResolvedValue({}); const result = await blocker.check({ packageManager: mockPackageManager } as any); expect(result).toBe(false); }); it('check - returns false if version check fails', async () => { - mockPackageManager.retrievePackageJson.mockResolvedValue({}); const result = await blocker.check({ packageManager: mockPackageManager } as any); expect(result).toBe(false); }); it('check - returns version data with reason if upgrade should be blocked', async () => { - mockPackageManager.retrievePackageJson.mockResolvedValue({ - dependencies: { - '@storybook/react': '6.0.0', + // @ts-expect-error Ignore readonly property + mockPackageManager.primaryPackageJson = { + packageJson: { + devDependencies: {}, + dependencies: { + '@storybook/react': '6.0.0', + }, }, - }); + packageJsonPath: 'some/path', + operationDir: 'some/path', + }; + const result = await blocker.check({ packageManager: mockPackageManager } as any); expect(result).toEqual({ currentVersion: '6.0.0', diff --git a/code/lib/cli-storybook/src/autoblock/block-major-version.ts b/code/lib/cli-storybook/src/autoblock/block-major-version.ts index 7d37107364a0..d37b1a14c7b0 100644 --- a/code/lib/cli-storybook/src/autoblock/block-major-version.ts +++ b/code/lib/cli-storybook/src/autoblock/block-major-version.ts @@ -52,7 +52,7 @@ export const blocker = createBlocker({ async check(options) { const { packageManager } = options; - const packageJson = await packageManager.retrievePackageJson(); + const { packageJson } = packageManager.primaryPackageJson; try { const current = getStorybookVersionSpecifier(packageJson); if (!current) { diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.test.ts index 704748959dde..1eaf4aadd0ac 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.test.ts @@ -1,7 +1,5 @@ /* eslint-disable depend/ban-dependencies */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { getProjectRoot } from 'storybook/internal/common'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { addonA11yParameters } from './addon-a11y-parameters'; @@ -16,7 +14,7 @@ vi.mock('globby', () => ({ vi.mock('storybook/internal/common', () => ({ commonGlobOptions: vi.fn(), - getProjectRoot: vi.fn(), + getProjectRoot: () => '/mock/project/root', })); vi.mock('storybook/internal/csf-tools', () => ({ @@ -25,17 +23,12 @@ vi.mock('storybook/internal/csf-tools', () => ({ })); describe('addon-a11y-parameters', () => { - const mockProjectRoot = '/mock/project/root'; const mockPreviewFile = '/mock/project/root/.storybook/preview.ts'; const mockStoryFiles = [ '/mock/project/root/src/components/Button.stories.ts', '/mock/project/root/src/components/Input.stories.ts', ]; - beforeEach(() => { - (getProjectRoot as unknown as ReturnType).mockReturnValue(mockProjectRoot); - }); - afterEach(() => { vi.clearAllMocks(); }); diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.ts index 885fe7a61665..20e7970869ef 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.ts @@ -1,7 +1,6 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { types as t } from 'storybook/internal/babel'; import { commonGlobOptions, getProjectRoot } from 'storybook/internal/common'; import { writeConfig, writeCsf } from 'storybook/internal/csf-tools'; @@ -37,10 +36,11 @@ export const addonA11yParameters: Fix = { return null; } - const projectRoot = getProjectRoot(); // eslint-disable-next-line depend/ban-dependencies const globby = (await import('globby')).globby; + const projectRoot = getProjectRoot(); + // Get story files from main config patterns const storyFiles = await globby([join(projectRoot, '**/*.stor(y|ies).@(js|jsx|mjs|ts|tsx)')], { ...commonGlobOptions(''), diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.test.ts index c3431d3a8ddf..34a48fe3d06a 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.test.ts @@ -1,8 +1,8 @@ /* eslint-disable depend/ban-dependencies */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { JsPackageManager } from 'storybook/internal/common'; -import type { StorybookConfig } from 'storybook/internal/types'; +import { JsPackageManager } from 'storybook/internal/common'; +import type { PackageJson, StorybookConfig } from 'storybook/internal/types'; import { readFileSync, writeFileSync } from 'fs'; import dedent from 'ts-dedent'; @@ -90,8 +90,22 @@ const checkAddonExperimentalTest = async ({ }); }; +const packageManager = vi.mocked(JsPackageManager.prototype); + describe('addon-experimental-test fix', () => { beforeEach(() => { + packageManager.getModulePackageJSON = vi.fn(); + // @ts-expect-error Ignore readonly property + packageManager.primaryPackageJson = { + packageJson: { devDependencies: {}, dependencies: {} }, + packageJsonPath: 'some/path', + operationDir: 'some/path', + }; + packageManager.runPackageCommand = vi.fn(); + packageManager.getAllDependencies = vi.fn(); + packageManager.addDependencies = vi.fn(); + packageManager.removeDependencies = vi.fn(); + vi.clearAllMocks(); // @ts-expect-error Ignore vi.mocked(readFileSync).mockImplementation((file: string) => { @@ -109,21 +123,25 @@ describe('addon-experimental-test fix', () => { describe('check function', () => { it('should return null if @storybook/experimental-addon-test is not installed', async () => { const packageManager = { - getPackageVersion: () => Promise.resolve(null), + getModulePackageJSON: () => null, }; await expect(checkAddonExperimentalTest({ packageManager })).resolves.toBeNull(); }); it('should find files containing @storybook/experimental-addon-test', async () => { const packageManager = { - getPackageVersion: (packageName: string) => { + getModulePackageJSON: (packageName: string) => { if (packageName === '@storybook/experimental-addon-test') { - return Promise.resolve('8.6.0'); + return { + version: '8.6.0', + }; } if (packageName === 'storybook') { - return Promise.resolve('9.0.0'); + return { + version: '9.0.0', + }; } - return Promise.resolve(null); + return null; }, }; @@ -185,25 +203,30 @@ describe('addon-experimental-test fix', () => { describe('run function', () => { it('should replace @storybook/experimental-addon-test in files', async () => { - const packageManager = { - getPackageVersion: (packageName: string) => { - if (packageName === '@storybook/experimental-addon-test') { - return Promise.resolve('8.6.0'); - } - if (packageName === 'storybook') { - return Promise.resolve('9.0.0'); - } - return Promise.resolve(null); + packageManager.getModulePackageJSON.mockImplementation((packageName: string) => { + if (packageName === '@storybook/experimental-addon-test') { + return { + version: '8.6.0', + }; + } + if (packageName === 'storybook') { + return { + version: '9.0.0', + }; + } + return null; + }); + + // @ts-expect-error Ignore readonly property + packageManager.primaryPackageJson = { + packageJson: { + dependencies: {}, + devDependencies: { + '@storybook/experimental-addon-test': '8.6.0', + }, }, - retrievePackageJson: () => - Promise.resolve({ - dependencies: {}, - devDependencies: { - '@storybook/experimental-addon-test': '8.6.0', - }, - }), - removeDependencies: vi.fn(() => Promise.resolve()), - addDependencies: vi.fn(() => Promise.resolve()), + packageJsonPath: '/some/path', + operationDir: '/some/path', }; const matchingFiles = ['.storybook/test-setup.ts', '.storybook/main.ts', 'vitest.setup.ts']; @@ -213,7 +236,7 @@ describe('addon-experimental-test fix', () => { matchingFiles, hasPackageJsonDependency: true, }, - packageManager: packageManager as any, + packageManager: packageManager as JsPackageManager, dryRun: false, } as any); @@ -245,36 +268,41 @@ describe('addon-experimental-test fix', () => { ); // Verify package dependencies were updated - expect(packageManager.removeDependencies).toHaveBeenCalledWith({}, [ + expect(packageManager.removeDependencies).toHaveBeenCalledWith([ '@storybook/experimental-addon-test', ]); expect(packageManager.addDependencies).toHaveBeenCalledWith( - { installAsDevDependencies: true }, + { installAsDevDependencies: true, skipInstall: true }, ['@storybook/addon-vitest@9.0.0'] ); }); it('should replace @storybook/experimental-addon-test in files (dependency)', async () => { - const packageManager = { - getPackageVersion: (packageName: string) => { - if (packageName === '@storybook/experimental-addon-test') { - return Promise.resolve('8.6.0'); - } - if (packageName === 'storybook') { - return Promise.resolve('9.0.0'); - } - return Promise.resolve(null); + packageManager.getModulePackageJSON.mockImplementation((packageName: string) => { + if (packageName === '@storybook/experimental-addon-test') { + return { + version: '8.6.0', + }; + } + if (packageName === 'storybook') { + return { + version: '9.0.0', + }; + } + return null; + }); + + // @ts-expect-error Ignore readonly property + packageManager.primaryPackageJson = { + packageJson: { + dependencies: { + '@storybook/experimental-addon-test': '8.6.0', + }, + devDependencies: {}, }, - retrievePackageJson: () => - Promise.resolve({ - dependencies: { - '@storybook/experimental-addon-test': '8.6.0', - }, - devDependencies: {}, - }), - removeDependencies: vi.fn(() => Promise.resolve()), - addDependencies: vi.fn(() => Promise.resolve()), + packageJsonPath: '/some/path', + operationDir: '/some/path', }; const matchingFiles = ['.storybook/test-setup.ts', '.storybook/main.ts', 'vitest.setup.ts']; @@ -289,14 +317,17 @@ describe('addon-experimental-test fix', () => { } as any); expect(packageManager.addDependencies).toHaveBeenCalledWith( - { installAsDevDependencies: false }, + { installAsDevDependencies: false, skipInstall: true }, ['@storybook/addon-vitest@9.0.0'] ); }); it('should not modify files or dependencies in dry run mode', async () => { const packageManager = { - getPackageVersion: () => Promise.resolve('0.2.0'), + getModulePackageJSON: () => + ({ + version: '0.2.0', + }) as PackageJson, removeDependencies: vi.fn(), addDependencies: vi.fn(), }; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.ts index f0577b7711d6..d600ecdd1435 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.ts @@ -27,9 +27,8 @@ export const addonExperimentalTest: Fix = { promptType: 'auto', async check({ packageManager }) { - const experimentalAddonTestVersion = await packageManager.getPackageVersion( - '@storybook/experimental-addon-test' - ); + const experimentalAddonTestVersion = + packageManager.getModulePackageJSON('@storybook/experimental-addon-test')?.version ?? null; if (!experimentalAddonTestVersion) { return null; @@ -103,16 +102,16 @@ export const addonExperimentalTest: Fix = { // Update package.json if needed if (!dryRun) { - const packageJson = await packageManager.retrievePackageJson(); + const { packageJson } = packageManager.primaryPackageJson; const devDependencies = packageJson.devDependencies ?? {}; - const storybookVersion = await packageManager.getPackageVersion('storybook'); + const storybookVersion = packageManager.getModulePackageJSON('storybook')?.version ?? null; const isExperimentalAddonTestDevDependency = Object.keys(devDependencies).includes( '@storybook/experimental-addon-test' ); - await packageManager.removeDependencies({}, ['@storybook/experimental-addon-test']); + await packageManager.removeDependencies(['@storybook/experimental-addon-test']); await packageManager.addDependencies( - { installAsDevDependencies: isExperimentalAddonTestDevDependency }, + { installAsDevDependencies: isExperimentalAddonTestDevDependency, skipInstall: true }, [`@storybook/addon-vitest@${storybookVersion}`] ); } diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.test.ts index 960df3a20631..08566c0bb0c2 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { JsPackageManager } from 'storybook/internal/common'; +import { JsPackageManager } from 'storybook/internal/common'; import type { StorybookConfigRaw } from 'storybook/internal/types'; import type { CheckOptions, RunOptions } from '../types'; @@ -46,11 +46,7 @@ const mockConfigs = new Map(); // Get reference to mocked readFile const readFileMock = vi.mocked(await import('node:fs/promises')).readFile; -const mockPackageManager = { - retrievePackageJson: vi.fn(), - removeDependencies: vi.fn(), - runPackageCommand: vi.fn(), -} as unknown as JsPackageManager; +const mockPackageManager = vi.mocked(JsPackageManager.prototype); const baseCheckOptions: CheckOptions = { packageManager: mockPackageManager, @@ -77,6 +73,7 @@ const typedAddonMdxGfmRemove = addonMdxGfmRemove as Migration; describe('addon-mdx-gfm-remove migration', () => { beforeEach(() => { vi.clearAllMocks(); + mockPackageManager.runPackageCommand = vi.fn(); mockConfigs.clear(); }); @@ -167,7 +164,7 @@ describe('addon-mdx-gfm-remove migration', () => { result: { hasMdxGfm: true, }, - packageManager: mockPackageManager, + packageManager: mockPackageManager as JsPackageManager, configDir: '.storybook', } as RunOptions); @@ -176,6 +173,7 @@ describe('addon-mdx-gfm-remove migration', () => { '@storybook/addon-mdx-gfm', '--config-dir', '.storybook', + '--skip-install', ]); }); }); diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.ts index 2f9dec4a99a2..81853a749230 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.ts @@ -53,6 +53,7 @@ export const addonMdxGfmRemove: Fix = { '@storybook/addon-mdx-gfm', '--config-dir', configDir, + '--skip-install', ]); }, }; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-code-panel.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-code-panel.test.ts index 4ba7005ccda1..4a03f38f61d5 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-code-panel.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-code-panel.test.ts @@ -5,9 +5,12 @@ import type { StorybookConfigRaw } from 'storybook/internal/types'; import dedent from 'ts-dedent'; +import { add } from '../../add'; import type { CheckOptions, RunOptions } from '../types'; import { type StorysourceOptions, addonStorysourceCodePanel } from './addon-storysource-code-panel'; +vi.mock('../../add'); + // Mock modules before any other imports or declarations vi.mock('node:fs/promises', async (importOriginal) => { const mod = (await importOriginal()) as any; @@ -48,11 +51,7 @@ vi.mock('picocolors', () => { }); // Create mock package manager -const mockPackageManager = { - retrievePackageJson: vi.fn(), - removeDependencies: vi.fn(), - runPackageCommand: vi.fn(), -} as unknown as JsPackageManager; +const mockPackageManager = {} as JsPackageManager; // Set up test data const baseCheckOptions: CheckOptions = { @@ -67,6 +66,7 @@ const baseCheckOptions: CheckOptions = { describe('addon-storysource-remove', () => { beforeEach(() => { + mockPackageManager.runPackageCommand = vi.fn(); vi.clearAllMocks(); }); @@ -179,7 +179,7 @@ describe('addon-storysource-remove', () => { hasStorysource: false, hasDocs: false, }, - packageManager: mockPackageManager, + packageManager: mockPackageManager as JsPackageManager, configDir: '.storybook', } as RunOptions); @@ -192,7 +192,7 @@ describe('addon-storysource-remove', () => { hasStorysource: true, hasDocs: true, }, - packageManager: mockPackageManager, + packageManager: mockPackageManager as JsPackageManager, previewConfigPath: '.storybook/preview.js', configDir: '.storybook', } as RunOptions); @@ -203,6 +203,7 @@ describe('addon-storysource-remove', () => { '@storybook/addon-storysource', '--config-dir', '.storybook', + '--skip-install', ]); expect(mockPackageManager.runPackageCommand).not.toHaveBeenCalledWith('storybook', [ @@ -246,17 +247,16 @@ describe('addon-storysource-remove', () => { hasStorysource: true, hasDocs: false, }, - packageManager: mockPackageManager, + packageManager: mockPackageManager as JsPackageManager, previewConfigPath: '.storybook/preview.js', configDir: '.storybook', } as RunOptions); - expect(mockPackageManager.runPackageCommand).toHaveBeenCalledWith('storybook', [ - 'add', - '@storybook/addon-docs', - '--config-dir', - '.storybook', - ]); + expect(vi.mocked(add)).toHaveBeenCalledWith('@storybook/addon-docs', { + configDir: '.storybook', + skipInstall: true, + skipPostinstall: true, + }); }); it('does nothing in dry run mode', async () => { @@ -266,7 +266,7 @@ describe('addon-storysource-remove', () => { hasDocs: true, }, previewConfigPath: '.storybook/preview.js', - packageManager: mockPackageManager, + packageManager: mockPackageManager as JsPackageManager, configDir: '.storybook', dryRun: true, } as RunOptions); diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-code-panel.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-code-panel.ts index 675b1603f120..c289d8db5872 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-code-panel.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-code-panel.ts @@ -3,6 +3,7 @@ import { getAddonNames } from 'storybook/internal/common'; import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; +import { add } from '../../add'; import { updateMainConfig } from '../helpers/mainConfigFile'; import type { Fix, RunOptions } from '../types'; @@ -73,17 +74,18 @@ export const addonStorysourceCodePanel: Fix = { '@storybook/addon-storysource', '--config-dir', configDir, + '--skip-install', ]); if (!hasDocs) { logger.log('Installing @storybook/addon-docs...'); - await packageManager.runPackageCommand('storybook', [ - 'add', - '@storybook/addon-docs', - '--config-dir', + await add('@storybook/addon-docs', { configDir, - ]); + packageManager: packageManager.type, + skipInstall: true, + skipPostinstall: true, + }); } // Update preview config to enable code panel diff --git a/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.test.ts index 25fc39de07aa..55c06ce250f6 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.test.ts @@ -2,7 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises'; import { describe, expect, it, vi } from 'vitest'; -import { versions } from 'storybook/internal/common'; +import { JsPackageManager, versions } from 'storybook/internal/common'; import { consolidatedImports, transformPackageJsonFiles } from './consolidated-imports'; @@ -34,10 +34,10 @@ const mockPackageJson = { }, }; +const mockPackageManager = vi.mocked(JsPackageManager.prototype); + const mockRunOptions = { - packageManager: { - retrievePackageJson: async () => mockPackageJson, - } as any, + packageManager: mockPackageManager, mainConfig: {} as any, mainConfigPath: 'main.ts', packageJson: mockPackageJson, diff --git a/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.ts b/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.ts index 6857c12c713a..5247550833e9 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.ts @@ -101,14 +101,13 @@ export const consolidatedImports: Fix = { id: 'consolidated-imports', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], check: async () => { - const projectRoot = getProjectRoot(); // eslint-disable-next-line depend/ban-dependencies const globby = (await import('globby')).globby; const packageJsonFiles = await globby(['**/package.json'], { ...commonGlobOptions(''), ignore: ['**/node_modules/**'], - cwd: projectRoot, + cwd: getProjectRoot(), gitignore: true, absolute: true, }); @@ -198,9 +197,5 @@ export const consolidatedImports: Fix = { .join('\n')}` ); } - - if (!dryRun && result.packageJsonFiles.length > 0) { - await options.packageManager.installDependencies(); - } }, }; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.test.ts index ffa40f3250bd..7c5c95ebd691 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.test.ts @@ -66,6 +66,7 @@ describe('removeAddonInteractions', () => { '@storybook/addon-interactions', '--config-dir', './storybook', + '--skip-install', ]); }); }); diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.ts index a4d684a3eae0..5e56fe631ec5 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.ts @@ -41,6 +41,7 @@ export const removeAddonInteractions: Fix<{}> = { '@storybook/addon-interactions', '--config-dir', configDir, + '--skip-install', ]); } }, diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.test.ts index 277d1f7f03e1..928339393125 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.test.ts @@ -3,7 +3,8 @@ import type * as fs from 'node:fs/promises'; import { describe, expect, it, vi } from 'vitest'; -import type { JsPackageManager, PackageJson } from 'storybook/internal/common'; +import type { PackageJson } from 'storybook/internal/common'; +import { JsPackageManager } from 'storybook/internal/common'; import type { StorybookConfigRaw } from 'storybook/internal/types'; import type { CheckOptions, Fix } from '../types'; @@ -22,13 +23,7 @@ vi.mock('node:fs/promises', async (importOriginal) => { }; }); -const mockPackageManager = { - retrievePackageJson: vi.fn().mockResolvedValue({ - dependencies: {}, - devDependencies: {}, - }), - runPackageCommand: vi.fn(), -} as unknown as JsPackageManager; +const mockPackageManager = vi.mocked(JsPackageManager.prototype); const mockPackageJson = { dependencies: {}, diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.test.ts index e3aaef89e428..30a05e4d6875 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.test.ts @@ -1,8 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { JsPackageManager, PackageJson } from 'storybook/internal/common'; +import type { PackageJson } from 'storybook/internal/common'; +import { JsPackageManager } from 'storybook/internal/common'; import type { StorybookConfigRaw } from 'storybook/internal/types'; +import { add } from '../../add'; import type { CheckOptions, RunOptions } from '../types'; import { removeEssentials } from './remove-essentials'; @@ -32,12 +34,16 @@ vi.mock('storybook/internal/common', async (importOriginal) => { return { ...(await importOriginal()), getAddonNames: vi.fn(), - getProjectRoot: vi.fn().mockReturnValue('/fake/project/root'), + getProjectRoot: () => '/fake/project/root', commonGlobOptions: vi.fn().mockReturnValue({}), scanAndTransformFiles: vi.fn().mockResolvedValue([]), }; }); +vi.mock('../../add', () => ({ + add: vi.fn(), +})); + vi.mock('prompts', () => ({ default: vi.fn().mockResolvedValue({ glob: '**/*.{mjs,cjs,js,jsx,ts,tsx,mdx}' }), })); @@ -64,14 +70,9 @@ const mockConfigs = new Map(); // Get reference to mocked readFile const readFileMock = vi.mocked(await import('node:fs/promises')).readFile; -const mockPackageManager = { - retrievePackageJson: vi.fn().mockResolvedValue({ - dependencies: {}, - devDependencies: {}, - }), - runPackageCommand: vi.fn(), - addDependencies: vi.fn(), -} as unknown as JsPackageManager; +const mockPackageManager = vi.mocked(JsPackageManager.prototype); + +const mockedAdd = vi.mocked(add); const mockPackageJson = { dependencies: {}, @@ -105,6 +106,16 @@ const typedAddonDocsEssentials = removeEssentials as Migration; describe('remove-essentials migration', () => { beforeEach(() => { + // @ts-expect-error Ignore readonly property + mockPackageManager.primaryPackageJson = { + packageJson: { devDependencies: {}, dependencies: {} }, + packageJsonPath: 'some/path', + operationDir: 'some/path', + }; + mockPackageManager.runPackageCommand = vi.fn(); + mockPackageManager.getAllDependencies = vi.fn(); + mockPackageManager.addDependencies = vi.fn(); + vi.clearAllMocks(); mockConfigs.clear(); }); @@ -168,9 +179,17 @@ describe('remove-essentials migration', () => { }, }; - vi.mocked(mockPackageManager.retrievePackageJson).mockResolvedValueOnce( - mockPackageJsonWithAddons - ); + // @ts-expect-error Ignore readonly property + mockPackageManager.primaryPackageJson = { + packageJson: mockPackageJsonWithAddons, + packageJsonPath: 'some/path', + operationDir: 'some/path', + }; + + mockPackageManager.getAllDependencies.mockReturnValue({ + ...mockPackageJsonWithAddons.dependencies, + ...mockPackageJsonWithAddons.devDependencies, + }); const result = await typedAddonDocsEssentials.check({ ...baseCheckOptions, @@ -199,7 +218,6 @@ describe('remove-essentials migration', () => { '@storybook/addon-controls': '^7.0.0', '@storybook/addon-toolbars': '^7.0.0', }, - packageJson: mockPackageJsonWithAddons, }); }); @@ -227,9 +245,17 @@ describe('remove-essentials migration', () => { }, }; - vi.mocked(mockPackageManager.retrievePackageJson).mockResolvedValueOnce( - mockPackageJsonWithViewport - ); + // @ts-expect-error Ignore readonly property + mockPackageManager.primaryPackageJson = { + packageJson: mockPackageJsonWithViewport, + packageJsonPath: 'some/path', + operationDir: 'some/path', + }; + + mockPackageManager.getAllDependencies.mockReturnValue({ + ...mockPackageJsonWithViewport.dependencies, + ...mockPackageJsonWithViewport.devDependencies, + }); const result = await typedAddonDocsEssentials.check({ ...baseCheckOptions, @@ -247,12 +273,6 @@ describe('remove-essentials migration', () => { allDeps: { '@storybook/addon-viewport': '^7.0.0', }, - packageJson: { - dependencies: {}, - devDependencies: { - '@storybook/addon-viewport': '^7.0.0', - }, - }, }); }); }); @@ -279,28 +299,26 @@ describe('remove-essentials migration', () => { '@storybook/addon-essentials', '--config-dir', '.storybook', + '--skip-install', ]); expect(mockPackageManager.runPackageCommand).toHaveBeenCalledWith('storybook', [ 'remove', '@storybook/addon-actions', '--config-dir', '.storybook', + '--skip-install', ]); expect(mockPackageManager.runPackageCommand).toHaveBeenCalledWith('storybook', [ 'remove', '@storybook/addon-controls', '--config-dir', '.storybook', + '--skip-install', ]); expect(mockPackageManager.runPackageCommand).toHaveBeenCalledTimes(3); }); it('removes core addons without essentials', async () => { - const mockPackageManagerLocal = { - retrievePackageJson: vi.fn(), - runPackageCommand: vi.fn(), - } as unknown as JsPackageManager; - await typedAddonDocsEssentials.run({ result: { hasEssentials: true, @@ -308,7 +326,7 @@ describe('remove-essentials migration', () => { hasDocsAddon: false, additionalAddonsToRemove: ['@storybook/addon-actions', '@storybook/addon-controls'], }, - packageManager: mockPackageManagerLocal, + packageManager: mockPackageManager, packageJson: mockPackageJson, mainConfigPath: '.storybook/main.ts', configDir: '.storybook', @@ -316,24 +334,26 @@ describe('remove-essentials migration', () => { mainConfig: {} as StorybookConfigRaw, }); - expect(mockPackageManagerLocal.runPackageCommand).toHaveBeenCalledWith('storybook', [ + expect(mockPackageManager.runPackageCommand).toHaveBeenCalledWith('storybook', [ 'remove', '@storybook/addon-actions', '--config-dir', '.storybook', + '--skip-install', ]); - expect(mockPackageManagerLocal.runPackageCommand).toHaveBeenCalledWith('storybook', [ + expect(mockPackageManager.runPackageCommand).toHaveBeenCalledWith('storybook', [ 'remove', '@storybook/addon-controls', '--config-dir', '.storybook', + '--skip-install', ]); - expect(mockPackageManagerLocal.runPackageCommand).toHaveBeenCalledWith('storybook', [ - 'add', - '@storybook/addon-docs', - '--config-dir', - '.storybook', - ]); + expect(mockedAdd).toHaveBeenCalledWith('@storybook/addon-docs', { + configDir: '.storybook', + packageManager: mockPackageManager.type, + skipInstall: true, + skipPostinstall: true, + }); }); it('does not add docs addon if essentials is not present', async () => { @@ -361,11 +381,6 @@ describe('remove-essentials migration', () => { }); it('does add docs addon if essentials is present', async () => { - const mockPackageManagerLocal = { - retrievePackageJson: vi.fn(), - runPackageCommand: vi.fn(), - } as unknown as JsPackageManager; - await typedAddonDocsEssentials.run({ result: { hasEssentials: true, @@ -373,7 +388,7 @@ describe('remove-essentials migration', () => { hasDocsAddon: false, additionalAddonsToRemove: [], }, - packageManager: mockPackageManagerLocal, + packageManager: mockPackageManager, packageJson: mockPackageJson, mainConfigPath: '.storybook/main.ts', configDir: '.storybook', @@ -381,12 +396,12 @@ describe('remove-essentials migration', () => { mainConfig: {} as StorybookConfigRaw, }); - expect(mockPackageManagerLocal.runPackageCommand).toHaveBeenCalledWith('storybook', [ - 'add', - '@storybook/addon-docs', - '--config-dir', - '.storybook', - ]); + expect(mockedAdd).toHaveBeenCalledWith('@storybook/addon-docs', { + configDir: '.storybook', + packageManager: mockPackageManager.type, + skipInstall: true, + skipPostinstall: true, + }); }); it('does install docs addon as dev dependency if essentials is present and docs is configured in main config', async () => { @@ -413,7 +428,7 @@ describe('remove-essentials migration', () => { }); expect(mockPackageManager.addDependencies).toHaveBeenCalledWith( - { installAsDevDependencies: true }, + { installAsDevDependencies: true, skipInstall: true }, ['@storybook/addon-docs@^9.0.0'] ); }); @@ -447,7 +462,7 @@ describe('remove-essentials migration', () => { }); expect(mockPackageManager.addDependencies).toHaveBeenCalledWith( - { installAsDevDependencies: asDevDependency }, + { installAsDevDependencies: asDevDependency, skipInstall: true }, ['@storybook/addon-docs@^9.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 844b51fffd69..d2694d966cce 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-essentials.ts @@ -1,5 +1,4 @@ import { - type PackageJson, getAddonNames, scanAndTransformFiles, transformImportFiles, @@ -8,6 +7,7 @@ import { import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; +import { add } from '../../add'; import type { Fix } from '../types'; interface AddonDocsOptions { @@ -16,7 +16,6 @@ interface AddonDocsOptions { hasDocsAddon: boolean; additionalAddonsToRemove: string[]; allDeps: Record; - packageJson: PackageJson; } const consolidatedAddons = { @@ -69,12 +68,7 @@ export const removeEssentials: Fix = { hasEssentialsAddon = addonNames.includes('@storybook/addon-essentials'); hasDocsAddon = addonNames.includes('@storybook/addon-docs'); - const packageJson = await packageManager.retrievePackageJson(); - - const allDeps = { - ...packageJson.dependencies, - ...packageJson.devDependencies, - } as Record; + const allDeps = packageManager.getAllDependencies(); const installedAddons = Object.keys(allDeps); @@ -111,7 +105,6 @@ export const removeEssentials: Fix = { hasDocsAddon, additionalAddonsToRemove, allDeps, - packageJson, }; } catch (err) { return null; @@ -176,6 +169,7 @@ export const removeEssentials: Fix = { '@storybook/addon-essentials', '--config-dir', configDir, + '--skip-install', ]); } @@ -186,6 +180,7 @@ export const removeEssentials: Fix = { addon, '--config-dir', configDir, + '--skip-install', ]); } @@ -208,12 +203,12 @@ export const removeEssentials: Fix = { if (!hasDocsDisabled && hasEssentials) { if (!hasDocsAddon) { console.log('Adding @storybook/addon-docs...'); - await packageManager.runPackageCommand('storybook', [ - 'add', - '@storybook/addon-docs', - '--config-dir', + await add('@storybook/addon-docs', { configDir, - ]); + packageManager: packageManager.type, + skipInstall: true, + skipPostinstall: true, + }); } else { const allDeps = result.allDeps; const isDocsInstalled = allDeps['@storybook/addon-docs'] !== undefined; @@ -223,7 +218,7 @@ export const removeEssentials: Fix = { const isStorybookDevDependency = packageJson.devDependencies?.storybook !== undefined; await packageManager.addDependencies( - { installAsDevDependencies: isStorybookDevDependency }, + { installAsDevDependencies: isStorybookDevDependency, skipInstall: true }, ['@storybook/addon-docs@' + storybookVersion] ); } diff --git a/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.test.ts index 0acab83c82a9..cdb0ed02c5f0 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.test.ts @@ -15,7 +15,7 @@ vi.mock('globby', () => ({ vi.mock('storybook/internal/common', async (importOriginal) => ({ ...(await importOriginal()), commonGlobOptions: () => ({}), - getProjectRoot: vi.fn().mockResolvedValue('/project/root'), + getProjectRoot: () => '/project/root', })); const mockPackageJson = { diff --git a/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts b/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts index 4346f9c59751..c8042bc273ed 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts @@ -128,14 +128,13 @@ export const rendererToFramework: Fix = { promptType: 'auto', async check(): Promise { - const projectRoot = await getProjectRoot(); // eslint-disable-next-line depend/ban-dependencies const { globby } = await import('globby'); const packageJsonFiles = await globby(['**/package.json'], { ...commonGlobOptions(''), ignore: ['**/node_modules/**'], - cwd: projectRoot, + cwd: getProjectRoot(), gitignore: true, absolute: true, }); @@ -189,12 +188,9 @@ export const rendererToFramework: Fix = { initialValue: defaultGlob, }); - const projectRoot = getProjectRoot(); // eslint-disable-next-line depend/ban-dependencies const globby = (await import('globby')).globby; - let didMigrate = false; - for (const selectedFramework of result.frameworks) { const frameworkName = frameworkPackages[selectedFramework]; if (!frameworkName) { @@ -220,7 +216,7 @@ export const rendererToFramework: Fix = { ...commonGlobOptions(''), ignore: ['**/node_modules/**'], dot: true, - cwd: projectRoot, + cwd: getProjectRoot(), absolute: true, }); @@ -236,12 +232,6 @@ export const rendererToFramework: Fix = { removeRendererInPackageJson(file, rendererPackage, dryRun) ) ); - didMigrate = true; - } - - // Install dependencies once if any migration was performed - if (didMigrate && !dryRun) { - await options.packageManager.installDependencies(); } }, }; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/rnstorybook-config.ts b/code/lib/cli-storybook/src/automigrate/fixes/rnstorybook-config.ts index 60ab6a992520..ca241c7e7ca1 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/rnstorybook-config.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/rnstorybook-config.ts @@ -41,7 +41,7 @@ export const rnstorybookConfig: Fix = { versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], async check({ packageManager, mainConfigPath }) { - const allDependencies = await packageManager.getAllDependencies(); + const allDependencies = packageManager.getAllDependencies(); if (!allDependencies['@storybook/react-native']) { return null; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.test.ts index bf06c2fe1cd2..bad8ee9055d2 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.test.ts @@ -61,7 +61,7 @@ describe('upgrade-storybook-related-dependencies fix', () => { await expect( check({ packageManager: { - getAllDependencies: async () => + getAllDependencies: () => analyzedPackages.reduce( (acc, { packageName, packageVersion }) => { acc[packageName] = packageVersion; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.ts b/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.ts index 682924b095de..653e143011ca 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.ts @@ -53,7 +53,7 @@ export const upgradeStorybookRelatedDependencies = { skipErrors: true, }); - const allDependencies = (await packageManager.getAllDependencies()) as Record; + const allDependencies = packageManager.getAllDependencies(); const storybookDependencies = Object.keys(allDependencies) .filter((dep) => dep.includes('storybook')) .filter((dep) => !isCorePackage(dep) && !isSatelliteAddon(dep)); @@ -111,7 +111,7 @@ export const upgradeStorybookRelatedDependencies = { } if (upgradable.length > 0) { - const packageJson = await packageManager.readPackageJson(); + const { packageJson } = packageManager.primaryPackageJson; upgradable.forEach((item) => { if (!item) { @@ -132,8 +132,7 @@ export const upgradeStorybookRelatedDependencies = { } }); - await packageManager.writePackageJson(packageJson); - await packageManager.installDependencies(); + packageManager.writePackageJson(packageJson); await packageManager .executeCommand({ command: 'dedupe', args: [], stdio: 'ignore' }) diff --git a/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts b/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts index 11da55c5551c..a13c8e14056d 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts @@ -1,6 +1,7 @@ import { normalize } from 'node:path'; import { + JsPackageManagerFactory, builderPackages, extractProperFrameworkName, frameworkPackages, @@ -8,7 +9,7 @@ import { loadMainConfig, rendererPackages, } from 'storybook/internal/common'; -import type { JsPackageManager } from 'storybook/internal/common'; +import type { PackageManagerName } from 'storybook/internal/common'; import { frameworkToRenderer, getCoercedStorybookVersion } from 'storybook/internal/common'; import type { ConfigFile } from 'storybook/internal/csf-tools'; import { readConfig, writeConfig as writeConfigFile } from 'storybook/internal/csf-tools'; @@ -123,23 +124,28 @@ export const getRendererPackageNameFromFramework = (frameworkPackageName: string }; export const getStorybookData = async ({ - packageManager, configDir: userDefinedConfigDir, + packageManagerName, }: { - packageManager: JsPackageManager; configDir?: string; + packageManagerName?: PackageManagerName; }) => { - const packageJson = await packageManager.retrievePackageJson(); const { - mainConfig: mainConfigPath, + mainConfigPath: mainConfigPath, version: storybookVersionSpecifier, configDir: configDirFromScript, - previewConfig: previewConfigPath, - } = getStorybookInfo(packageJson, userDefinedConfigDir); - const storybookVersion = await getCoercedStorybookVersion(packageManager); + previewConfigPath, + } = getStorybookInfo(userDefinedConfigDir); const configDir = userDefinedConfigDir || configDirFromScript || '.storybook'; + const packageManager = JsPackageManagerFactory.getPackageManager({ + force: packageManagerName, + configDir, + }); + + const storybookVersion = await getCoercedStorybookVersion(packageManager); + let mainConfig: StorybookConfigRaw; try { mainConfig = (await loadMainConfig({ configDir, noCache: true })) as StorybookConfigRaw; @@ -156,7 +162,7 @@ export const getStorybookData = async ({ storybookVersion, mainConfigPath, previewConfigPath, - packageJson, + packageManager, }; }; export type GetStorybookData = typeof getStorybookData; diff --git a/code/lib/cli-storybook/src/automigrate/helpers/testing-helpers.ts b/code/lib/cli-storybook/src/automigrate/helpers/testing-helpers.ts index bb73cc6ab036..59e11bb2f1a3 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/testing-helpers.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/testing-helpers.ts @@ -1,6 +1,10 @@ import { vi } from 'vitest'; -import type { JsPackageManager, PackageJson } from 'storybook/internal/common'; +import type { + JsPackageManager, + PackageJson, + PackageJsonWithDepsAndDevDeps, +} from 'storybook/internal/common'; vi.mock('./mainConfigFile', async (importOriginal) => ({ ...(await importOriginal()), @@ -15,8 +19,16 @@ vi.mock('storybook/internal/common', async (importOriginal) => ({ export const makePackageManager = (packageJson: PackageJson) => { const { dependencies = {}, devDependencies = {}, peerDependencies = {} } = packageJson; return { - retrievePackageJson: async () => ({ dependencies: {}, devDependencies: {}, ...packageJson }), - getAllDependencies: async () => ({ + primaryPackageJson: { + packageJson: { + dependencies: {}, + devDependencies: {}, + ...packageJson, + } as PackageJsonWithDepsAndDevDeps, + packageJsonPath: '/some/path', + operationDir: '/some/path', + }, + getAllDependencies: () => ({ ...dependencies, ...devDependencies, ...peerDependencies, diff --git a/code/lib/cli-storybook/src/automigrate/index.test.ts b/code/lib/cli-storybook/src/automigrate/index.test.ts index 6f147c159b8b..cc4c70074d3c 100644 --- a/code/lib/cli-storybook/src/automigrate/index.test.ts +++ b/code/lib/cli-storybook/src/automigrate/index.test.ts @@ -1,14 +1,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { JsPackageManager, PackageJsonWithDepsAndDevDeps } from 'storybook/internal/common'; +import type { JsPackageManager, PackageJson } from 'storybook/internal/common'; import { runFixes } from './index'; import type { Fix } from './types'; const check1 = vi.fn(); const run1 = vi.fn(); -const retrievePackageJson = vi.fn(); -const getPackageVersion = vi.fn(); +const getModulePackageJSON = vi.fn(); const prompt1Message = 'prompt1Message'; vi.spyOn(console, 'error').mockImplementation(console.log); @@ -57,12 +56,8 @@ vi.mock('prompts', () => { }); class PackageManager implements Partial { - public async retrievePackageJson(): Promise { - return retrievePackageJson(); - } - - getPackageVersion(packageName: string, basePath?: string | undefined): Promise { - return getPackageVersion(packageName, basePath); + getModulePackageJSON(packageName: string, basePath?: string | undefined): PackageJson | null { + return getModulePackageJSON(packageName, basePath); } } @@ -103,12 +98,10 @@ const runFixWrapper = async ({ describe('runFixes', () => { beforeEach(() => { - retrievePackageJson.mockResolvedValue({ - dependencies: [], - devDependencies: [], - }); - getPackageVersion.mockImplementation((packageName) => { - return beforeVersion; + getModulePackageJSON.mockImplementation(() => { + return { + version: beforeVersion, + }; }); check1.mockResolvedValue({ some: 'result' }); }); diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index e3015a115c0d..9730a1d7c962 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -72,22 +72,20 @@ const logAvailableMigrations = () => { }; export const doAutomigrate = async (options: AutofixOptionsFromCLI) => { - const packageManager = JsPackageManagerFactory.getPackageManager({ - force: options.packageManager, - }); - const { mainConfig, mainConfigPath, previewConfigPath, storybookVersion, configDir, - packageJson, + packageManager, } = await getStorybookData({ configDir: options.configDir, - packageManager, + packageManagerName: options.packageManager, }); + const { packageJson } = packageManager.primaryPackageJson; + if (!storybookVersion) { throw new Error('Could not determine Storybook version'); } @@ -110,6 +108,8 @@ export const doAutomigrate = async (options: AutofixOptionsFromCLI) => { isLatest: false, }); + packageManager.installDependencies(); + if (outcome) { await doctor({ configDir, packageManager: options.packageManager }); } diff --git a/code/lib/cli-storybook/src/bin/index.ts b/code/lib/cli-storybook/src/bin/index.ts index 47d9b7bdb8f0..7ee14b7ed158 100644 --- a/code/lib/cli-storybook/src/bin/index.ts +++ b/code/lib/cli-storybook/src/bin/index.ts @@ -73,6 +73,7 @@ command('add ') 'Force package manager for installing dependencies' ) .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') + .option('--skip-install', 'Skip installing deps') .option('-s --skip-postinstall', 'Skip package specific postinstall config modifications') .option('-y --yes', 'Skip prompting the user') .action((addonName: string, options: any) => add(addonName, options)); @@ -84,6 +85,7 @@ command('remove ') 'Force package manager for installing dependencies' ) .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') + .option('-s --skip-install', 'Skip installing deps') .action((addonName: string, options: any) => withTelemetry('remove', { cliOptions: options }, async () => { await remove(addonName, options); @@ -110,7 +112,7 @@ command('info') .description('Prints debugging information about the local environment') .action(async () => { consoleLogger.log(picocolors.bold('\nStorybook Environment Info:')); - const pkgManager = await JsPackageManagerFactory.getPackageManager(); + const pkgManager = JsPackageManagerFactory.getPackageManager(); const activePackageManager = pkgManager.type.replace(/\d/, ''); // 'yarn1' -> 'yarn' const output = await envinfo.run({ System: ['OS', 'CPU', 'Shell'], diff --git a/code/lib/cli-storybook/src/codemod/csf-factories.ts b/code/lib/cli-storybook/src/codemod/csf-factories.ts index f72d50a107cd..ae60425aa5bb 100644 --- a/code/lib/cli-storybook/src/codemod/csf-factories.ts +++ b/code/lib/cli-storybook/src/codemod/csf-factories.ts @@ -32,7 +32,12 @@ async function runStoriesCodemod(options: { // TODO: Move the csf-2-to-3 codemod into automigrations await packageManager.executeCommand({ - command: `${packageManager.getRemoteRunCommand()} storybook migrate csf-2-to-3 --glob=${globString}`, + command: packageManager.getRemoteRunCommand('storybook', [ + 'migrate', + 'csf-2-to-3', + '--glob', + globString, + ]), args: [], stdio: 'ignore', ignoreError: true, @@ -74,7 +79,7 @@ export const csfFactories: CommandFix = { As we modify your story files, we can create two types of imports: - ${picocolors.bold('Subpath imports (recommended):')} ${picocolors.cyan("`import preview from '#.storybook/preview'`")} - - ${picocolors.bold('Relative imports:')} ${picocolors.cyan("`import preview from '../../.storybook/preview'`")} + - ${picocolors.bold('Relative imports (suitable for mono repos):')} ${picocolors.cyan("`import preview from '../../.storybook/preview'`")} `); useSubPathImports = await prompt.select({ @@ -87,13 +92,15 @@ export const csfFactories: CommandFix = { } if (useSubPathImports && !packageJson.imports?.['#*']) { - logger.log(`🗺️ Adding imports map in ${picocolors.cyan(packageManager.packageJsonPath())}`); + logger.log( + `🗺️ Adding imports map in ${picocolors.cyan(packageManager.primaryPackageJson.packageJsonPath)}` + ); packageJson.imports = { ...packageJson.imports, // @ts-expect-error we need to upgrade type-fest '#*': ['./*', './*.ts', './*.tsx', './*.js', './*.jsx'], }; - await packageManager.writePackageJson(packageJson); + packageManager.writePackageJson(packageJson); } await runStoriesCodemod({ diff --git a/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.test.ts b/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.test.ts index d09d615f9329..38db41cda841 100644 --- a/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.test.ts +++ b/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.test.ts @@ -20,18 +20,17 @@ vi.mock('picocolors', () => { }); const packageManagerMock = { - getAllDependencies: () => - Promise.resolve({ - '@storybook/addon-docs': '8.0.0', - }), + getAllDependencies: () => ({ + '@storybook/addon-docs': '8.0.0', + }), latestVersion: vi.fn(() => Promise.resolve('9.0.0')), - getPackageJSON: vi.fn(() => Promise.resolve('9.0.0')), -} as any as JsPackageManager; + getModulePackageJSON: vi.fn(() => ({ version: '9.0.0' })), +} as Partial as JsPackageManager; describe('checkPackageCompatibility', () => { it('returns that a package is incompatible', async () => { const packageName = 'my-storybook-package'; - vi.mocked(packageManagerMock.getPackageJSON).mockResolvedValueOnce({ + vi.mocked(packageManagerMock.getModulePackageJSON).mockReturnValueOnce({ name: packageName, version: '1.0.0', dependencies: { @@ -53,7 +52,7 @@ describe('checkPackageCompatibility', () => { it('returns that a package is compatible', async () => { const packageName = 'my-storybook-package'; - vi.mocked(packageManagerMock.getPackageJSON).mockResolvedValueOnce({ + vi.mocked(packageManagerMock.getModulePackageJSON).mockReturnValueOnce({ name: packageName, version: '1.0.0', dependencies: { @@ -76,7 +75,7 @@ describe('checkPackageCompatibility', () => { it('returns that a package is incompatible and because it is core, can be upgraded', async () => { const packageName = '@storybook/addon-docs'; - vi.mocked(packageManagerMock.getPackageJSON).mockResolvedValueOnce({ + vi.mocked(packageManagerMock.getModulePackageJSON).mockReturnValueOnce({ name: packageName, version: '8.0.0', dependencies: { @@ -102,7 +101,7 @@ describe('checkPackageCompatibility', () => { it('returns that an addon is incompatible because it uses legacy consolidated packages', async () => { const packageName = '@storybook/addon-designs'; - vi.mocked(packageManagerMock.getPackageJSON).mockResolvedValueOnce({ + vi.mocked(packageManagerMock.getModulePackageJSON).mockReturnValueOnce({ name: packageName, version: '8.0.0', dependencies: { @@ -127,7 +126,7 @@ describe('checkPackageCompatibility', () => { describe('getIncompatibleStorybookPackages', () => { it('returns an array of incompatible packages', async () => { - vi.mocked(packageManagerMock.getPackageJSON).mockResolvedValueOnce({ + vi.mocked(packageManagerMock.getModulePackageJSON).mockReturnValueOnce({ name: '@storybook/addon-docs', version: '8.0.0', dependencies: { diff --git a/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.ts b/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.ts index 085f2cc3f98c..08c5d51dd95b 100644 --- a/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.ts +++ b/code/lib/cli-storybook/src/doctor/getIncompatibleStorybookPackages.ts @@ -33,7 +33,7 @@ export const checkPackageCompatibility = async ( ): Promise => { const { currentStorybookVersion, skipErrors, packageManager } = context; try { - const dependencyPackageJson = await packageManager.getPackageJSON(dependency); + const dependencyPackageJson = packageManager.getModulePackageJSON(dependency); if (dependencyPackageJson === null) { return { packageName: dependency }; } @@ -103,7 +103,7 @@ export const getIncompatibleStorybookPackages = async ( ): Promise => { const packageManager = context.packageManager ?? JsPackageManagerFactory.getPackageManager(); - const allDeps = await packageManager.getAllDependencies(); + const allDeps = packageManager.getAllDependencies(); const storybookLikeDeps = Object.keys(allDeps).filter((dep) => dep.includes('storybook')); if (storybookLikeDeps.length === 0 && !context.skipErrors) { diff --git a/code/lib/cli-storybook/src/doctor/index.ts b/code/lib/cli-storybook/src/doctor/index.ts index fdcd909fbbcf..96f371f744a1 100644 --- a/code/lib/cli-storybook/src/doctor/index.ts +++ b/code/lib/cli-storybook/src/doctor/index.ts @@ -2,8 +2,9 @@ import { createWriteStream } from 'node:fs'; import { rename, rm } from 'node:fs/promises'; import { join } from 'node:path'; -import { JsPackageManagerFactory, prompt, temporaryFile } from 'storybook/internal/common'; -import type { PackageManagerName } from 'storybook/internal/common'; +import { prompt, temporaryFile } from 'storybook/internal/common'; +import type { JsPackageManager, PackageManagerName } from 'storybook/internal/common'; +import type { StorybookConfigRaw } from 'storybook/internal/types'; import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; @@ -64,17 +65,15 @@ export const doctor = async ({ logger.info('🩺 The doctor is checking the health of your Storybook..'); - const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); - let storybookVersion; - let mainConfig; + let storybookVersion: string | undefined; + let mainConfig: StorybookConfigRaw | undefined; + let packageManager!: JsPackageManager; try { - const storybookData = await getStorybookData({ + ({ storybookVersion, mainConfig, packageManager } = await getStorybookData({ configDir: userSpecifiedConfigDir, - packageManager, - }); - storybookVersion = storybookData.storybookVersion; - mainConfig = storybookData.mainConfig; + packageManagerName: pkgMgr, + })); } catch (err: any) { if (err.message.includes('No configuration files have been found')) { logger.info( @@ -99,7 +98,7 @@ export const doctor = async ({ throw new Error('mainConfig is undefined'); } - const allDependencies = (await packageManager.getAllDependencies()) as Record; + const allDependencies = packageManager.getAllDependencies() as Record; if (!('storybook' in allDependencies)) { logDiagnostic( diff --git a/code/lib/cli-storybook/src/migrate.ts b/code/lib/cli-storybook/src/migrate.ts index c3e4df7ea918..145d55052a85 100644 --- a/code/lib/cli-storybook/src/migrate.ts +++ b/code/lib/cli-storybook/src/migrate.ts @@ -1,15 +1,5 @@ -import { getStorybookVersionSpecifier } from 'storybook/internal/cli'; -import { - JsPackageManagerFactory, - getCoercedStorybookVersion, - getStorybookInfo, -} from 'storybook/internal/common'; - import { listCodemods, runCodemod } from '@storybook/codemod'; -import { runFixes } from './automigrate'; -import { getStorybookData } from './automigrate/helpers/mainConfigFile'; - const logger = console; type CLIOptions = { @@ -23,10 +13,7 @@ type CLIOptions = { parser?: 'babel' | 'babylon' | 'flow' | 'ts' | 'tsx'; }; -export async function migrate( - migration: any, - { glob, dryRun, list, rename, parser, configDir: userSpecifiedConfigDir }: CLIOptions -) { +export async function migrate(migration: any, { glob, dryRun, list, rename, parser }: CLIOptions) { if (list) { listCodemods().forEach((key: any) => logger.log(key)); } else if (migration) { diff --git a/code/lib/cli-storybook/src/sandbox.ts b/code/lib/cli-storybook/src/sandbox.ts index e40d6129dfc0..e1df93d1290b 100644 --- a/code/lib/cli-storybook/src/sandbox.ts +++ b/code/lib/cli-storybook/src/sandbox.ts @@ -215,7 +215,7 @@ export const sandbox = async ({ const before = process.cwd(); process.chdir(templateDestination); // we run doInitiate, instead of initiate, to avoid sending this init event to telemetry, because it's not a real world project - // @ts-expect-error (no types for this) + // @ts-expect-error - Fails on CI const { initiate } = await import('create-storybook'); await initiate({ dev: process.env.CI !== 'true' && process.env.IN_STORYBOOK_SANDBOX !== 'true', diff --git a/code/lib/cli-storybook/src/upgrade.test.ts b/code/lib/cli-storybook/src/upgrade.test.ts index c3b86bf10ac9..5b85a4c49569 100644 --- a/code/lib/cli-storybook/src/upgrade.test.ts +++ b/code/lib/cli-storybook/src/upgrade.test.ts @@ -21,8 +21,8 @@ vi.mock('storybook/internal/common', async (importOriginal) => { findInstallations: findInstallationsMock, getInstalledVersion: getInstalledVersionMock, latestVersion: async () => '8.0.0', - retrievePackageJson: async () => {}, - getAllDependencies: async () => ({ storybook: '8.0.0' }), + getAllDependencies: () => ({ storybook: '8.0.0' }), + getModulePackageJSON: vi.fn(), }), }, versions: Object.keys(originalModule.versions).reduce( diff --git a/code/lib/cli-storybook/src/upgrade.ts b/code/lib/cli-storybook/src/upgrade.ts index 37be1bc4d120..4c89e4a783c0 100644 --- a/code/lib/cli-storybook/src/upgrade.ts +++ b/code/lib/cli-storybook/src/upgrade.ts @@ -222,7 +222,11 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { yes, ...options } = allOptions; - const packageManager = JsPackageManagerFactory.getPackageManager({ force: packageManagerName }); + const { configDir, mainConfig, mainConfigPath, previewConfigPath, packageManager } = + await getStorybookData({ + configDir: userSpecifiedConfigDir, + packageManagerName, + }); // If we can't determine the existing version fallback to v0.0.0 to not block the upgrade const beforeVersion = (await getInstalledStorybookVersion(packageManager)) ?? '0.0.0'; @@ -233,7 +237,7 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { beforeVersion.startsWith('portal:') || beforeVersion.startsWith('workspace:'); - if (!(await hasStorybookDependencies(packageManager))) { + if (!hasStorybookDependencies(packageManager)) { throw new UpgradeStorybookInWrongWorkingDirectory(); } if (!isCanary && lt(currentCLIVersion, beforeVersion)) { @@ -269,7 +273,7 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { latestCLIVersionOnNPM )}! You likely ran the upgrade command through a remote command like npx, which can use a locally cached version. To upgrade to the latest version please run: - ${picocolors.bold(`${packageManager.getRemoteRunCommand()} storybook@latest upgrade`)} + ${picocolors.bold(`${packageManager.getRemoteRunCommand('storybook', ['upgrade'], 'latest')}`)} You may want to CTRL+C to stop, and run with the latest version instead. `), @@ -286,11 +290,7 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { let results; - const { configDir, mainConfig, mainConfigPath, previewConfigPath, packageJson } = - await getStorybookData({ - packageManager, - configDir: userSpecifiedConfigDir, - }); + const { packageJson } = packageManager.primaryPackageJson; // GUARDS if (!beforeVersion) { @@ -354,7 +354,7 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { packageJson.overrides.storybook = `$storybook`; if (!dryRun) { - await packageManager.writePackageJson(packageJson); + packageManager.writePackageJson(packageJson); logger.info( `Added Storybook overrides to ${picocolors.cyan( 'package.json' @@ -369,6 +369,7 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { // Users struggle to upgrade Storybook with npm because of conflicting peer-dependencies // GitHub Issue: https://github.com/storybookjs/storybook/issues/30306 // Solution: Remove all Storybook packages (except 'storybook') from the package.json and install them again + // TODO: Check if this is still needed due to the resolution fix for the `storybook` package if (packageManager.type === 'npm') { const getPackageName = (dep: string) => { const lastAtIndex = dep.lastIndexOf('@'); @@ -377,7 +378,6 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { // Remove all Storybook packages except 'storybook' await packageManager.removeDependencies( - { skipInstall: false }, [...upgradedDependencies, ...upgradedDevDependencies] .map(getPackageName) .filter((dep) => dep !== 'storybook') @@ -391,10 +391,16 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { const storybookDevDep = findStorybookPackage(upgradedDevDependencies); if (storybookDep) { - await packageManager.addDependencies({ installAsDevDependencies: false }, [storybookDep]); + await packageManager.addDependencies( + { installAsDevDependencies: false, skipInstall: true }, + [storybookDep] + ); } if (storybookDevDep) { - await packageManager.addDependencies({ installAsDevDependencies: true }, [storybookDevDep]); + await packageManager.addDependencies( + { installAsDevDependencies: true, skipInstall: true }, + [storybookDevDep] + ); } } @@ -403,7 +409,7 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { const addDeps = async (deps: string[], isDev: boolean) => { if (deps.length > 0) { await packageManager.addDependencies( - { installAsDevDependencies: isDev, skipInstall: true, packageJson }, + { installAsDevDependencies: isDev, skipInstall: true }, deps ); } @@ -448,6 +454,8 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { }); } + await packageManager.installDependencies(); + await doctor(allOptions); }; diff --git a/code/lib/create-storybook/src/generators/ANGULAR/index.ts b/code/lib/create-storybook/src/generators/ANGULAR/index.ts index 376ef8af5a08..769b6dee698f 100644 --- a/code/lib/create-storybook/src/generators/ANGULAR/index.ts +++ b/code/lib/create-storybook/src/generators/ANGULAR/index.ts @@ -58,9 +58,7 @@ const generator: Generator<{ projectName: string }> = async ( }); angularJSON.write(); - const packageJson = await packageManager.retrievePackageJson(); - const angularVersion = - packageJson.dependencies['@angular/core'] ?? packageJson.devDependencies['@angular/core']; + const angularVersion = packageManager.getDependencyVersion('@angular/core'); await baseGenerator( packageManager, diff --git a/code/lib/create-storybook/src/generators/REACT_NATIVE/index.ts b/code/lib/create-storybook/src/generators/REACT_NATIVE/index.ts index 38a715372815..23aa9d48df8e 100644 --- a/code/lib/create-storybook/src/generators/REACT_NATIVE/index.ts +++ b/code/lib/create-storybook/src/generators/REACT_NATIVE/index.ts @@ -3,12 +3,9 @@ import { SupportedLanguage } from '../../../../../core/src/cli/project_types'; import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { - const packageJson = await packageManager.retrievePackageJson(); + const missingReactDom = !packageManager.getDependencyVersion('react-dom'); - const missingReactDom = - !packageJson.dependencies['react-dom'] && !packageJson.devDependencies['react-dom']; - - const reactVersion = packageJson.dependencies.react; + const reactVersion = packageManager.getDependencyVersion('react'); const peerDependencies = [ 'react-native-safe-area-context', @@ -19,7 +16,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { 'react-native-gesture-handler', '@gorhom/bottom-sheet', 'react-native-svg', - ].filter((dep) => !packageJson.dependencies[dep] && !packageJson.devDependencies[dep]); + ].filter((dep) => !packageManager.getDependencyVersion(dep)); const packagesToResolve = [ ...peerDependencies, @@ -32,7 +29,8 @@ const generator: Generator = async (packageManager, npmOptions, options) => { const versionedPackages = await packageManager.getVersionedPackages(packagesToResolve); - const babelDependencies = await getBabelDependencies(packageManager as any, packageJson); + // TODO: Investigate why packageManager type does not match on CI + const babelDependencies = await getBabelDependencies(packageManager as any); const packages: string[] = []; @@ -46,7 +44,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { packages.push(`react-dom@${reactVersion}`); } - await packageManager.addDependencies({ ...npmOptions, packageJson }, packages); + await packageManager.addDependencies({ ...npmOptions }, packages); packageManager.addScripts({ 'storybook-generate': 'sb-rn-get-stories', diff --git a/code/lib/create-storybook/src/generators/REACT_SCRIPTS/index.ts b/code/lib/create-storybook/src/generators/REACT_SCRIPTS/index.ts index 6b02bc39798c..e62410406a94 100644 --- a/code/lib/create-storybook/src/generators/REACT_SCRIPTS/index.ts +++ b/code/lib/create-storybook/src/generators/REACT_SCRIPTS/index.ts @@ -26,7 +26,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { } : {}; - const craVersion = await packageManager.getPackageVersion('react-scripts'); + const craVersion = packageManager.getModulePackageJSON('react-scripts')?.version ?? null; if (craVersion === null) { throw new Error(dedent` diff --git a/code/lib/create-storybook/src/generators/baseGenerator.ts b/code/lib/create-storybook/src/generators/baseGenerator.ts index ab517dd06111..88b78f9c5256 100644 --- a/code/lib/create-storybook/src/generators/baseGenerator.ts +++ b/code/lib/create-storybook/src/generators/baseGenerator.ts @@ -309,7 +309,7 @@ export async function baseGenerator( ...extraAddons, ].filter(Boolean); - const packageJson = await packageManager.retrievePackageJson(); + const { packageJson } = packageManager.primaryPackageJson; const installedDependencies = new Set( Object.keys({ ...packageJson.dependencies, ...packageJson.devDependencies }) ); @@ -354,13 +354,15 @@ export async function baseGenerator( try { if (process.env.CI !== 'true') { const { hasEslint, isStorybookPluginInstalled, isFlatConfig, eslintConfigFile } = - await extractEslintInfo(packageManager); + // TODO: Investigate why packageManager type does not match on CI + await extractEslintInfo(packageManager as any); if (hasEslint && !isStorybookPluginInstalled) { packagesToInstall.push('eslint-plugin-storybook'); await configureEslintPlugin({ eslintConfigFile, - packageManager, + // TODO: Investigate why packageManager type does not match on CI + packageManager: packageManager as any, isFlatConfig, }); } @@ -380,7 +382,7 @@ export async function baseGenerator( text: 'Installing Storybook dependencies', }).start(); - await packageManager.addDependencies({ ...npmOptions, packageJson }, versionedPackages); + await packageManager.addDependencies({ ...npmOptions }, versionedPackages); addDependenciesSpinner.succeed(); } @@ -445,7 +447,7 @@ export async function baseGenerator( } if (addScripts) { - await packageManager.addStorybookCommandInScripts({ + packageManager.addStorybookCommandInScripts({ port: 6006, }); } diff --git a/code/lib/create-storybook/src/initiate.ts b/code/lib/create-storybook/src/initiate.ts index d572b0174495..c1e7a0aebccb 100644 --- a/code/lib/create-storybook/src/initiate.ts +++ b/code/lib/create-storybook/src/initiate.ts @@ -23,7 +23,7 @@ import type { JsPackageManager } from '../../../core/src/common/js-package-manag import { JsPackageManagerFactory } from '../../../core/src/common/js-package-manager/JsPackageManagerFactory'; import { HandledError } from '../../../core/src/common/utils/HandledError'; import { commandLog, paddedLog } from '../../../core/src/common/utils/log'; -import { getProjectRoot } from '../../../core/src/common/utils/paths'; +import { getProjectRoot, invalidateProjectRootCache } from '../../../core/src/common/utils/paths'; import versions from '../../../core/src/common/versions'; import { withTelemetry } from '../../../core/src/core-server/withTelemetry'; import { NxProjectDetectedError } from '../../../core/src/server-errors'; @@ -374,6 +374,24 @@ export async function doInitiate(options: CommandOptions): Promise< force: pkgMgr, }); + // Check if the current directory is empty. + if (options.force !== true && currentDirectoryIsEmpty(packageManager.type)) { + // Initializing Storybook in an empty directory with yarn1 + // will very likely fail due to different kind of hoisting issues + // which doesn't get fixed anymore in yarn1. + // We will fallback to npm in this case. + if (packageManager.type === 'yarn1') { + packageManager = JsPackageManagerFactory.getPackageManager({ force: 'npm' }); + } + // Prompt the user to create a new project from our list. + await scaffoldNewProject(packageManager.type, options); + invalidateProjectRootCache(); + } + + if (!options.skipInstall) { + await packageManager.installDependencies(); + } + const latestVersion = await packageManager.latestVersion('storybook'); const currentVersion = versions.storybook; const isPrerelease = prerelease(currentVersion); @@ -448,19 +466,6 @@ export async function doInitiate(options: CommandOptions): Promise< test: selectedFeatures.has('test'), }; - // Check if the current directory is empty. - if (options.force !== true && currentDirectoryIsEmpty(packageManager.type)) { - // Initializing Storybook in an empty directory with yarn1 - // will very likely fail due to different kind of hoisting issues - // which doesn't get fixed anymore in yarn1. - // We will fallback to npm in this case. - if (packageManager.type === 'yarn1') { - packageManager = JsPackageManagerFactory.getPackageManager({ force: 'npm' }); - } - // Prompt the user to create a new project from our list. - await scaffoldNewProject(packageManager.type, options); - } - let projectType: ProjectType; const projectTypeProvided = options.type; const infoText = projectTypeProvided @@ -501,6 +506,7 @@ export async function doInitiate(options: CommandOptions): Promise< projectType = manualType; } } catch (err) { + console.log(err); done(String(err)); throw new HandledError(err); } @@ -549,9 +555,7 @@ export async function doInitiate(options: CommandOptions): Promise< process.exit(0); } } - } - if (selectedFeatures.has('test')) { const vitestConfigFilesData = await vitestConfigFiles.condition( { babel, findUp, fs } as any, { directory: process.cwd() } as any @@ -576,11 +580,6 @@ export async function doInitiate(options: CommandOptions): Promise< } } } - - if (!options.skipInstall) { - await packageManager.installDependencies(); - } - // Update the options object with the selected features before passing it down to the generator options.features = Array.from(selectedFeatures); @@ -645,7 +644,7 @@ export async function doInitiate(options: CommandOptions): Promise< const storybookCommand = projectType === ProjectType.ANGULAR ? `ng run ${installResult.projectName}:storybook` - : packageManager.getRunStorybookCommand(); + : packageManager.getRunCommand('storybook'); if (selectedFeatures.has('test')) { logger.log( diff --git a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx index 69c895521948..9f3533dd356f 100644 --- a/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx +++ b/code/lib/create-storybook/src/ink/steps/checks/vitestConfigFiles.tsx @@ -3,6 +3,7 @@ import * as fs from 'node:fs/promises'; import { findUp } from 'find-up'; import * as babel from '../../../../../../core/src/babel'; +import { getProjectRoot } from '../../../../../../core/src/common/utils/paths'; import type { Check } from './Check'; import { CompatibilityType } from './CompatibilityType'; @@ -85,16 +86,17 @@ export const isValidWorkspaceConfigFile: (fileContents: string, babel: any) => b * - Yes -> ignore test intent * - No -> exit */ -const name = 'Vitest configuration'; export const vitestConfigFiles: Check = { condition: async (context, state) => { const deps = ['babel', 'findUp', 'fs']; if (babel && findUp && fs) { const reasons = []; + const projectRoot = getProjectRoot(); + const vitestWorkspaceFile = await findUp( ['ts', 'js', 'json'].flatMap((ex) => [`vitest.workspace.${ex}`, `vitest.projects.${ex}`]), - { cwd: state.directory } + { cwd: state.directory, stopAt: projectRoot } ); if (vitestWorkspaceFile?.endsWith('.json')) { reasons.push(`Cannot auto-update JSON workspace file: ${vitestWorkspaceFile}`); @@ -107,7 +109,7 @@ export const vitestConfigFiles: Check = { const vitestConfigFile = await findUp( ['ts', 'js', 'tsx', 'jsx', 'cts', 'cjs', 'mts', 'mjs'].map((ex) => `vitest.config.${ex}`), - { cwd: state.directory } + { cwd: state.directory, stopAt: projectRoot } ); if (vitestConfigFile?.endsWith('.cts') || vitestConfigFile?.endsWith('.cjs')) { reasons.push(`Cannot auto-update CommonJS config file: ${vitestConfigFile}`); diff --git a/code/presets/react-webpack/package.json b/code/presets/react-webpack/package.json index fece27c8c34d..8f2a8ac3067e 100644 --- a/code/presets/react-webpack/package.json +++ b/code/presets/react-webpack/package.json @@ -67,7 +67,7 @@ "@storybook/core-webpack": "workspace:*", "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", "@types/semver": "^7.3.4", - "find-up": "^5.0.0", + "find-up": "^7.0.0", "magic-string": "^0.30.5", "react-docgen": "^7.1.1", "resolve": "^1.22.8", diff --git a/code/presets/react-webpack/src/loaders/react-docgen-loader.ts b/code/presets/react-webpack/src/loaders/react-docgen-loader.ts index 49ddce41dbd3..a987244c624b 100644 --- a/code/presets/react-webpack/src/loaders/react-docgen-loader.ts +++ b/code/presets/react-webpack/src/loaders/react-docgen-loader.ts @@ -1,6 +1,7 @@ +import { getProjectRoot } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; -import findUp from 'find-up'; +import { findUp } from 'find-up'; import MagicString from 'magic-string'; import { ERROR_CODES, @@ -80,7 +81,10 @@ export default async function reactDocgenLoader( const { debug = false } = options; if (!tsconfigPathsInitialized) { - const tsconfigPath = await findUp('tsconfig.json', { cwd: process.cwd() }); + const tsconfigPath = await findUp('tsconfig.json', { + cwd: process.cwd(), + stopAt: getProjectRoot(), + }); const tsconfig = TsconfigPaths.loadConfig(tsconfigPath); if (tsconfig.resultType === 'success') { diff --git a/code/yarn.lock b/code/yarn.lock index 4a3e3ca4ea12..f2ead9091e17 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6121,7 +6121,7 @@ __metadata: "@types/node": "npm:^22.0.0" "@types/webpack-env": "npm:^1.18.0" fd-package-json: "npm:^1.2.0" - find-up: "npm:^5.0.0" + find-up: "npm:^7.0.0" rimraf: "npm:^6.0.1" telejson: "npm:8.0.0" ts-dedent: "npm:^2.0.0" @@ -6334,7 +6334,7 @@ __metadata: "@storybook/global": "npm:^5.0.0" babel-loader: "npm:9.1.3" ember-source: "npm:~3.28.1" - find-up: "npm:^5.0.0" + find-up: "npm:^7.0.0" typescript: "npm:^5.8.3" peerDependencies: "@babel/core": "*" @@ -6529,7 +6529,7 @@ __metadata: "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" "@types/node": "npm:^22.0.0" "@types/semver": "npm:^7.3.4" - find-up: "npm:^5.0.0" + find-up: "npm:^7.0.0" magic-string: "npm:^0.30.5" react-docgen: "npm:^7.1.1" resolve: "npm:^1.22.8" @@ -6626,7 +6626,7 @@ __metadata: "@storybook/builder-vite": "workspace:*" "@storybook/react": "workspace:*" "@types/node": "npm:^22.0.0" - find-up: "npm:^5.0.0" + find-up: "npm:^7.0.0" magic-string: "npm:^0.30.0" react-docgen: "npm:^8.0.0" resolve: "npm:^1.22.8" diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index 267e3d01d3e5..bbb2c7d45c1e 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -13,7 +13,6 @@ import { writeFile, writeJson, } from 'fs-extra'; -import { readFile } from 'fs/promises'; import JSON5 from 'json5'; import { createRequire } from 'module'; import { join, relative, resolve, sep } from 'path'; @@ -609,11 +608,10 @@ export async function addExtraDependencies({ return; } - const packageJson = JSON.parse(await readFile(join(cwd, 'package.json'), { encoding: 'utf8' })); - const packageManager = JsPackageManagerFactory.getPackageManager({}, cwd); + await packageManager.addDependencies( - { installAsDevDependencies: true, skipInstall: true, packageJson }, + { installAsDevDependencies: true, skipInstall: true }, extraDevDeps ); @@ -623,7 +621,7 @@ export async function addExtraDependencies({ logger.log('\uD83C\uDF81 Adding extra deps', versionedExtraDeps); } await packageManager.addDependencies( - { installAsDevDependencies: true, skipInstall: true, packageJson }, + { installAsDevDependencies: true, skipInstall: true }, versionedExtraDeps ); } diff --git a/scripts/utils/cli-step.ts b/scripts/utils/cli-step.ts index deb9e601aa33..9e2e3e9a03bf 100644 --- a/scripts/utils/cli-step.ts +++ b/scripts/utils/cli-step.ts @@ -126,6 +126,7 @@ export async function executeCLIStep( cwd: options.cwd, env: { STORYBOOK_DISABLE_TELEMETRY: 'true', + STORYBOOK_PROJECT_ROOT: options.cwd, ...options.env, }, },