Skip to content

Upgrade: Preserve package.json indentation when upgrading#32280

Merged
ndelangen merged 14 commits into
storybookjs:nextfrom
y-hsgw:fix/upgrade-package-json-indent
Dec 24, 2025
Merged

Upgrade: Preserve package.json indentation when upgrading#32280
ndelangen merged 14 commits into
storybookjs:nextfrom
y-hsgw:fix/upgrade-package-json-indent

Conversation

@y-hsgw
Copy link
Copy Markdown
Member

@y-hsgw y-hsgw commented Aug 16, 2025

Closes #25812

What I did

This PR fixes an issue where running storybook upgrade reformatted package.json by changing tab indentation to 2 spaces.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

  1. Change the indentation in react-vite-default-ts/package.json to tabs
  2. In the sandbox, add the following script to react-vite-default-ts/package.json (merge with existing scripts), then run it:
{
  "scripts": {
    // ...existing scripts
    "upgrade-storybook": "storybook upgrade"
  }
}
yarn upgrade-storybook

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr=<PR_NUMBER>

Greptile Summary

This PR addresses a user experience issue where the Storybook upgrade process was reformatting package.json files by converting tab indentation to 2-space indentation. The fix modifies the writePackageJson method in the JsPackageManager class to preserve the original indentation style of package.json files.

The change introduces the detect-indent package to analyze the existing package.json file's indentation pattern before writing the updated version. Instead of always using a hardcoded 2-space indentation (JSON.stringify(packageJsonToWrite, null, 2)), the code now:

  1. Reads the existing package.json file content
  2. Uses detect-indent to determine the current indentation style (tabs, 2 spaces, 4 spaces, etc.)
  3. Preserves that detected indentation when writing the updated package.json

This fix is part of the core package manager functionality that handles package.json modifications during Storybook upgrades. The JsPackageManager class is a foundational component used across different package managers (npm, yarn, pnpm) to handle package.json operations, making this fix universally applicable to all Storybook upgrade scenarios.

Confidence score: 4/5

  • This PR is safe to merge with low risk as it addresses a formatting preservation issue without changing core functionality
  • Score reflects a straightforward change using a well-established library (detect-indent) for a specific, well-defined problem
  • The JsPackageManager.ts file requires attention to ensure the detect-indent dependency is properly added to the package.json

Summary by CodeRabbit

Bug Fixes

  • Package.json files now retain their original indentation formatting when modified and written back to disk, ensuring consistency with the source file's style.

✏️ Tip: You can customize this high-level summary in your review settings.

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, no comments

Edit Code Review Bot Settings | Greptile

@valentinpalkovic valentinpalkovic changed the title fix: Preserve package.json indentation during Storybook upgrade Upgrade: Preserve package.json indentation when upgrading Aug 18, 2025
@storybook-app-bot
Copy link
Copy Markdown

storybook-app-bot Bot commented Aug 18, 2025

Package Benchmarks

Commit: 89a7f09, ran on 23 December 2025 at 10:00:14 UTC

No significant changes detected, all good. 👏

Comment thread code/core/src/common/js-package-manager/JsPackageManager.ts
@Sidnioulz
Copy link
Copy Markdown
Member

@y-hsgw when testing a locally built CLI on your branch, I am still seeing changes to package.json indentation. Could you please update testing instructions?

@ndelangen I'm gonna be OOO tonight and originally wanted to take this PR's review because I had feedback during the weekly triage. Is it ok if I give it to you so we don't make Yukihiro wait too long? 🙏

@y-hsgw
Copy link
Copy Markdown
Member Author

y-hsgw commented Aug 21, 2025

@Sidnioulz
Thanks for the heads-up. I’ve revised the testing steps and clarified how to keep tab indentation in package.json. Could you re-test with the updated instructions?

@y-hsgw y-hsgw requested a review from Sidnioulz August 21, 2025 14:10
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Aug 21, 2025

View your CI Pipeline Execution ↗ for commit 89a7f09


