diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts index bd5bd4b542d6..72e97894a055 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.ts @@ -3,6 +3,7 @@ import { dirname, isAbsolute, join, normalize, resolve } from 'node:path'; import { logger, prompt } from 'storybook/internal/node-logger'; +import detectIndent from 'detect-indent'; import * as find from 'empathic/find'; // eslint-disable-next-line depend/ban-dependencies import { type ExecaChildProcess } from 'execa'; @@ -29,6 +30,12 @@ export enum PackageManagerName { type StorybookPackage = keyof typeof storybookPackagesVersions; +const indentSymbol = Symbol('indent'); + +type PackageJsonWithIndent = PackageJsonWithDepsAndDevDeps & { + [indentSymbol]?: any; +}; + /** * Extract package name and version from input * @@ -85,7 +92,7 @@ export abstract class JsPackageManager { static readonly installedVersionCache = new Map(); /** Cache for package.json files to avoid repeated file system calls. */ - static readonly packageJsonCache = new Map(); + static readonly packageJsonCache = new Map(); constructor(options?: JsPackageManagerOptions) { this.cwd = options?.cwd || process.cwd(); @@ -164,7 +171,7 @@ export abstract class JsPackageManager { } /** Read the `package.json` file available in the provided directory */ - static getPackageJson(packageJsonPath: string): PackageJsonWithDepsAndDevDeps { + static getPackageJson(packageJsonPath: string): PackageJsonWithIndent { // Normalize path to absolute for consistent cache keys // Always use resolve() to ensure consistent format on Windows // (handles drive letter casing and path separator differences) @@ -181,8 +188,10 @@ export abstract class JsPackageManager { // Read from disk if not in cache const jsonContent = readFileSync(absolutePath, 'utf8'); const packageJSON = JSON.parse(jsonContent); + // Symbol key keeps this metadata non-enumerable so JSON.stringify omits it + packageJSON[indentSymbol] = detectIndent(jsonContent).indent ?? 2; - const result: PackageJsonWithDepsAndDevDeps = { + const result: PackageJsonWithIndent = { ...packageJSON, dependencies: { ...(packageJSON.dependencies || {}) }, devDependencies: { ...(packageJSON.devDependencies || {}) }, @@ -195,6 +204,15 @@ export abstract class JsPackageManager { return result; } + #getIndent(filePath: string): string | number { + try { + const packageJson = JsPackageManager.getPackageJson(filePath); + return packageJson[indentSymbol]; + } catch (e) { + return 2; + } + } + writePackageJson(packageJson: PackageJson, directory = this.cwd) { const packageJsonToWrite = { ...packageJson }; const dependencyTypes = ['dependencies', 'devDependencies', 'peerDependencies'] as const; @@ -205,19 +223,21 @@ export abstract class JsPackageManager { delete packageJsonToWrite[type]; } }); - + const filePath = join(directory, 'package.json'); + const indent = this.#getIndent(filePath); const packageJsonPath = normalize(resolve(directory, 'package.json')); - const content = `${JSON.stringify(packageJsonToWrite, null, 2)}\n`; + const content = `${JSON.stringify(packageJsonToWrite, null, indent)}\n`; writeFileSync(packageJsonPath, content, 'utf8'); // Update cache with the written content // Ensure dependencies and devDependencies exist (even if empty) to match PackageJsonWithDepsAndDevDeps type - const cachedPackageJson: PackageJsonWithDepsAndDevDeps = { + const cachedPackageJson: PackageJsonWithIndent = { ...packageJsonToWrite, dependencies: { ...(packageJsonToWrite.dependencies || {}) }, devDependencies: { ...(packageJsonToWrite.devDependencies || {}) }, peerDependencies: { ...(packageJsonToWrite.peerDependencies || {}) }, }; + cachedPackageJson[indentSymbol] = indent; JsPackageManager.packageJsonCache.set(packageJsonPath, cachedPackageJson); }