Skip to content

UI: Improve syntax-highlighter bundling#32776

Merged
ndelangen merged 4 commits into
storybookjs:nextfrom
mrginglymus:no-syntax-highlighter-barrel
Oct 21, 2025
Merged

UI: Improve syntax-highlighter bundling#32776
ndelangen merged 4 commits into
storybookjs:nextfrom
mrginglymus:no-syntax-highlighter-barrel

Conversation

@mrginglymus
Copy link
Copy Markdown
Contributor

@mrginglymus mrginglymus commented Oct 20, 2025

What I did

Ensured no side-effect import of syntaxhighlighter from storybook/internal/components barrel file

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

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

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 publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • Performance

    • Syntax highlighting now loads lazily to improve initial page load performance.
  • Refactor

    • Clipboard copy logic reorganized into a dedicated module with robust feature-detection and a non-clipboard fallback to improve cross-browser reliability and maintainability.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 20, 2025

📝 Walkthrough

Walkthrough

Extracts clipboard logic into a new clipboard.ts module and updates imports to use it; SyntaxHighlighter delegates copy behavior to the new function. The Code component now lazy-loads SyntaxHighlighter. Export paths in the components index are adjusted accordingly.

Changes

Cohort / File(s) Summary
Clipboard module
code/core/src/components/components/syntaxhighlighter/clipboard.ts
New file exporting createCopyToClipboardFunction that feature-detects navigator.clipboard.writeText and falls back to a textarea-based document.execCommand('copy') approach with error handling.
SyntaxHighlighter update
code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx
Removed local createCopyToClipboardFunction implementation; now imports and uses the function from the new clipboard module. Adjusted global object extraction to use globalWindow.
Lazy-load SyntaxHighlighter
code/core/src/components/components/typography/elements/Code.tsx
Switched import from .../syntaxhighlighter/syntaxhighlighter to .../syntaxhighlighter/lazy-syntaxhighlighter to enable lazy loading.
Re-export path change
code/core/src/components/index.ts
Updated export source of createCopyToClipboardFunction to ./components/syntaxhighlighter/clipboard (previously exported from syntaxhighlighter).

Sequence Diagram(s)

sequenceDiagram
  participant UI as UI (copy button)
  participant SH as SyntaxHighlighter
  participant CB as createCopyToClipboardFunction
  participant Win as globalWindow / top
  participant Doc as document (via Storybook globals)

  UI->>SH: user clicks copy
  SH->>CB: invoke copy function with text
  CB->>Win: check navigator?.clipboard
  alt navigator.clipboard available
    CB->>Win: try top.navigator.clipboard.writeText(text)
    alt succeeds
      CB-->>SH: resolved (success)
    else fails
      CB->>Win: fallback to globalWindow.navigator.clipboard.writeText(text)
      CB-->>SH: resolved/rejected
    end
  else navigator.clipboard unavailable
    CB->>Doc: create textarea, set value, append
    CB->>Doc: select & document.execCommand('copy')
    CB->>Doc: remove textarea, restore focus
    CB-->>SH: resolved/rejected
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff2239 and 61894dc.

📒 Files selected for processing (1)
  • code/core/src/components/components/syntaxhighlighter/clipboard.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/src/components/components/syntaxhighlighter/clipboard.ts
⏰ 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

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

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Oct 20, 2025

View your CI Pipeline Execution ↗ for commit 61894dc

Command Status Duration Result
nx run-many -t build --parallel=3 ✅ Succeeded 46s View ↗

☁️ Nx Cloud last updated this comment at 2025-10-21 13:52:00 UTC

@ndelangen ndelangen changed the title fix(perf): Ensure imports only from lazy-syntaxhighlighter UI: Improve syntax-highlighter bundling Oct 21, 2025
@ndelangen ndelangen self-assigned this Oct 21, 2025
@ndelangen ndelangen added bug ui ci:normal Run our default set of CI jobs (choose this for most PRs). labels Oct 21, 2025
@storybook-app-bot
Copy link
Copy Markdown

