Repository: storybookjs/storybook
File: code/core/src/common/js-package-manager/JsPackageManager.ts (TypeScript)

Code before:

      Object.assign(allDependencies, dependencies, devDependencies, peerDependencies);
    }

    return allDependencies;
  }

  isDependencyInstalled(dependency: string) {
    return Object.keys(this.getAllDependencies()).includes(dependency);
  }

  /**
   * Add dependencies to a project using `yarn add` or `npm install`.
   *
   * @example
   *
   * ```ts
   * addDependencies(options, [
   *   `@storybook/react@${storybookVersion}`,
   *   `@storybook/addon-links@${linksVersion}`,
   * ]);
   * ```
   *
   * @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 add.
   */
  public async addDependencies(
    options:
      | {
          skipInstall: true;
          type: 'dependencies' | 'devDependencies' | 'peerDependencies';
          writeOutputToFile?: boolean;
          packageJsonInfo?: PackageJsonInfo;
        }
      | {
          skipInstall?: false;
          type: 'dependencies' | 'devDependencies';
          writeOutputToFile?: boolean;
          packageJsonInfo?: PackageJsonInfo;
        },
    dependencies: string[]
  ): Promise<void | ExecaChildProcess> {
    const {
      skipInstall,
      writeOutputToFile = true,
      packageJsonInfo = this.primaryPackageJson,
    } = options;

    if (skipInstall) {
      const { operationDir, packageJson } = packageJsonInfo;

Review comment (bug):
It's potentially risky to read the local package.json, because scripts could use `writePackageJson` to move a package.json to a new location.

You could make the code more robust locally by encapsulating the indent computation in a try catch (and defaulting to 2 on catch). This way, for all use cases where we modify a packageJson in place, it'd work; and for other use cases, it wouldn't crash.

If you're up for it, you could also compute indentation in the code paths that _load_ packageJsons and add it to the object with a Symbol (e.g. `[Symbol.for('indent')]: detectIndent(...)`). You could then read that indent symbol to preserve indentation on existing files. It wouldn't be added to the resulting files as symbols aren't enumerable by default.

Code after:
    // Store in cache
    JsPackageManager.packageJsonCache.set(absolutePath, result);

    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;

    // Remove empty dependency objects
    dependencyTypes.forEach((type) => {
      if (packageJsonToWrite[type] && Object.keys(packageJsonToWrite[type]).length === 0) {
        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, 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: PackageJsonWithIndent = {
      ...packageJsonToWrite,
      dependencies: { ...(packageJsonToWrite.dependencies || {}) },
      devDependencies: { ...(packageJsonToWrite.devDependencies || {}) },
      peerDependencies: { ...(packageJsonToWrite.peerDependencies || {}) },
    };
    cachedPackageJson[indentSymbol] = indent;
    JsPackageManager.packageJsonCache.set(packageJsonPath, cachedPackageJson);
  }

  getAllDependencies() {
    const allDependencies: Record<string, string> = {};

    for (const packageJsonPath of this.packageJsonPaths) {
      const packageJson = JsPackageManager.getPackageJson(packageJsonPath);
      const { dependencies, devDependencies, peerDependencies } = packageJson;

      Object.assign(allDependencies, dependencies, devDependencies, peerDependencies);