Builder-Vite: Add plugin to enforce Storybook's output directory in Vite build configuration#33740
Conversation
…ite build configuration
|
View your CI Pipeline Execution ↗ for commit 4c506c2
☁️ Nx Cloud last updated this comment at |
📝 WalkthroughWalkthroughAdds a Vite plugin named "storybook:enforce-output-dir" that runs with enforce: 'post' and sets build.outDir to options.outputDir via both a config hook and a configEnvironment hook (Vite 6) to enforce output directory in all config phases. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@code/builders/builder-vite/src/build.ts`:
- Around line 56-63: The plugin object currently includes the Vite 6+ hook
configEnvironment which causes TypeScript errors under Vite 5; update the plugin
creation to be type-safe by either casting the plugin object to PluginOption
(import PluginOption from 'vite') or by only adding configEnvironment when Vite
>= 6 at runtime, or use the TypeScript satisfies PluginOption assertion; target
the object that defines configEnvironment (the anonymous plugin passed into
InlineConfig.plugins) and apply one of these approaches so TypeScript accepts
the plugin across the supported Vite versions.
🧹 Nitpick comments (1)
code/builders/builder-vite/src/build.ts (1)
43-64: Simplify theconfighook to match the existing pattern.The
confighook unnecessarily spreads the entire config object. Vite's config hook deep-merges the returned value, so you only need to return the properties you want to override. This is inconsistent with thestorybook:define-envplugin pattern used just below (lines 69-75), which correctly returns only the partial config.♻️ Proposed simplification
finalConfig.plugins?.push({ name: 'storybook:enforce-output-dir', enforce: 'post', - config: (config) => ({ - ...config, + config: () => ({ build: { outDir: options.outputDir, }, }), // configEnvironment is a new method in Vite 6 // It is used to configure configs based on the environment // E.g. Nitro uses this method to set the output directory to .output/public/ configEnvironment: () => ({ build: { outDir: options.outputDir, }, }), });
| // configEnvironment is a new method in Vite 6 | ||
| // It is used to configure configs based on the environment | ||
| // E.g. Nitro uses this method to set the output directory to .output/public/ | ||
| configEnvironment: () => ({ | ||
| build: { | ||
| outDir: options.outputDir, | ||
| }, | ||
| }), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's look at the actual code in the file
cat -n code/builders/builder-vite/src/build.ts | sed -n '50,70p'Repository: storybookjs/storybook
Length of output: 783
🏁 Script executed:
#!/bin/bash
# Check the Vite version in package.json
find . -name "package.json" -path "*/builder-vite/*" | head -1 | xargs cat | grep -A2 -B2 '"vite"'Repository: storybookjs/storybook
Length of output: 346
🏁 Script executed:
#!/bin/bash
# Search for how configEnvironment is used across the codebase
rg "configEnvironment" --type ts --type tsx -B2 -A2Repository: storybookjs/storybook
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Check if there are type definitions or interface declarations related to configEnvironment
rg "Plugin.*interface\|type.*Plugin\|configEnvironment" --type ts --type tsx -B3 -A3 | head -80Repository: storybookjs/storybook
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Search for configEnvironment without file type restrictions
rg "configEnvironment" -B2 -A2Repository: storybookjs/storybook
Length of output: 680
🏁 Script executed:
#!/bin/bash
# Check if there's a tsconfig or any TypeScript configuration
find code/builders/builder-vite -name "tsconfig*.json" -o -name "*.d.ts" | head -10Repository: storybookjs/storybook
Length of output: 105
🏁 Script executed:
#!/bin/bash
# Look at the actual Plugin type usage in the file
head -30 code/builders/builder-vite/src/build.tsRepository: storybookjs/storybook
Length of output: 1218
🏁 Script executed:
#!/bin/bash
# Check Vite's plugin type definition from type stubs
find . -path "*/node_modules/vite/dist/node/*.d.ts" -o -path "*/vite/types/*" 2>/dev/null | head -5 | xargs grep -l "Plugin\|configEnvironment" 2>/dev/null | head -3Repository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look for any type casting or 'as' statements related to plugins
rg "as.*Plugin\|Plugin.*as\|satisfies.*Plugin" --type tsRepository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look at the full context where the plugin is created and returned
cat -n code/builders/builder-vite/src/build.ts | sed -n '40,90p'Repository: storybookjs/storybook
Length of output: 2231
🏁 Script executed:
#!/bin/bash
# Check the commonConfig to see how plugins are handled
cat -n code/builders/builder-vite/src/vite-config.ts | head -100Repository: storybookjs/storybook
Length of output: 4170
🏁 Script executed:
#!/bin/bash
# Check what version of Vite types are being used by looking at Vite's plugin definition
node -e "const vite = require('vite'); console.log(Object.keys(vite));" 2>/dev/null || echo "Node import failed"Repository: storybookjs/storybook
Length of output: 83
🏁 Script executed:
#!/bin/bash
# Look for any type definitions or extends in the builder-vite directory
find code/builders/builder-vite -type f \( -name "*.ts" \) -exec grep -l "Plugin\|plugin\|configEnvironment" {} \; | head -5Repository: storybookjs/storybook
Length of output: 830
🏁 Script executed:
#!/bin/bash
# Check the actual Vite package.json to see the types version
find . -path "*/node_modules/vite/package.json" | head -1 | xargs cat | grep -A5 -B5 "version\|types"Repository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for how PluginOption type is used with plugins containing configEnvironment
rg "PluginOption\|Plugin\>" code/builders/builder-vite/src/build.ts -B5 -A5Repository: storybookjs/storybook
Length of output: 173
🏁 Script executed:
#!/bin/bash
# Check if there's a types package or any type augmentation for Vite
find code/builders/builder-vite -name "*.d.ts" -o -name "types.ts" | xargs cat 2>/dev/nullRepository: storybookjs/storybook
Length of output: 653
🏁 Script executed:
#!/bin/bash
# Look for any TypeScript errors or build issues by checking if the project compiles
# Check the tsconfig for strict mode settings
cat code/builders/builder-vite/tsconfig.jsonRepository: storybookjs/storybook
Length of output: 155
🏁 Script executed:
#!/bin/bash
# Check if configEnvironment is documented in Vite's types by checking the actual Plugin interface
python3 << 'EOF'
import json
import subprocess
# Try to find and read the Vite type definitions
try:
result = subprocess.run(['find', '.', '-path', '*/node_modules/vite/dist/node/index.d.ts'],
capture_output=True, text=True, timeout=5)
if result.stdout.strip():
print("Found Vite types at:")
print(result.stdout)
# Read the file
with open(result.stdout.strip().split('\n')[0], 'r') as f:
content = f.read()
# Look for Plugin interface or configEnvironment
if 'configEnvironment' in content:
print("\nconfigEnvironment found in Vite types")
# Print surrounding lines
lines = content.split('\n')
for i, line in enumerate(lines):
if 'configEnvironment' in line:
start = max(0, i-2)
end = min(len(lines), i+3)
print('\n'.join(lines[start:end]))
else:
print("\nconfigEnvironment NOT found in main Vite types")
except Exception as e:
print(f"Error: {e}")
EOFRepository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look for any .d.ts files in vite module
find . -path "*/node_modules/vite/*" -name "*.d.ts" 2>/dev/null | head -20Repository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Check if there are any build errors or TypeScript issues by looking at the actual return type
cat code/builders/builder-vite/src/build.ts | sed -n '47,77p'Repository: storybookjs/storybook
Length of output: 861
🏁 Script executed:
#!/bin/bash
# Check if there's a satisfies clause or as cast for the plugin
rg "satisfies|as.*Plugin" code/builders/builder-vite/src/build.ts -B2 -A2Repository: storybookjs/storybook
Length of output: 877
🏁 Script executed:
#!/bin/bash
# Look for any comment or TODO related to type issues with configEnvironment
rg "configEnvironment|TODO|FIXME|type" code/builders/builder-vite/src/build.tsRepository: storybookjs/storybook
Length of output: 299
🏁 Script executed:
#!/bin/bash
# Check what PluginOption actually includes by looking at how it's imported and used
python3 << 'EOF'
import subprocess
import os
# Search for PluginOption type definition
result = subprocess.run(['rg', 'type PluginOption|interface PluginOption', '--type', 'ts'],
capture_output=True, text=True, cwd='.')
print("PluginOption definition search:")
print(result.stdout if result.stdout else "Not found in codebase")
print(result.stderr if result.stderr else "")
# Try to find the Vite node types
result2 = subprocess.run(['find', '.', '-type', 'f', '-path', '*/vite/*', '-name', 'index.d.ts'],
capture_output=True, text=True, timeout=10)
print("\nVite type files found:")
print(result2.stdout)
EOFRepository: storybookjs/storybook
Length of output: 144
🏁 Script executed:
#!/bin/bash
# Let's check if there are any existing TypeScript errors by looking at the root tsconfig
cat tsconfig.json | head -50Repository: storybookjs/storybook
Length of output: 110
🏁 Script executed:
#!/bin/bash
# Search in vite-config.ts for how PluginOption is used
cat code/builders/builder-vite/src/vite-config.ts | sed -n '1,30p'Repository: storybookjs/storybook
Length of output: 767
🏁 Script executed:
#!/bin/bash
# Check the actual Plugin type being used - look at the imports more carefully
head -20 code/builders/builder-vite/src/build.tsRepository: storybookjs/storybook
Length of output: 852
🏁 Script executed:
#!/bin/bash
# Let's examine what InlineConfig expects for plugins
python3 << 'EOF'
import subprocess
# Check if we can find any type information about configEnvironment in node_modules
result = subprocess.run(['find', '.', '-path', '*/node_modules/vite/dist/node/*.d.ts', '-type', 'f'],
capture_output=True, text=True, timeout=10)
files = result.stdout.strip().split('\n')
print(f"Found {len([f for f in files if f])} Vite type definition files")
# Try to read the plugin definition if we can find it
for f in files[:5]:
if f and 'plugin' in f.lower():
print(f"\nReading {f}:")
try:
with open(f, 'r') as file:
content = file.read(2000)
if 'configEnvironment' in content:
print("Found configEnvironment!")
print(content[:1000])
except Exception as e:
print(f"Error: {e}")
EOFRepository: storybookjs/storybook
Length of output: 99
🏁 Script executed:
#!/bin/bash
# Check if this is actually compiled without errors by looking at build outputs
find code/builders/builder-vite -name "dist" -o -name "build" | head -5Repository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Let's check the Vite release notes or see what version introduced configEnvironment
# by looking at comments in the code
rg "Vite [0-9]|configEnvironment" code/builders/builder-vite/src/ -B1 -A1Repository: storybookjs/storybook
Length of output: 556
🏁 Script executed:
#!/bin/bash
# Check if there's any explicit type casting for the plugin object
cat code/builders/builder-vite/src/build.ts | sed -n '45,80p'Repository: storybookjs/storybook
Length of output: 1089
🏁 Script executed:
#!/bin/bash
# Look for any type definitions or extensions related to Vite plugin compatibility
rg "declare|extend|augment" code/builders/builder-vite/src/ --type ts -B2 -A2Repository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Check if there's a satisfies keyword or any inline type assertion
cat code/builders/builder-vite/src/build.ts | grep -E "satisfies|as |type |interface" | head -20Repository: storybookjs/storybook
Length of output: 477
🏁 Script executed:
#!/bin/bash
# Let's check if Vite 5 is actually being used/tested in this codebase
rg "vite.*5\|version.*5" code/builders/builder-vite/ --type ts --type jsonRepository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/antml/bash
# Check what the actual pushed plugin object looks like - if it has type issues it should show
python3 << 'EOF'
# Read the build.ts file and check if the plugin push statement is typed
with open('code/builders/builder-vite/src/build.ts', 'r') as f:
lines = f.readlines()
# Look at lines around 47-64 where plugin is pushed
for i in range(46, min(65, len(lines))):
print(f"{i+1:3d}: {lines[i]}", end='')
# Check if there's a 'satisfies' or 'as' keyword
content = ''.join(lines)
if 'satisfies' in content:
print("\n\nFound 'satisfies' keyword in file")
else:
print("\n\nNo 'satisfies' keyword found - object is untyped")
EOFRepository: storybookjs/storybook
Length of output: 717
🏁 Script executed:
#!/bin/bash
# Check the package.json more carefully to understand version constraints
cat code/builders/builder-vite/package.json | grep -A5 -B5 "vite"Repository: storybookjs/storybook
Length of output: 1130
🏁 Script executed:
#!/bin/bash
# Check if there's any version detection or conditional logic for Vite versions
rg "version|Vite 5|Vite 6|semver" code/builders/builder-vite/src/build.ts -B2 -A2Repository: storybookjs/storybook
Length of output: 278
🏁 Script executed:
#!/bin/bash
# Check if the plugin object should have a type annotation or if other plugins use one
cat code/builders/builder-vite/src/build.ts | sed -n '66,77p'Repository: storybookjs/storybook
Length of output: 354
🏁 Script executed:
#!/bin/bash
# See how other Storybook builders handle version-specific features
rg "configEnvironment" code/builders/ -B5 -A5 | head -40Repository: storybookjs/storybook
Length of output: 1010
🏁 Script executed:
#!/bin/bash
# Check Vite 5 compatibility - does the code have any workarounds?
rg "TODO|FIXME|hack|workaround" code/builders/builder-vite/src/build.tsRepository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Check what happens with the plugin object type - look at the complete function
tail -50 code/builders/builder-vite/src/build.tsRepository: storybookjs/storybook
Length of output: 1679
🏁 Script executed:
#!/bin/bash
# Verify if the issue is actually causing TypeScript errors by looking at any build logs
find code/builders/builder-vite -name "*.log" -o -name "tsc-out*" -o -name "typescript*" 2>/dev/nullRepository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Check if the plugin object should be typed as PluginOption to catch the type issue
python3 << 'EOF'
import subprocess
# Search for how PluginOption is actually defined in vite-config.ts vs build.ts
result = subprocess.run(['rg', 'push.*plugin', 'code/builders/builder-vite/src/', '--type', 'ts', '-B2', '-A8'],
capture_output=True, text=True)
print("Plugin push statements:")
print(result.stdout)
EOFRepository: storybookjs/storybook
Length of output: 1399
🏁 Script executed:
#!/bin/bash
# Check what happens when we look at the types - does InlineConfig.plugins accept the plugin object?
python3 << 'EOF'
# The key question: is the plugin object properly typed?
# InlineConfig.plugins should be of type PluginOption[]
# configEnvironment is only in Vite 6+, so Vite 5 wouldn't have it
# Let's see what the actual issue would be
with open('code/builders/builder-vite/src/build.ts', 'r') as f:
content = f.read()
# Check if they import PluginOption
if 'PluginOption' in content:
print("PluginOption is imported in build.ts")
else:
print("PluginOption is NOT imported in build.ts - this is the issue!")
# Check if they use 'as' or 'satisfies'
lines = content.split('\n')
for i, line in enumerate(lines[46:65], start=47):
if 'as' in line or 'satisfies' in line:
print(f"Line {i}: Found type assertion: {line.strip()}")
print("\nThe plugin object is pushed without explicit typing,")
print("but finalConfig.plugins is of type PluginOption[] (from InlineConfig)")
print("configEnvironment doesn't exist in Vite 5, so TypeScript would error")
EOFRepository: storybookjs/storybook
Length of output: 410
🏁 Script executed:
#!/bin/bash
# Confirm: does Vite 5 have configEnvironment hook?
web_search "Vite 5 configEnvironment hook plugin"Repository: storybookjs/storybook
Length of output: 115
🌐 Web query:
Vite 5 vs Vite 6 configEnvironment plugin hook
💡 Result:
-
Vite 5: there is no
configEnvironmentplugin hook. Plugins can only influence configuration globally via hooks likeconfig/configResolved, and users can branch config by command/mode and whether it’s an SSR build (e.g.isSsrBuild) in the config function. [3] -
Vite 6:
configEnvironment(name, options)is a new plugin hook added with the (experimental) Environment API. It’s called once per environment (e.g.client,ssr, and any custom environments) with that environment’s partially resolved options (including final defaults), and you can return a partial config to merge for just that environment. [1], [2]- Typical pattern in Vite 6: set broad defaults in
config, then fine-tune per-environment inconfigEnvironment. [1]
- Typical pattern in Vite 6: set broad defaults in
Example (Vite 6):
export default function myPlugin() {
return {
name: 'my-plugin',
configEnvironment(name, options) {
if (name === 'rsc') {
return { resolve: { conditions: ['workerd'] } }
}
},
}
}[1]
Practical takeaway: if you need “client vs ssr (or more) specific config” inside a plugin, that’s exactly what configEnvironment enables in Vite 6; in Vite 5 you had to approximate it with global config hooks and SSR/build branching rather than a per-environment config phase. [1], [3]
References:
[1] Vite “Environment API for Plugins” (configEnvironment) https://main.vitejs.dev/guide/api-environment-plugins
[2] Vite “Environment API” overview (Vite 6 environments) https://vite.dev/guide/api-environment
[3] Vite 5 “Configuring Vite” (conditional config incl. isSsrBuild) https://v5.vite.dev/config/
🏁 Script executed:
#!/bin/bash
# Verify if there are any tests or CI that would catch this TypeScript issue
find code/builders/builder-vite -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "configEnvironment" 2>/dev/nullRepository: storybookjs/storybook
Length of output: 49
🏁 Script executed:
#!/bin/bash
# Check if there's a type guard or version check for this feature
rg "version|semver" code/builders/builder-vite/src/build.tsRepository: storybookjs/storybook
Length of output: 47
🏁 Script executed:
#!/antml/bash
# Most importantly - check if this code actually compiles without errors
# by looking at what type the plugin object has when pushed
python3 << 'EOF'
# The issue is clear:
# 1. finalConfig is typed as InlineConfig (from Vite)
# 2. InlineConfig.plugins is of type PluginOption[] | undefined
# 3. PluginOption in Vite 5 doesn't include configEnvironment
# 4. PluginOption in Vite 6+ does include configEnvironment
# 5. The code pushes a plugin with configEnvironment without type annotation
# This would cause a TypeScript error in Vite 5
print("ANALYSIS:")
print("=========")
print("1. build.ts does NOT import PluginOption type")
print("2. Plugin object is pushed to finalConfig.plugins without type annotation")
print("3. The plugin object includes 'configEnvironment' property")
print("4. configEnvironment is Vite 6+ feature only")
print("5. Package.json supports Vite ^5.0.0 || ^6.0.0 || ^7.0.0")
print()
print("PROBLEM:")
print("When using Vite 5, TypeScript would report:")
print(" Property 'configEnvironment' does not exist on type 'Plugin'")
print()
print("SOLUTIONS:")
print("1. Add explicit type cast: (plugin as PluginOption)")
print("2. Use 'satisfies PluginOption' to validate")
print("3. Add version check to only include configEnvironment for Vite 6+")
print("4. Import and use PluginOption type for the object")
EOFRepository: storybookjs/storybook
Length of output: 708
Add type safety to configEnvironment hook or implement version-aware inclusion.
The configEnvironment hook is a Vite 6+ feature that will cause TypeScript type errors when the code runs with Vite 5 (which is in peer dependencies: ^5.0.0 || ^6.0.0 || ^7.0.0). The plugin object at lines 47–64 includes configEnvironment without type annotation, and InlineConfig.plugins expects PluginOption[].
When Vite 5 is used, TypeScript will report: "Property 'configEnvironment' does not exist on type 'Plugin'".
Either:
- Cast the plugin object:
{ ... } as PluginOption(import from 'vite') - Conditionally include
configEnvironmentonly for Vite 6+ by checking the Vite version at runtime - Use
satisfies PluginOptionfor type validation
🤖 Prompt for AI Agents
In `@code/builders/builder-vite/src/build.ts` around lines 56 - 63, The plugin
object currently includes the Vite 6+ hook configEnvironment which causes
TypeScript errors under Vite 5; update the plugin creation to be type-safe by
either casting the plugin object to PluginOption (import PluginOption from
'vite') or by only adding configEnvironment when Vite >= 6 at runtime, or use
the TypeScript satisfies PluginOption assertion; target the object that defines
configEnvironment (the anonymous plugin passed into InlineConfig.plugins) and
apply one of these approaches so TypeScript accepts the plugin across the
supported Vite versions.
…-build-output Builder-Vite: Add plugin to enforce Storybook's output directory in Vite build configuration (cherry picked from commit d349bf8)
Closes #33738
What I did
Fixes an issue where Storybook build artifacts are incorrectly redirected to Nitro's output directory (.output/public/) instead of the expected Storybook output directory (storybook-static) when using TanStack Start projects.
Checklist for Contributors
Testing
The changes in this PR are covered in the following automated tests:
Manual testing
[ ] Create a new TanStack Start project
[ ] Add Storybook using npx storybook@latest init
[ ] Run npx storybook build
[ ] Verify that storybook-static contains the sb-preview folder
[ ] Verify that .output/public/ does NOT contain Storybook's preview assets
[ ] Run Chromatic CLI to verify the build is valid
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