-
-
Notifications
You must be signed in to change notification settings - Fork 249
feat(rsc): validate client-only and server-only import during resolve
#624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
hi-ogawa
merged 11 commits into
main
from
copilot/fix-d57593a5-2c47-481f-9623-1a4fd3279339
Jul 26, 2025
Merged
Changes from 3 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
041884c
Initial plan
Copilot 48f0449
Implement validateImportPlugin for client-only and server-only valida…
Copilot deb502e
Add comprehensive unit tests for validateImportPlugin
Copilot 30c8fc0
Add e2e tests for validateImportPlugin functionality
Copilot 67f291f
Remove unit tests as requested, keep e2e tests
Copilot e762f57
Fix validateImportPlugin e2e tests and improve client component detec…
Copilot 85f1e63
fix: fix copilot
hi-ogawa 15ebd9d
test: valid imports
hi-ogawa 6abcb7e
test: simplify
hi-ogawa 08eda48
chore: tweak
hi-ogawa e8879d9
Merge branch 'main' into copilot/fix-d57593a5-2c47-481f-9623-1a4fd327…
hi-ogawa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
|
hi-ogawa marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Extract the validateImportPlugin function for testing | ||
| function validateImportPlugin() { | ||
| let isScanBuild = false | ||
|
|
||
| return { | ||
| name: 'rsc:validate-imports', | ||
| enforce: 'pre' as const, | ||
| resolveId(source: string, importer?: string, options?: { scan?: boolean }) { | ||
| // skip validation during optimizeDeps scan since for now | ||
| // we want to allow going through server/client boundary loosely | ||
| if (isScanBuild || (options && 'scan' in options && options.scan)) { | ||
| return | ||
| } | ||
|
|
||
| // Validate client-only imports in server environments | ||
| if ( | ||
| source === 'client-only' && | ||
| (this.environment.name === 'rsc' || this.environment.name === 'ssr') | ||
| ) { | ||
| throw new Error( | ||
| `'client-only' is included in server build (importer: ${importer ?? 'unknown'})`, | ||
| ) | ||
| } | ||
|
|
||
| // Validate server-only imports in client environment | ||
| if (source === 'server-only' && this.environment.name === 'client') { | ||
| throw new Error( | ||
| `'server-only' is included in client build (importer: ${importer ?? 'unknown'})`, | ||
| ) | ||
| } | ||
|
|
||
| return | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| describe('validateImportPlugin', () => { | ||
| it('should allow client-only imports in client environment', () => { | ||
| const plugin = validateImportPlugin() | ||
| const context = { environment: { name: 'client' } } | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(context, 'client-only', 'test.tsx', {}), | ||
| ).not.toThrow() | ||
| }) | ||
|
|
||
| it('should allow server-only imports in rsc environment', () => { | ||
| const plugin = validateImportPlugin() | ||
| const context = { environment: { name: 'rsc' } } | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(context, 'server-only', 'test.tsx', {}), | ||
| ).not.toThrow() | ||
| }) | ||
|
|
||
| it('should allow server-only imports in ssr environment', () => { | ||
| const plugin = validateImportPlugin() | ||
| const context = { environment: { name: 'ssr' } } | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(context, 'server-only', 'test.tsx', {}), | ||
| ).not.toThrow() | ||
| }) | ||
|
|
||
| it('should allow non-restricted imports in any environment', () => { | ||
| const plugin = validateImportPlugin() | ||
| const clientContext = { environment: { name: 'client' } } | ||
| const rscContext = { environment: { name: 'rsc' } } | ||
| const ssrContext = { environment: { name: 'ssr' } } | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(clientContext, 'react', 'test.tsx', {}), | ||
| ).not.toThrow() | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(rscContext, 'react', 'test.tsx', {}), | ||
| ).not.toThrow() | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(ssrContext, 'react', 'test.tsx', {}), | ||
| ).not.toThrow() | ||
| }) | ||
|
|
||
| it('should block client-only imports in rsc environment', () => { | ||
| const plugin = validateImportPlugin() | ||
| const context = { environment: { name: 'rsc' } } | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(context, 'client-only', 'test.tsx', {}), | ||
| ).toThrow("'client-only' is included in server build (importer: test.tsx)") | ||
| }) | ||
|
|
||
| it('should block client-only imports in ssr environment', () => { | ||
| const plugin = validateImportPlugin() | ||
| const context = { environment: { name: 'ssr' } } | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(context, 'client-only', 'test.tsx', {}), | ||
| ).toThrow("'client-only' is included in server build (importer: test.tsx)") | ||
| }) | ||
|
|
||
| it('should block server-only imports in client environment', () => { | ||
| const plugin = validateImportPlugin() | ||
| const context = { environment: { name: 'client' } } | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(context, 'server-only', 'test.tsx', {}), | ||
| ).toThrow("'server-only' is included in client build (importer: test.tsx)") | ||
| }) | ||
|
|
||
| it('should skip validation during scan mode', () => { | ||
| const plugin = validateImportPlugin() | ||
| const clientContext = { environment: { name: 'client' } } | ||
| const rscContext = { environment: { name: 'rsc' } } | ||
|
|
||
| // These should not throw even though they would normally be invalid | ||
| expect(() => | ||
| plugin.resolveId.call(clientContext, 'server-only', 'test.tsx', { | ||
| scan: true, | ||
| }), | ||
| ).not.toThrow() | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(rscContext, 'client-only', 'test.tsx', { | ||
| scan: true, | ||
| }), | ||
| ).not.toThrow() | ||
| }) | ||
|
|
||
| it('should handle missing importer gracefully', () => { | ||
| const plugin = validateImportPlugin() | ||
| const context = { environment: { name: 'client' } } | ||
|
|
||
| expect(() => | ||
| plugin.resolveId.call(context, 'server-only', undefined, {}), | ||
| ).toThrow("'server-only' is included in client build (importer: unknown)") | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.