☁️ Nx Cloud last updated this comment at 2025-12-23 09:49:10 UTC

Copy link
Copy Markdown
Member

@Sidnioulz Sidnioulz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing using the local CLI from a sandbox:

cd code
yarn task compile
cd ../sandbox/cra-default-js
node ../../code/lib/cli-storybook/dist/bin/index.js upgrade

It seems to work for tabs and for e.g. 8 spaces, but not for 0 spaces. This is likely a limitation of the detect-indent package.

@Sidnioulz Sidnioulz assigned ndelangen and unassigned Sidnioulz Aug 21, 2025
@Sidnioulz
Copy link
Copy Markdown
Member

@ndelangen I've successfullyr reviwed and locally tested the PR. Leaving it in your hands as I don't have push permission!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 30, 2025

📝 Walkthrough

Walkthrough

Enhances JsPackageManager to detect and preserve indentation when reading and writing package.json files using the detect-indent library. Indentation metadata is stored non-enumerably on cached package.json objects using a Symbol key and applied dynamically during serialization.

Changes

Cohort / File(s) Change Summary
Indentation detection and preservation
code/core/src/common/js-package-manager/JsPackageManager.ts
Introduces detect-indent for reading original indentation; adds internal indentSymbol and PackageJsonWithIndent type; stores indent metadata on cached objects; adds private #getIndent() method; updates getPackageJson() and writePackageJson() to handle dynamic indentation; modifies static cache type and method return type

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Indentation metadata consistency: Verify that the indentSymbol-keyed metadata is correctly persisted across cache read/write cycles and doesn't cause unexpected enumeration issues
  • API signature changes: Confirm that changes to packageJsonCache type and getPackageJson() return type are propagated correctly throughout the codebase
  • Default indentation handling: Review the fallback logic in #getIndent() and confirm that the default indent of 2 is appropriate for all scenarios

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning This PR additionally includes unrelated modifications such as a whitespace-only test snapshot adjustment in CsfFile.test.ts, a type declaration change allowing symbol keys in PackageJson, formatting-only rewrites of Angular schema files, and a JSON syntax fix in the SvelteKit tsconfig, none of which pertain to preserving package.json indentation. Please remove or isolate the unrelated schema formatting, type declaration, test snapshot, and tsconfig changes into separate pull requests so this PR remains focused solely on preserving package.json indentation.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Upgrade: Preserve package.json indentation when upgrading” succinctly and accurately describes the primary change—modifying the upgrade process to retain existing package.json indentation—without extraneous detail or vagueness, making it clear to reviewers what the PR achieves.
Linked Issues Check ✅ Passed The changes in JsPackageManager.ts introduce and use detect-indent to read and apply the original indentation when writing package.json, directly fulfilling the objective from issue #25812 to prevent the upgrade command from altering existing indentation styles.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/core/src/common/js-package-manager/JsPackageManager.ts (1)

169-176: Use a namespaced global symbol and guard against spreading undefined dependency objects

  • Namespacing: Symbol.for('indent') is very generic and global. Prefer a namespaced key to avoid accidental collisions.
  • Safety: { ...packageJSON.dependencies } will throw if the field is undefined.

Apply this diff:

-    packageJSON[Symbol.for('indent')] = detectIndent(jsonContent).indent ?? 2;
+    const INDENT_SYM = Symbol.for('storybook:package-json:indent');
+    packageJSON[INDENT_SYM] = detectIndent(jsonContent).indent ?? 2;
@@
-    return {
-      ...packageJSON,
-      dependencies: { ...packageJSON.dependencies },
-      devDependencies: { ...packageJSON.devDependencies },
-      peerDependencies: { ...packageJSON.peerDependencies },
-    };
+    return {
+      ...packageJSON,
+      dependencies: { ...(packageJSON.dependencies ?? {}) },
+      devDependencies: { ...(packageJSON.devDependencies ?? {}) },
+      peerDependencies: { ...(packageJSON.peerDependencies ?? {}) },
+    };
🧹 Nitpick comments (2)
code/core/src/common/js-package-manager/JsPackageManager.ts (2)