storybook-app-bot Bot commented Oct 21, 2025

Package Benchmarks

Commit: 61894dc, ran on 21 October 2025 at 13:45:30 UTC

The following packages have significant changes to their size or dependencies:

storybook

Before After Difference
Dependency count 43 43 0
Self size 30.19 MB 30.14 MB 🎉 -51 KB 🎉
Dependency size 17.36 MB 17.36 MB 0 B
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 187 187 0
Self size 927 KB 927 KB 🚨 +84 B 🚨
Dependency size 79.97 MB 79.92 MB 🎉 -51 KB 🎉
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 169 169 0
Self size 35 KB 35 KB 🚨 +42 B 🚨
Dependency size 76.40 MB 76.35 MB 🎉 -51 KB 🎉
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 44 44 0
Self size 1.55 MB 1.55 MB 🎉 -30 B 🎉
Dependency size 47.55 MB 47.50 MB 🎉 -51 KB 🎉
Bundle Size Analyzer node node

@ndelangen ndelangen marked this pull request as ready for review October 21, 2025 13:10
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf268ca and 2ff2239.

📒 Files selected for processing (4)
  • code/core/src/components/components/syntaxhighlighter/clipboard.ts (1 hunks)
  • code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx (1 hunks)
  • code/core/src/components/components/typography/elements/Code.tsx (1 hunks)
  • code/core/src/components/index.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,json,html,ts,tsx,mjs}

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

**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier formatting on changed files before committing
Run ESLint on changed files and fix all errors/warnings before committing (use yarn lint:js:cmd <file>)

Files:

  • code/core/src/components/components/typography/elements/Code.tsx
  • code/core/src/components/components/syntaxhighlighter/clipboard.ts
  • code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx
  • code/core/src/components/index.ts
**/*.{ts,tsx,js,jsx,mjs}

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

Export functions from modules when they need to be unit-tested

Files:

  • code/core/src/components/components/typography/elements/Code.tsx
  • code/core/src/components/components/syntaxhighlighter/clipboard.ts
  • code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx
  • code/core/src/components/index.ts
code/**/*.{ts,tsx,js,jsx,mjs}

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

In application code, use Storybook loggers instead of console.* (client code: storybook/internal/client-logger; server code: storybook/internal/node-logger)

