-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix: import protection robustness #6746
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { createFileRoute } from '@tanstack/react-router' | ||
| import { createServerFn } from '@tanstack/react-start' | ||
| // Import from the barrel — NOT directly from .server. | ||
| // `getUsers` (from ./db.server) is only used inside a server fn (compiler strips it). | ||
| // `userColumns` (from ./shared) is a plain object used in JSX — not server-only. | ||
| // Tree-shaking should eliminate the ./db.server dependency entirely from the | ||
| // client bundle, so no import-protection violation should fire for it. | ||
| import { getUsers, userColumns, type User } from '../violations/barrel-reexport' | ||
|
|
||
| const fetchUsers = createServerFn().handler(async () => { | ||
| return getUsers() | ||
| }) | ||
|
|
||
| export const Route = createFileRoute('/barrel-false-positive')({ | ||
| loader: () => fetchUsers(), | ||
| component: BarrelFalsePositive, | ||
| }) | ||
|
|
||
| function BarrelFalsePositive() { | ||
| const users = Route.useLoaderData() | ||
|
|
||
| return ( | ||
| <div> | ||
| <h1 data-testid="barrel-heading">Barrel False Positive</h1> | ||
| <table> | ||
| <thead> | ||
| <tr> | ||
| <th data-testid="col-name">{userColumns.name}</th> | ||
| <th data-testid="col-email">{userColumns.email}</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {users.map((user: User) => ( | ||
| <tr key={user.id}> | ||
| <td>{user.name}</td> | ||
| <td>{user.email}</td> | ||
| </tr> | ||
| ))} | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { createFileRoute } from '@tanstack/react-router' | ||
| // This import is used ONLY inside the route component. | ||
| // The router plugin code-splits the component into a lazy chunk, | ||
| // moving this import into the split module. | ||
| // Import protection must still catch this as a violation. | ||
| import { getComponentSecret } from '../violations/db-credentials.server' | ||
|
|
||
| export const Route = createFileRoute('/component-server-leak')({ | ||
| component: ComponentServerLeak, | ||
| }) | ||
|
|
||
| function ComponentServerLeak() { | ||
| return ( | ||
| <div> | ||
| <h1 data-testid="component-leak-heading">Component Server Leak</h1> | ||
| <p data-testid="component-leak-secret">{getComponentSecret()}</p> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Server-only database module. | ||||||||||||||||||||
| * Only exports server-only values: getUsers (DB access) and User type. | ||||||||||||||||||||
| * The side-effect (DATABASE_URL log) should NOT reach the client. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const DATABASE_URL = | ||||||||||||||||||||
| process.env.DATABASE_URL ?? 'postgres://admin:s3cret@localhost:5432/myapp' | ||||||||||||||||||||
|
|
||||||||||||||||||||
| console.log(`[db] connecting to ${DATABASE_URL}`) | ||||||||||||||||||||
|
Comment on lines
+7
to
+10
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid logging credentials — use a redacted connection string in the side-effect log. The default fallback on line 8 embeds plaintext credentials (
Since the only requirement of the side-effect is that it be detectable (i.e., proves the module was loaded), logging a redacted or static string is sufficient. 🔒 Proposed fix-console.log(`[db] connecting to ${DATABASE_URL}`)
+// Log a redacted URL so credentials are never emitted even if the env var is real.
+console.log(`[db] connecting to ${DATABASE_URL.replace(/:\/\/[^@]+@/, '://<redacted>@')}`)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| export interface User { | ||||||||||||||||||||
| id: number | ||||||||||||||||||||
| name: string | ||||||||||||||||||||
| email: string | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const FAKE_USERS: Array<User> = [ | ||||||||||||||||||||
| { id: 1, name: 'Alice', email: 'alice@example.com' }, | ||||||||||||||||||||
| { id: 2, name: 'Bob', email: 'bob@example.com' }, | ||||||||||||||||||||
| ] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export async function getUsers(): Promise<Array<User>> { | ||||||||||||||||||||
| await new Promise((r) => setTimeout(r, 50)) | ||||||||||||||||||||
| return FAKE_USERS | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * Server-only module protected by the marker import pattern. | ||
| * Re-exported through the barrel (index.ts) but never imported by | ||
| * barrel-false-positive.tsx — tree-shaking eliminates it from the | ||
| * client bundle, so no import-protection violation should fire. | ||
| */ | ||
| import '@tanstack/react-start/server-only' | ||
|
|
||
| export function foo(): string { | ||
| return 'server-only value from foo' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /** | ||
| * Barrel file — NOT a .server file itself. | ||
| * Re-exports from a .server module, a marker-protected module, and a shared module. | ||
| * | ||
| * The key scenario: a consumer imports { getUsers, userColumns } from here. | ||
| * - getUsers comes from ./db.server (server-only via file suffix) | ||
| * - foo comes from ./foo (server-only via marker import) | ||
| * - userColumns comes from ./shared (client-safe) | ||
| * | ||
| * If getUsers is only used inside a createServerFn handler (compiler strips it), | ||
| * and foo is never imported by the consumer at all, tree-shaking should eliminate | ||
| * both ./db.server and ./foo from the client bundle — so the import-protection | ||
| * plugin should NOT fire violations for either. | ||
| */ | ||
| export { getUsers, type User } from './db.server' | ||
| export { foo } from './foo' | ||
| export { userColumns } from './shared' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /** | ||
| * Shared (non-server) module with client-safe values. | ||
| * These are safe to use on the client. | ||
| */ | ||
| import type { User } from './db.server' | ||
|
|
||
| export const userColumns = { | ||
| name: 'Full Name', | ||
| email: 'Email Address', | ||
| } as const | ||
|
|
||
| /** Placeholder for when no users have loaded yet. */ | ||
| export const emptyUser: User = { id: 0, name: '', email: '' } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * Server-only secret used exclusively inside a route component. | ||
| * The route component is code-split by the router plugin into a separate | ||
| * lazy chunk, so the import to this file ends up in the split module — | ||
| * NOT the original route file. Import protection must still detect this. | ||
| */ | ||
| export const COMPONENT_SECRET = 'component-only-server-secret-99999' | ||
|
|
||
| export function getComponentSecret() { | ||
| return COMPONENT_SECRET | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Split the type-only import and keep value imports sorted.
ESLint flags inline type specifiers and ordering here. Separate
Userinto a type-only import to satisfyimport/consistent-type-specifier-styleandsort-imports.🧹 Suggested fix
🧰 Tools
🪛 ESLint
[error] 8-8: Member 'User' of the import declaration should be sorted alphabetically.
(sort-imports)
[error] 8-8: Prefer using a top-level type-only import instead of inline type specifiers.
(import/consistent-type-specifier-style)
🤖 Prompt for AI Agents