179-186: Avoid unnecessary JSON parse: detect indent directly from file content

#getIndent can compute indentation from the file content without parsing JSON (cheaper and more robust when JSON is temporarily invalid).

-  #getIndent(filePath: string): string | number {
-    try {
-      const packageJson = JsPackageManager.getPackageJson(filePath);
-      return packageJson[Symbol.for('indent')];
-    } catch (e) {
-      return 2;
-    }
-  }
+  #getIndent(filePath: string): string | number {
+    try {
+      const src = readFileSync(filePath, 'utf8');
+      return detectIndent(src).indent ?? 2;
+    } catch {
+      return 2;
+    }
+  }

198-201: Minor: reuse filePath for write; add tests to lock behavior

  • Nit: you already compute filePath; use it for writeFileSync for consistency.
  • Please add tests covering tabs, 2/4-space, minified (no indent/empty string), missing-destination file (fallback to 2).
-    const filePath = join(directory, 'package.json');
+    const filePath = join(directory, 'package.json');
     const indent = this.#getIndent(filePath);
     const content = `${JSON.stringify(packageJsonToWrite, null, indent)}\n`;
-    writeFileSync(resolve(directory, 'package.json'), content, 'utf8');
+    writeFileSync(filePath, content, 'utf8');

Happy to draft a Jest test suite for these cases if useful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65b5dfc and 47851a0.

📒 Files selected for processing (8)
  • code/core/src/common/js-package-manager/JsPackageManager.ts (4 hunks)
  • code/core/src/csf-tools/CsfFile.test.ts (0 hunks)
  • code/core/src/outline/outlineCSS.ts (1 hunks)
  • code/core/src/types/modules/core-common.ts (1 hunks)
  • code/frameworks/angular/build-schema.json (5 hunks)
  • code/frameworks/angular/start-schema.json (6 hunks)
  • code/frameworks/sveltekit/tsconfig.json (1 hunks)
  • code/renderers/vue3/src/extractArgTypes.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • code/core/src/csf-tools/CsfFile.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Adhere to ESLint and Prettier rules across all JS/TS source files

Files:

  • code/core/src/types/modules/core-common.ts
  • code/core/src/common/js-package-manager/JsPackageManager.ts
  • code/renderers/vue3/src/extractArgTypes.ts
  • code/core/src/outline/outlineCSS.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Fix type errors and prefer precise typings instead of using any or suppressions, consistent with strict mode

Files:

  • code/core/src/types/modules/core-common.ts
  • code/core/src/common/js-package-manager/JsPackageManager.ts
  • code/renderers/vue3/src/extractArgTypes.ts
  • code/core/src/outline/outlineCSS.ts
code/**/tsconfig*.json

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep TypeScript strict mode enabled; do not relax strict compiler options in tsconfig files

Files:

  • code/frameworks/sveltekit/tsconfig.json
🧬 Code graph analysis (1)
code/core/src/outline/outlineCSS.ts (1)
scripts/utils/tools.ts (1)
  • dedent (118-118)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (5)
code/renderers/vue3/src/extractArgTypes.ts (1)

352-353: Doc example tweak looks good

Updated example formatting keeps the intent clear and consistent with surrounding docs.

code/core/src/outline/outlineCSS.ts (1)

7-7: Whitespace trim is harmless

Spacing cleanup around the dedent tag retains the same runtime behavior.

code/frameworks/sveltekit/tsconfig.json (1)

5-7: Valid JSON restored

Removing the trailing comma fixes the invalid JSON—thanks for tightening this up.

code/frameworks/angular/build-schema.json (1)

70-197: Schema formatting change looks good

Collapsing the array literals keeps the schema equivalent while matching the new formatting.

code/frameworks/angular/start-schema.json (1)

97-233: Consistent formatting maintained

The schema keeps its behavior; the tightened array formatting mirrors the build schema updates.