Files:

  • code/core/src/components/components/typography/elements/Code.tsx
  • code/core/src/components/components/syntaxhighlighter/clipboard.ts
  • code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx
  • code/core/src/components/index.ts
{code/**,scripts/**}/**/*.{ts,tsx,js,jsx,mjs}

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

Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/core/src/components/components/typography/elements/Code.tsx
  • code/core/src/components/components/syntaxhighlighter/clipboard.ts
  • code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx
  • code/core/src/components/index.ts
🧬 Code graph analysis (1)
code/core/src/components/components/syntaxhighlighter/clipboard.ts (1)
code/core/src/components/index.ts (1)
  • createCopyToClipboardFunction (40-40)
⏰ 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 (4)
code/core/src/components/components/typography/elements/Code.tsx (1)

6-6: LGTM! Lazy-loading path correctly applied.

The import path change to lazy-syntaxhighlighter enables lazy-loading of the SyntaxHighlighter component, which aligns with the PR objective to improve bundling and avoid side-effect imports.

code/core/src/components/index.ts (1)

40-40: LGTM! Export path updated to new clipboard module.

The export path correctly points to the new dedicated clipboard module while preserving the public API surface.

code/core/src/components/components/syntaxhighlighter/syntaxhighlighter.tsx (2)

29-29: LGTM! Correctly imports from new clipboard module.

The import change properly delegates clipboard functionality to the dedicated clipboard module, supporting the modularization effort.


36-36: LGTM! Proper usage of extracted clipboard function.

The removal of document from the global destructuring (line 36) is correct since it's no longer needed in this file. Line 60 correctly creates the copyToClipboard function using the imported createCopyToClipboardFunction.

Also applies to: 60-60

Comment on lines +7 to +13
return async (text: string) => {
try {
await globalWindow.top?.navigator.clipboard.writeText(text);
} catch {
await globalWindow.navigator.clipboard.writeText(text);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix silent failure when top is nullish.

If globalWindow.top is nullish, the optional chaining on line 9 returns undefined. Awaiting undefined succeeds immediately without actually writing to the clipboard, causing the function to silently fail.

Apply this diff to properly handle the fallback:

-    try {
-      await globalWindow.top?.navigator.clipboard.writeText(text);
-    } catch {
-      await globalWindow.navigator.clipboard.writeText(text);
-    }
+    try {
+      if (globalWindow.top?.navigator.clipboard) {
+        await globalWindow.top.navigator.clipboard.writeText(text);
+      } else {
+        await globalWindow.navigator.clipboard.writeText(text);
+      }
+    } catch {
+      await globalWindow.navigator.clipboard.writeText(text);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return async (text: string) => {
try {
await globalWindow.top?.navigator.clipboard.writeText(text);
} catch {
await globalWindow.navigator.clipboard.writeText(text);
}
};
return async (text: string) => {
try {
if (globalWindow.top?.navigator.clipboard) {
await globalWindow.top.navigator.clipboard.writeText(text);
} else {
await globalWindow.navigator.clipboard.writeText(text);
}
} catch {
await globalWindow.navigator.clipboard.writeText(text);
}
};
🤖 Prompt for AI Agents
In code/core/src/components/components/syntaxhighlighter/clipboard.ts around
lines 7 to 13, the optional chaining on
globalWindow.top?.navigator.clipboard.writeText can return undefined and be
awaited (no-op) when top is nullish; change the implementation to first check
whether top.navigator.clipboard.writeText is a callable function and call/await
it if present, otherwise call/await
globalWindow.navigator.clipboard.writeText(text) as the fallback, and keep the
try/catch to handle runtime exceptions.

Comment on lines +15 to +26
return async (text: string) => {
const tmp = document.createElement('TEXTAREA') as HTMLTextAreaElement;
const focus = document.activeElement as HTMLTextAreaElement;

tmp.value = text;

document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
focus.focus();
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add null check for activeElement before calling focus().

Line 17 assigns document.activeElement to focus, which can be null. Line 25 calls focus.focus() without checking, which will throw a TypeError if activeElement was null.

Apply this diff to add a null check:

   return async (text: string) => {
     const tmp = document.createElement('TEXTAREA') as HTMLTextAreaElement;
     const focus = document.activeElement as HTMLTextAreaElement;
 
     tmp.value = text;
 
     document.body.appendChild(tmp);
     tmp.select();
     document.execCommand('copy');
     document.body.removeChild(tmp);
-    focus.focus();
+    focus?.focus();
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return async (text: string) => {
const tmp = document.createElement('TEXTAREA') as HTMLTextAreaElement;
const focus = document.activeElement as HTMLTextAreaElement;
tmp.value = text;
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
focus.focus();
};
return async (text: string) => {
const tmp = document.createElement('TEXTAREA') as HTMLTextAreaElement;
const focus = document.activeElement as HTMLTextAreaElement;
tmp.value = text;
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
focus?.focus();
};
🤖 Prompt for AI Agents
In code/core/src/components/components/syntaxhighlighter/clipboard.ts around
lines 15 to 26, document.activeElement is assigned to focus which may be null;
add a null check before calling focus() to avoid a TypeError. Update the code to
only call focus.focus() when focus is non-null and is an element with a focus
method (e.g., check if focus !== null and typeof (focus as HTMLElement).focus
=== 'function'), so restore focus safely after removing the temporary textarea.

…se separate async functions for clipboard API and fallback method
@ndelangen ndelangen merged commit beddf9d into storybookjs:next Oct 21, 2025
54 checks passed
@github-actions github-actions Bot mentioned this pull request Oct 21, 2025
12 tasks
@mrginglymus mrginglymus deleted the no-syntax-highlighter-barrel branch October 24, 2025 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug ci:normal Run our default set of CI jobs (choose this for most PRs). ui

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants