Vite: Use vite hook filter for performance improvements#33900
Vite: Use vite hook filter for performance improvements#33900huang-julien wants to merge 3 commits into
Conversation
📝 WalkthroughWalkthroughThis PR refactors Vite plugin implementations across the codebase to adopt a structured API for transform, resolveId, and load hooks. Instead of direct function implementations, plugins now use objects with Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~28 minutes Possibly related PRs
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
code/frameworks/react-vite/src/plugins/react-docgen.ts (1)
82-82: Use===instead of==fordefinedInFilecomparison.
definedInFile == iduses loose equality. Since both values are strings, this works, but strict equality (===) is the standard practice in TypeScript codebases and avoids subtle type-coercion surprises.Proposed fix
- if (actualName && definedInFile == id) { + if (actualName && definedInFile === id) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@code/frameworks/react-vite/src/plugins/react-docgen.ts` at line 82, Replace the loose equality in the react-docgen plugin conditional: change the comparison in the if statement that checks actualName and definedInFile against id from `definedInFile == id` to strict equality `definedInFile === id`; update the condition `if (actualName && definedInFile == id)` to use `===` so the check involving actualName, definedInFile and id is type-safe.code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts (1)
126-128: Static analysis flags ReDoS on dynamically constructed RegExp — low risk here.The
namevariable originates fromchecker.getExportNames(id), which yields JS identifiers (not user-supplied input), so the practical ReDoS risk is negligible. This is pre-existing code, not introduced by this PR, but worth noting for future hardening — e.g., escapingnamewith a regex-escape utility before interpolation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts` around lines 126 - 128, The dynamic RegExp construction using the identifier variable name (from checker.getExportNames(id)) can cause a ReDoS concern; update the two RegExp constructions that interpolate ${name} (the ones testing src with patterns like `export {.*${name}.*}` and `export \* from ['"]\S*${name}['"]`) to escape any regex-special characters in name before interpolation (e.g., use a shared escapeRegExp utility or a safe escape function) so the generated patterns are safe even if name contains metacharacters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@code/builders/builder-vite/src/plugins/code-generator-plugin.ts`:
- Around line 79-103: The load filter currently only matches the virtual IDs
(via filter: { id: /\0virtual:\/@storybook\/builder-vite\// }) so the iframeId
branch in the handler is never invoked; replace the filter+handler pair with a
single load(id) function (or broaden the filter to include iframeId) that
explicitly checks id against getResolvedVirtualModuleId(SB_VIRTUAL_FILES.*) and
iframeId (which is set in configResolved) before returning content—update the
plugin to use load(id) { switch(id) { case getResolvedVirtualModuleId(...): ...;
case iframeId: ... } } so iframe.html is actually served.
In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts`:
- Around line 110-112: Remove the duplicated comment "if there is no component
meta, return undefined" in vue-component-meta.ts so only a single instance
remains; locate the repeated comment near the component meta handling logic in
the plugin (the block that checks for component meta) and delete the redundant
line to avoid duplicate comments while preserving the single explanatory
comment.
In `@code/frameworks/vue3-vite/src/plugins/vue-docgen.ts`:
- Around line 13-14: Update the Vite transform hook's filter to use the
documented object format: instead of passing the RegExp directly as `id:
include`, wrap it as `id: { include }` so the transform config matches the
expected `{ id: { include, exclude } }` shape (mirror the usage in
svelte-docgen's transform filter); modify the `transform` config where `filter`
is defined and replace `filter: { id: include }` with `filter: { id: { include }
}`.
---
Nitpick comments:
In `@code/frameworks/react-vite/src/plugins/react-docgen.ts`:
- Line 82: Replace the loose equality in the react-docgen plugin conditional:
change the comparison in the if statement that checks actualName and
definedInFile against id from `definedInFile == id` to strict equality
`definedInFile === id`; update the condition `if (actualName && definedInFile ==
id)` to use `===` so the check involving actualName, definedInFile and id is
type-safe.
In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts`:
- Around line 126-128: The dynamic RegExp construction using the identifier
variable name (from checker.getExportNames(id)) can cause a ReDoS concern;
update the two RegExp constructions that interpolate ${name} (the ones testing
src with patterns like `export {.*${name}.*}` and `export \* from
['"]\S*${name}['"]`) to escape any regex-special characters in name before
interpolation (e.g., use a shared escapeRegExp utility or a safe escape
function) so the generated patterns are safe even if name contains
metacharacters.
| load: { | ||
| filter: { id: /\0virtual:\/@storybook\/builder-vite\// }, | ||
| async handler(id) { | ||
| switch (id) { | ||
| case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_STORIES_FILE): { | ||
| const storyIndexGenerator = await storyIndexGeneratorPromise; | ||
| const index = await storyIndexGenerator?.getIndex(); | ||
| return generateImportFnScriptCode(index); | ||
| } | ||
|
|
||
| case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_ADDON_SETUP_FILE): { | ||
| return generateAddonSetupCode(); | ||
| } | ||
| case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_APP_FILE): { | ||
| return generateModernIframeScriptCode(options, projectRoot); | ||
| case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_ADDON_SETUP_FILE): { | ||
| return generateAddonSetupCode(); | ||
| } | ||
| case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_APP_FILE): { | ||
| return generateModernIframeScriptCode(options, projectRoot); | ||
| } | ||
| case iframeId: { | ||
| return readFileSync( | ||
| fileURLToPath(importMetaResolve('@storybook/builder-vite/input/iframe.html')), | ||
| 'utf-8' | ||
| ); | ||
| } | ||
| } | ||
| case iframeId: { | ||
| return readFileSync( | ||
| fileURLToPath(importMetaResolve('@storybook/builder-vite/input/iframe.html')), | ||
| 'utf-8' | ||
| ); | ||
| } | ||
| } | ||
| }, | ||
| }, |
There was a problem hiding this comment.
load filter excludes iframeId — the case iframeId branch is unreachable.
The filter at line 80 only matches IDs containing \0virtual:/@storybook/builder-vite/, but iframeId is set to ${config.root}/iframe.html (line 64), which doesn't contain that prefix. Vite's native filter will skip calling the handler for iframeId entirely, so lines 95–100 are dead code and the iframe HTML content will never be served by this plugin.
The filter needs to also match iframeId. Since iframeId is only known after configResolved, consider broadening the filter:
Proposed fix
load: {
- filter: { id: /\0virtual:\/@storybook\/builder-vite\// },
+ filter: { id: [/\0virtual:\/@storybook\/builder-vite\//, /iframe\.html/] },
async handler(id) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@code/builders/builder-vite/src/plugins/code-generator-plugin.ts` around lines
79 - 103, The load filter currently only matches the virtual IDs (via filter: {
id: /\0virtual:\/@storybook\/builder-vite\// }) so the iframeId branch in the
handler is never invoked; replace the filter+handler pair with a single load(id)
function (or broaden the filter to include iframeId) that explicitly checks id
against getResolvedVirtualModuleId(SB_VIRTUAL_FILES.*) and iframeId (which is
set in configResolved) before returning content—update the plugin to use
load(id) { switch(id) { case getResolvedVirtualModuleId(...): ...; case
iframeId: ... } } so iframe.html is actually served.
| // if there is no component meta, return undefined | ||
|
|
||
| const hasEvent = meta.events.find((event) => event.name === nameWithoutOnPrefix); | ||
| return !hasEvent; | ||
| }) | ||
| // remove unwanted duplicated "$slots" expose | ||
| .filter((expose) => { | ||
| if (expose.name === '$slots') { | ||
| const slotNames = meta.slots.map((slot) => slot.name); | ||
| return !slotNames.every((slotName) => expose.type.includes(slotName)); | ||
| } | ||
| return true; | ||
| }); | ||
| // if there is no component meta, return undefined |
There was a problem hiding this comment.
Duplicate comment.
Line 112 is an exact duplicate of line 110.
Proposed fix
// if there is no component meta, return undefined
-
- // if there is no component meta, return undefined
if (metaSources.length === 0) {📝 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.
| // if there is no component meta, return undefined | |
| const hasEvent = meta.events.find((event) => event.name === nameWithoutOnPrefix); | |
| return !hasEvent; | |
| }) | |
| // remove unwanted duplicated "$slots" expose | |
| .filter((expose) => { | |
| if (expose.name === '$slots') { | |
| const slotNames = meta.slots.map((slot) => slot.name); | |
| return !slotNames.every((slotName) => expose.type.includes(slotName)); | |
| } | |
| return true; | |
| }); | |
| // if there is no component meta, return undefined | |
| // if there is no component meta, return undefined | |
| if (metaSources.length === 0) { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts` around lines 110
- 112, Remove the duplicated comment "if there is no component meta, return
undefined" in vue-component-meta.ts so only a single instance remains; locate
the repeated comment near the component meta handling logic in the plugin (the
block that checks for component meta) and delete the redundant line to avoid
duplicate comments while preserving the single explanatory comment.
2a92ec8 to
2a76f88
Compare
Package BenchmarksCommit: The following packages have significant changes to their size or dependencies:
|
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 188 | 185 | 🎉 -3 🎉 |
| Self size | 76 KB | 76 KB | 0 B |
| Dependency size | 32.23 MB | 32.18 MB | 🎉 -46 KB 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/angular
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 188 | 185 | 🎉 -3 🎉 |
| Self size | 139 KB | 139 KB | 0 B |
| Dependency size | 30.44 MB | 30.40 MB | 🎉 -46 KB 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/ember
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 192 | 189 | 🎉 -3 🎉 |
| Self size | 15 KB | 15 KB | 0 B |
| Dependency size | 28.94 MB | 28.89 MB | 🎉 -46 KB 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/react-webpack5
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 274 | 271 | 🎉 -3 🎉 |
| Self size | 24 KB | 24 KB | 0 B |
| Dependency size | 44.60 MB | 44.56 MB | 🎉 -46 KB 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/server-webpack5
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 200 | 197 | 🎉 -3 🎉 |
| Self size | 16 KB | 16 KB | 0 B |
| Dependency size | 33.48 MB | 33.44 MB | 🎉 -46 KB 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/preset-react-webpack
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 170 | 167 | 🎉 -3 🎉 |
| Self size | 18 KB | 18 KB | 0 B |
| Dependency size | 31.44 MB | 31.39 MB | 🎉 -46 KB 🎉 |
| Bundle Size Analyzer | Link | Link |
|
Cloosing as started to work on wrong branch |
Closes #33899
What I did
This PR moves Vite plugins to use
HookObjects instead of functions.For example
transform(code, id)would move toThis allows to reduce the overhead between JS/Rust runtimes for vite 8 with rolldown. Implementation within handlers doesn't change,
filter()are left within to keep plugins backward compatible with vite < 6.3Checklist for Contributors
Testing
The changes in this PR are covered in the following automated tests:
Manual testing
Caution
This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!
Documentation
MIGRATION.MD
Checklist for Maintainers
When this PR is ready for testing, make sure to add
ci:normal,ci:mergedorci:dailyGH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found incode/lib/cli-storybook/src/sandbox-templates.tsMake 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/coreteam 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