Comment thread code/core/src/common/js-package-manager/JsPackageManager.ts
Comment thread code/core/src/types/modules/core-common.ts Outdated
static getPackageJson(packageJsonPath: string): PackageJsonWithDepsAndDevDeps {
const jsonContent = readFileSync(packageJsonPath, 'utf8');
const packageJSON = JSON.parse(jsonContent);
packageJSON[Symbol.for('indent')] = detectIndent(jsonContent).indent ?? 2;
Copy link
Copy Markdown
Member

@ndelangen ndelangen Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels rather hacky to me. I get that it works, but it's very unexpected behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably gonna be less often wrong than it was before, and we use the same hack in the ESLint plugin so we're not increasing our maintenance surface wrt. dependencies.

I personally think it's nice to reformat a file after doing codegen on it, and this is likely as close as we can get without knowing what prettifier end users are using.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with this trick as long as it's clarified why this uses a Symbol. A code comment along the lines of "Using a Symbol key so JSON.stringify will omit it later" would suffice. It wasn't obvious to me that this is how stringify behaves, I had to manually verify.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the comment to dd0989b.

@valentinpalkovic
Copy link
Copy Markdown
Contributor

@y-hsgw

Thank you for your contribution! Could you please resolve the merge conflicts?

@y-hsgw
Copy link
Copy Markdown
Member Author

y-hsgw commented Dec 1, 2025

@valentinpalkovic
I’ve resolved the merge conflicts!
There are some unit test errors, but they seem unrelated to the changes in this PR.
Is there anything I should address regarding those errors within this PR?

@Sidnioulz
Copy link
Copy Markdown
Member

@y-hsgw thanks! I have re-run the failing tests. I also believe they are unrelated. If re-running them several times does not allow them to pass, then it will mean that it is caused by your code (though I fail to see how).

static getPackageJson(packageJsonPath: string): PackageJsonWithDepsAndDevDeps {
const jsonContent = readFileSync(packageJsonPath, 'utf8');
const packageJSON = JSON.parse(jsonContent);
packageJSON[Symbol.for('indent')] = detectIndent(jsonContent).indent ?? 2;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with this trick as long as it's clarified why this uses a Symbol. A code comment along the lines of "Using a Symbol key so JSON.stringify will omit it later" would suffice. It wasn't obvious to me that this is how stringify behaves, I had to manually verify.

#getIndent(filePath: string): string | number {
try {
const packageJson = JsPackageManager.getPackageJson(filePath);
return packageJson[Symbol.for('indent')];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to use Symbol.for. Just define it as a constant at the top of the file and reference that.

Suggested change
return packageJson[Symbol.for('indent')];
return packageJson[indentSymbol];

With at the top of the file:

const indentSymbol = Symbol('indent');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I fixed in 3830ed0.

}

export type PackageJson = PackageJsonFromTypeFest & Record<string, any>;
export type PackageJson = PackageJsonFromTypeFest & Record<string | symbol, any>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should be changing the type for something that's purely internal. Just extend PackageJsonWithDepsAndDevDeps where needed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed in a5da708.

coderabbitai[bot]

This comment was marked as spam.

@ndelangen ndelangen assigned ndelangen and unassigned ghengeveld Dec 22, 2025
@ndelangen ndelangen dismissed ghengeveld’s stale review December 24, 2025 09:13

Your review comment was:

I'm okay with this trick as long as it's clarified why this uses a Symbol. A code comment along the lines of "Using a Symbol key so JSON.stringify will omit it later" would suffice. It wasn't obvious to me that this is how stringify behaves, I had to manually verify.

As far as I can see, these 2 issues were fully resolved, so on that account I'm dismissing your review.

Since you're OOO for the next few days.

@ndelangen ndelangen merged commit 3510ad6 into storybookjs:next Dec 24, 2025
58 of 59 checks passed
@github-actions github-actions Bot mentioned this pull request Dec 24, 2025
22 tasks
@y-hsgw y-hsgw deleted the fix/upgrade-package-json-indent branch December 24, 2025 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: unexpected package.json format changes

5 participants